@zq-silk/yui 0.2.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
export const COMPONENTS_SCRIPT = `
|
|
2
|
+
// Reusable widgets and record cards. Everything here is a pure builder:
|
|
3
|
+
// data + i18n in, DOM out. Page composition lives in view.js.
|
|
4
|
+
import { node } from "/assets/js/dom.js";
|
|
5
|
+
import { formatDateTime, relativeTime } from "/assets/js/format.js";
|
|
6
|
+
import { escapeHtml, inlineMarkdown, renderMarkdown } from "/assets/js/markdown.js";
|
|
7
|
+
|
|
8
|
+
export function translatedStatus(t, prefix, status) {
|
|
9
|
+
return t(prefix + "." + status);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Mirrors src/agent/adapterCatalog.ts to avoid a runtime import loop.
|
|
13
|
+
const adapterLabels = { codex: "Codex", claude: "Claude" };
|
|
14
|
+
function adapterLabel(adapterId) {
|
|
15
|
+
return adapterLabels[adapterId] || adapterId;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// --- Small widgets -----------------------------------------------------------
|
|
19
|
+
export function metaItem(label, value) {
|
|
20
|
+
const item = node("span", "detail-meta-item");
|
|
21
|
+
item.append(node("small", "", label), node("span", "", value));
|
|
22
|
+
return item;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function pathMetaItem(label, value) {
|
|
26
|
+
const item = node("span", "detail-meta-item");
|
|
27
|
+
const path = node("span", "meta-path", value);
|
|
28
|
+
path.title = value;
|
|
29
|
+
item.append(node("small", "", label), path);
|
|
30
|
+
return item;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function chip(text, extraClass) {
|
|
34
|
+
return node("span", "chip" + (extraClass ? " " + extraClass : ""), text);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function agentBadge(agent) {
|
|
38
|
+
if (!agent) return null;
|
|
39
|
+
const badge = node("span", "agent-badge");
|
|
40
|
+
if (agent.adapterId) badge.append(chip(adapterLabel(agent.adapterId), "is-adapter"));
|
|
41
|
+
if (agent.model) badge.append(chip(agent.model));
|
|
42
|
+
if (agent.effort) badge.append(chip(agent.effort));
|
|
43
|
+
return badge.childNodes.length ? badge : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function chipRow(label, values, activeValue) {
|
|
47
|
+
const list = (values || []).filter(function (value) { return value !== undefined && value !== null && value !== ""; });
|
|
48
|
+
if (!list.length) return null;
|
|
49
|
+
const block = node("div", "record-block");
|
|
50
|
+
block.append(node("small", "", label));
|
|
51
|
+
const row = node("div", "chip-row");
|
|
52
|
+
list.forEach(function (value) {
|
|
53
|
+
row.append(chip(String(value), activeValue !== undefined && value === activeValue ? "is-active" : ""));
|
|
54
|
+
});
|
|
55
|
+
block.append(row);
|
|
56
|
+
return block;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function pill(t, namespace, status) {
|
|
60
|
+
const element = node("span", "pill", translatedStatus(t, namespace, status));
|
|
61
|
+
element.dataset.status = status;
|
|
62
|
+
return element;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function statusDot(status) {
|
|
66
|
+
const dot = node("span", "status-dot " + status);
|
|
67
|
+
dot.setAttribute("aria-hidden", "true");
|
|
68
|
+
return dot;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function emptyRow(t, key) {
|
|
72
|
+
return node("div", "row is-empty", t(key || "empty.none"));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function sectionHead(label, options) {
|
|
76
|
+
const head = node("div", "section-head");
|
|
77
|
+
head.append(node("h3", "", label));
|
|
78
|
+
if (options && options.count !== undefined) head.append(node("span", "section-count", String(options.count)));
|
|
79
|
+
if (options && options.kicker) head.append(node("span", "section-kicker", options.kicker));
|
|
80
|
+
if (options && options.right) head.append(node("span", "section-label", options.right));
|
|
81
|
+
return head;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function anchorSection(id, head, body) {
|
|
85
|
+
const section = node("section", "detail-section anchor");
|
|
86
|
+
section.id = id;
|
|
87
|
+
section.append(head, body);
|
|
88
|
+
return section;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- Progressive disclosure --------------------------------------------------
|
|
92
|
+
// Re-render is cheap and happens whenever the underlying data changes, so all
|
|
93
|
+
// disclosure state (collapsed blocks, visible pages) is deliberately
|
|
94
|
+
// deterministic per render: long content starts collapsed and expanding is a
|
|
95
|
+
// pure local toggle.
|
|
96
|
+
function shouldCollapse(text, threshold) {
|
|
97
|
+
const limit = threshold || 700;
|
|
98
|
+
const value = String(text);
|
|
99
|
+
if (value.length > limit) return true;
|
|
100
|
+
let lines = 0;
|
|
101
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
102
|
+
if (value[i] === "\\n") lines += 1;
|
|
103
|
+
}
|
|
104
|
+
return lines > 12;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function collapseToggle(block, t) {
|
|
108
|
+
block.classList.add("is-collapsible", "is-collapsed");
|
|
109
|
+
const toggle = node("button", "md-toggle", t("actions.showMore"));
|
|
110
|
+
toggle.type = "button";
|
|
111
|
+
toggle.addEventListener("click", function () {
|
|
112
|
+
const collapsed = block.classList.toggle("is-collapsed");
|
|
113
|
+
toggle.textContent = collapsed ? t("actions.showMore") : t("actions.showLess");
|
|
114
|
+
});
|
|
115
|
+
block.append(toggle);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Rich text block: small label + Markdown body, auto-collapsed when long.
|
|
119
|
+
export function richText(label, text, t, options) {
|
|
120
|
+
if (!text) return null;
|
|
121
|
+
const baseClass = (options && options.className) || "record-block";
|
|
122
|
+
const extra = options && options.extraClass ? " " + options.extraClass : "";
|
|
123
|
+
const block = node("div", baseClass + extra);
|
|
124
|
+
if (label) block.append(node("small", "", label));
|
|
125
|
+
const body = node("div", "md" + (options && options.muted ? " muted" : ""));
|
|
126
|
+
body.innerHTML = renderMarkdown(text);
|
|
127
|
+
block.append(body);
|
|
128
|
+
if (shouldCollapse(text, options && options.threshold)) collapseToggle(block, t);
|
|
129
|
+
return block;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Renders the first page of a long list plus a button that reveals the rest
|
|
133
|
+
// in place, one page at a time.
|
|
134
|
+
export function pagedList(container, items, pageSize, renderItem, t) {
|
|
135
|
+
let shown = 0;
|
|
136
|
+
let more = null;
|
|
137
|
+
function renderChunk() {
|
|
138
|
+
const chunk = items.slice(shown, shown + pageSize);
|
|
139
|
+
chunk.forEach(function (item) {
|
|
140
|
+
container.insertBefore(renderItem(item), more);
|
|
141
|
+
});
|
|
142
|
+
shown += chunk.length;
|
|
143
|
+
if (more) {
|
|
144
|
+
const remaining = items.length - shown;
|
|
145
|
+
if (remaining <= 0) {
|
|
146
|
+
more.remove();
|
|
147
|
+
more = null;
|
|
148
|
+
} else {
|
|
149
|
+
more.textContent = t("actions.showRemaining").replace("{count}", String(remaining));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (items.length > pageSize) {
|
|
154
|
+
more = node("button", "show-more", "");
|
|
155
|
+
more.type = "button";
|
|
156
|
+
more.addEventListener("click", renderChunk);
|
|
157
|
+
container.append(more);
|
|
158
|
+
}
|
|
159
|
+
renderChunk();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function criteriaList(label, items, t) {
|
|
163
|
+
const list = (items || []).filter(Boolean);
|
|
164
|
+
if (!list.length) return null;
|
|
165
|
+
const block = node("div", "record-block is-wide criteria-block");
|
|
166
|
+
block.append(node("small", "", label));
|
|
167
|
+
const listElement = node("ul", "criteria-list");
|
|
168
|
+
list.forEach(function (item) {
|
|
169
|
+
const li = node("li", "");
|
|
170
|
+
li.innerHTML = inlineMarkdown(escapeHtml(item));
|
|
171
|
+
listElement.append(li);
|
|
172
|
+
});
|
|
173
|
+
block.append(listElement);
|
|
174
|
+
if (list.length > 6) collapseToggle(block, t);
|
|
175
|
+
return block;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- Authors ------------------------------------------------------------------
|
|
179
|
+
function messageAuthor(message, t) {
|
|
180
|
+
return message.author.type === "role"
|
|
181
|
+
? message.author.roleName
|
|
182
|
+
: t("author." + message.author.type);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function authorName(t, who) {
|
|
186
|
+
const key = "author." + who;
|
|
187
|
+
const label = t(key);
|
|
188
|
+
return label === key ? who : label;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- Input (attention) cards ---------------------------------------------------
|
|
192
|
+
function answerActions(input, actions, t) {
|
|
193
|
+
const answers = node("div", "input-actions");
|
|
194
|
+
if (input.choices && input.choices.length) {
|
|
195
|
+
input.choices.forEach(function (choice) {
|
|
196
|
+
const button = node("button", "input-answer", choice.label);
|
|
197
|
+
button.type = "button";
|
|
198
|
+
button.addEventListener("click", function () {
|
|
199
|
+
if (actions.answerInput) actions.answerInput(input, { choiceKey: choice.key });
|
|
200
|
+
});
|
|
201
|
+
answers.append(button);
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
const form = node("form", "input-form");
|
|
205
|
+
const field = node("input", "");
|
|
206
|
+
field.type = "text";
|
|
207
|
+
field.required = true;
|
|
208
|
+
field.placeholder = t("input.freeText");
|
|
209
|
+
const submit = node("button", "input-answer", t("actions.answer"));
|
|
210
|
+
submit.type = "submit";
|
|
211
|
+
form.append(field, submit);
|
|
212
|
+
form.addEventListener("submit", function (event) {
|
|
213
|
+
event.preventDefault();
|
|
214
|
+
const text = field.value.trim();
|
|
215
|
+
if (text && actions.answerInput) actions.answerInput(input, { text: text });
|
|
216
|
+
});
|
|
217
|
+
answers.append(form);
|
|
218
|
+
}
|
|
219
|
+
return answers;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function inputCard(input, _options, t, locale, actions) {
|
|
223
|
+
const card = node("article", "input-card");
|
|
224
|
+
const top = node("div", "input-card-top");
|
|
225
|
+
top.append(node("small", "", t("detail.openInput")));
|
|
226
|
+
const question = node("p", "input-question", input.question);
|
|
227
|
+
top.append(question);
|
|
228
|
+
const context = node("div", "input-context");
|
|
229
|
+
if (input.policy) {
|
|
230
|
+
if (input.policy.kind === "recommended") {
|
|
231
|
+
context.append(pill(t, "input", "recommended"));
|
|
232
|
+
if (input.policy.timeoutAt) {
|
|
233
|
+
context.append(node("span", "", t("input.timeoutAt") + " · " + relativeTime(input.policy.timeoutAt, locale, t)));
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
context.append(pill(t, "input", "required"));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
context.append(node("time", "", formatDateTime(input.createdAt, locale)));
|
|
240
|
+
top.append(context);
|
|
241
|
+
if (input.blockedRefs && input.blockedRefs.length) {
|
|
242
|
+
const blocked = input.blockedRefs.map(function (ref) { return ref.type + "·" + ref.id; }).join(" ");
|
|
243
|
+
top.append(node("span", "input-blocked", blocked));
|
|
244
|
+
}
|
|
245
|
+
card.append(top);
|
|
246
|
+
card.append(answerActions(input, actions, t));
|
|
247
|
+
return card;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- Conclusion band ------------------------------------------------------------
|
|
251
|
+
export function conclusionMeta(task, t, locale, kind) {
|
|
252
|
+
const meta = node("div", "conclusion-meta");
|
|
253
|
+
const actor = kind === "archived" ? task.archivedBy
|
|
254
|
+
: kind === "retired" ? task.retiredBy
|
|
255
|
+
: task.completedBy;
|
|
256
|
+
const at = kind === "archived" ? task.archivedAt
|
|
257
|
+
: kind === "retired" ? task.retiredAt
|
|
258
|
+
: task.completedAt;
|
|
259
|
+
const label = kind === "archived" ? t("detail.archivedBy")
|
|
260
|
+
: kind === "retired" ? t("detail.retiredBy")
|
|
261
|
+
: t("detail.completedBy");
|
|
262
|
+
if (actor) meta.append(node("span", "", label + " · " + authorName(t, actor)));
|
|
263
|
+
if (at) meta.append(node("time", "", formatDateTime(at, locale)));
|
|
264
|
+
return meta;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// --- Record cards -----------------------------------------------------------------
|
|
268
|
+
// WorkItems that no longer need action render collapsed; everything actionable
|
|
269
|
+
// (pending, running, awaiting acceptance, failed) stays expanded.
|
|
270
|
+
const WORK_ITEM_OPEN_STATUSES = ["pending", "running", "awaiting_acceptance", "failed"];
|
|
271
|
+
|
|
272
|
+
export function workItemCard(item, titles, t, locale, actions, taskId) {
|
|
273
|
+
const collapsible = WORK_ITEM_OPEN_STATUSES.indexOf(item.status) === -1;
|
|
274
|
+
const card = node(collapsible ? "details" : "article", "record-card work-item-card");
|
|
275
|
+
const head = node(collapsible ? "summary" : "div", "record-head");
|
|
276
|
+
const titleRow = node("div", "record-title-row");
|
|
277
|
+
titleRow.append(statusDot(item.status), node("strong", "record-title", item.title));
|
|
278
|
+
const pills = node("div", "record-pills");
|
|
279
|
+
pills.append(pill(t, "work", item.status));
|
|
280
|
+
if (item.workspaceDisposition) pills.append(pill(t, "disposition", item.workspaceDisposition));
|
|
281
|
+
head.append(titleRow, pills);
|
|
282
|
+
card.append(head);
|
|
283
|
+
|
|
284
|
+
const body = node("div", "work-item-body");
|
|
285
|
+
const meta = node("div", "record-meta");
|
|
286
|
+
if (item.assignee) meta.append(node("span", "meta-name", item.assignee));
|
|
287
|
+
meta.append(node("span", "mono", item.id));
|
|
288
|
+
meta.append(node("time", "", formatDateTime(item.updatedAt, locale)));
|
|
289
|
+
body.append(meta);
|
|
290
|
+
|
|
291
|
+
if (item.objective && item.objective !== item.title) {
|
|
292
|
+
body.append(richText(t("detail.objective"), item.objective, t));
|
|
293
|
+
}
|
|
294
|
+
if (item.acceptance && item.acceptance.length) {
|
|
295
|
+
body.append(criteriaList(t("detail.acceptance"), item.acceptance, t));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Short chip rows (dependencies, writable projects) sit side by side; long
|
|
299
|
+
// text blocks above and below stay full-width so the card reads top-to-bottom
|
|
300
|
+
// like an issue rather than a mismatched column grid.
|
|
301
|
+
const chipCols = node("div", "work-item-chips");
|
|
302
|
+
if (item.dependsOn && item.dependsOn.length) {
|
|
303
|
+
chipCols.append(chipRow(t("detail.dependsOn"), item.dependsOn.map(function (id) { return titles[id] || id; })));
|
|
304
|
+
}
|
|
305
|
+
if (item.writeProjectIds && item.writeProjectIds.length) {
|
|
306
|
+
chipCols.append(chipRow(t("detail.writeProjects"), item.writeProjectIds));
|
|
307
|
+
}
|
|
308
|
+
if (chipCols.childNodes.length) body.append(chipCols);
|
|
309
|
+
|
|
310
|
+
if (item.outcome) {
|
|
311
|
+
body.append(richText(t("detail.outcome"), item.outcome, t, { extraClass: "outcome-callout", muted: true }));
|
|
312
|
+
}
|
|
313
|
+
card.append(body);
|
|
314
|
+
return card;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function runCard(run, t, locale) {
|
|
318
|
+
const card = node("article", "execute-card");
|
|
319
|
+
card.dataset.status = run.status;
|
|
320
|
+
|
|
321
|
+
const idRow = node("div", "execute-id");
|
|
322
|
+
idRow.append(statusDot(run.status));
|
|
323
|
+
idRow.append(node("span", "role", run.roleName));
|
|
324
|
+
idRow.append(node("span", "", run.id));
|
|
325
|
+
if (run.workItemId) idRow.append(node("span", "", t("detail.workItem") + " · " + run.workItemId));
|
|
326
|
+
idRow.append(node("time", "", formatDateTime(run.updatedAt, locale)));
|
|
327
|
+
card.append(idRow);
|
|
328
|
+
|
|
329
|
+
card.append(richText(t("detail.instruction"), run.input, t, { className: "execute-io", threshold: 320 }));
|
|
330
|
+
if (run.summary) {
|
|
331
|
+
card.append(richText(t("detail.outcome"), run.summary, t, { className: "execute-io outcome", threshold: 320 }));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const foot = node("div", "execute-foot");
|
|
335
|
+
const tags = node("div", "execute-tags");
|
|
336
|
+
tags.append(chip(t("mode." + run.mode)));
|
|
337
|
+
const deliveryKey = run.deliveredAt
|
|
338
|
+
? "delivery.delivered"
|
|
339
|
+
: run.pushedAt
|
|
340
|
+
? "delivery.pushed"
|
|
341
|
+
: "delivery.pending";
|
|
342
|
+
tags.append(chip(t(deliveryKey)));
|
|
343
|
+
const badge = run.effective ? agentBadge(run.effective) : (run.agentId ? agentBadge(run) : null);
|
|
344
|
+
if (badge) tags.append(badge);
|
|
345
|
+
foot.append(tags);
|
|
346
|
+
foot.append(pill(t, "run", run.status));
|
|
347
|
+
card.append(foot);
|
|
348
|
+
|
|
349
|
+
if (run.effective) {
|
|
350
|
+
const eff = node("div", "record-meta");
|
|
351
|
+
eff.append(node("span", "", t("detail.effective") + " · r" + run.effective.sourceDesiredRevision));
|
|
352
|
+
eff.append(node("span", "", t("detail.profileIntent") + " · " + run.effective.profileAccess));
|
|
353
|
+
eff.append(node("span", "", t("detail.permission") + " · " + run.effective.permission.strategy));
|
|
354
|
+
card.append(eff);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return card;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function reviewCard(round, t, locale) {
|
|
361
|
+
const card = node("article", "record-card");
|
|
362
|
+
const head = node("div", "record-head");
|
|
363
|
+
const titleRow = node("div", "record-title-row");
|
|
364
|
+
titleRow.append(node("strong", "record-title", round.id));
|
|
365
|
+
head.append(titleRow);
|
|
366
|
+
head.append(pill(t, "review", round.status));
|
|
367
|
+
card.append(head);
|
|
368
|
+
const meta = node("div", "record-meta");
|
|
369
|
+
meta.append(node("span", "meta-name", round.reviewerRoleName));
|
|
370
|
+
meta.append(node("span", "mono", round.workItemId + " · " + round.candidateId));
|
|
371
|
+
if (round.createdAt) meta.append(node("time", "", formatDateTime(round.createdAt, locale)));
|
|
372
|
+
meta.append(node("span", "", t("detail.reviewBase") + " · " + round.reviewBaseCommit));
|
|
373
|
+
if (round.workspace && round.workspace.root) {
|
|
374
|
+
meta.append(pathMetaItem(t("detail.workspace"), round.workspace.root));
|
|
375
|
+
}
|
|
376
|
+
if (round.evidenceCommit) {
|
|
377
|
+
meta.append(node("span", "", t("detail.evidence") + " · " + round.evidenceCommit));
|
|
378
|
+
}
|
|
379
|
+
card.append(meta);
|
|
380
|
+
if (round.checks && round.checks.length) {
|
|
381
|
+
card.append(richText(t("detail.checks"), round.checks.map(function (check) {
|
|
382
|
+
return check.name + "=" + check.outcome;
|
|
383
|
+
}).join(", "), t));
|
|
384
|
+
}
|
|
385
|
+
if (round.summary) card.append(richText(null, round.summary, t));
|
|
386
|
+
return card;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function roleCard(role, task, t, locale, actions) {
|
|
390
|
+
const card = node("article", "record-card");
|
|
391
|
+
const head = node("div", "record-head");
|
|
392
|
+
head.append(node("strong", "record-title", role.name));
|
|
393
|
+
const headRight = node("div", "record-pills");
|
|
394
|
+
headRight.append(pill(t, "role", role.status));
|
|
395
|
+
const open = node("button", "record-open", "");
|
|
396
|
+
open.type = "button";
|
|
397
|
+
open.append(node("span", "", t("actions.openRun")), node("span", "arrow", "→"));
|
|
398
|
+
open.addEventListener("click", function () {
|
|
399
|
+
if (actions.openTerminal) actions.openTerminal({ scope: "task", taskId: task.id, roleName: role.name });
|
|
400
|
+
});
|
|
401
|
+
headRight.append(open);
|
|
402
|
+
head.append(headRight);
|
|
403
|
+
card.append(head);
|
|
404
|
+
|
|
405
|
+
const meta = node("div", "record-meta");
|
|
406
|
+
meta.append(node("span", "meta-name", role.activeAgentId));
|
|
407
|
+
const activeBinding = role.agentBindings && role.agentBindings[role.activeAgentId];
|
|
408
|
+
if (activeBinding) {
|
|
409
|
+
const badge = agentBadge({
|
|
410
|
+
adapterId: activeBinding.adapterId,
|
|
411
|
+
model: activeBinding.config && activeBinding.config.model,
|
|
412
|
+
effort: activeBinding.config && activeBinding.config.effort
|
|
413
|
+
});
|
|
414
|
+
if (badge) meta.append(badge);
|
|
415
|
+
}
|
|
416
|
+
if (role.launchRevision !== undefined) {
|
|
417
|
+
meta.append(node("span", "", t("detail.desired") + " · r" + role.launchRevision));
|
|
418
|
+
}
|
|
419
|
+
if (role.defaultAccess !== undefined) {
|
|
420
|
+
meta.append(node("span", "", t("detail.profileIntent") + " · " + role.defaultAccess));
|
|
421
|
+
}
|
|
422
|
+
if (role.updatedAt) meta.append(node("time", "", formatDateTime(role.updatedAt, locale)));
|
|
423
|
+
card.append(meta);
|
|
424
|
+
|
|
425
|
+
if (role.effectiveLaunch) {
|
|
426
|
+
const eff = node("div", "record-meta");
|
|
427
|
+
eff.append(node("span", "", t("detail.effectiveAgent") + " · " + role.effectiveLaunch.agentId));
|
|
428
|
+
const effBadge = agentBadge(role.effectiveLaunch);
|
|
429
|
+
if (effBadge) eff.append(effBadge);
|
|
430
|
+
if (role.effectiveLaunch.sourceDesiredRevision !== undefined) {
|
|
431
|
+
eff.append(node("span", "", t("detail.effective") + " · r" + role.effectiveLaunch.sourceDesiredRevision));
|
|
432
|
+
}
|
|
433
|
+
if (role.effectiveLaunch.profileAccess !== undefined) {
|
|
434
|
+
eff.append(node("span", "", t("detail.profileIntent") + " · " + role.effectiveLaunch.profileAccess));
|
|
435
|
+
}
|
|
436
|
+
if (role.effectiveLaunch.permission && role.effectiveLaunch.permission.strategy !== undefined) {
|
|
437
|
+
eff.append(node("span", "", t("detail.permission") + " · " + role.effectiveLaunch.permission.strategy));
|
|
438
|
+
}
|
|
439
|
+
eff.append(node("span", "", role.launchDrift ? t("launch.drift") : t("launch.current")));
|
|
440
|
+
card.append(eff);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (role.description) card.append(richText(null, role.description, t, { muted: true }));
|
|
444
|
+
|
|
445
|
+
const cols = node("div", "record-cols");
|
|
446
|
+
const bindingIds = role.agentBindings ? Object.keys(role.agentBindings) : [];
|
|
447
|
+
if (bindingIds.length > 1) {
|
|
448
|
+
const bindings = bindingIds.map(function (id) {
|
|
449
|
+
const binding = role.agentBindings[id];
|
|
450
|
+
const model = binding.config && binding.config.model;
|
|
451
|
+
return adapterLabel(binding.adapterId) + (model ? " · " + model : "");
|
|
452
|
+
});
|
|
453
|
+
const activeBindingLabel = activeBinding
|
|
454
|
+
? adapterLabel(activeBinding.adapterId) + (activeBinding.config && activeBinding.config.model ? " · " + activeBinding.config.model : "")
|
|
455
|
+
: undefined;
|
|
456
|
+
cols.append(chipRow(t("detail.agents"), bindings, activeBindingLabel));
|
|
457
|
+
}
|
|
458
|
+
if (role.skills && role.skills.length) cols.append(chipRow(t("detail.skills"), role.skills));
|
|
459
|
+
if (role.responsibilities && role.responsibilities.length) cols.append(criteriaList(t("detail.responsibilities"), role.responsibilities, t));
|
|
460
|
+
if (role.constraints && role.constraints.length) cols.append(criteriaList(t("detail.constraints"), role.constraints, t));
|
|
461
|
+
if (role.expectedOutput) cols.append(richText(t("detail.expectedOutput"), role.expectedOutput, t, { muted: true }));
|
|
462
|
+
if (cols.childNodes.length) card.append(cols);
|
|
463
|
+
|
|
464
|
+
if (role.workspace) {
|
|
465
|
+
const workspaceMeta = node("div", "record-meta");
|
|
466
|
+
workspaceMeta.append(pathMetaItem(t("detail.workspace"), role.workspace));
|
|
467
|
+
card.append(workspaceMeta);
|
|
468
|
+
}
|
|
469
|
+
return card;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function historyEventRow(event, t, locale) {
|
|
473
|
+
if (event.kind === "milestone") {
|
|
474
|
+
const milestone = event.item;
|
|
475
|
+
const card = node("article", "record-card");
|
|
476
|
+
const head = node("div", "record-head");
|
|
477
|
+
const titleRow = node("div", "record-title-row");
|
|
478
|
+
titleRow.append(node("strong", "record-title", milestone.title));
|
|
479
|
+
head.append(titleRow);
|
|
480
|
+
head.append(pill(t, "history", "milestone"));
|
|
481
|
+
card.append(head);
|
|
482
|
+
const meta = node("div", "record-meta");
|
|
483
|
+
meta.append(node("span", "mono", milestone.id));
|
|
484
|
+
meta.append(node("time", "", formatDateTime(milestone.createdAt, locale)));
|
|
485
|
+
card.append(meta);
|
|
486
|
+
if (milestone.summary) card.append(richText(null, milestone.summary, t, { muted: true }));
|
|
487
|
+
return card;
|
|
488
|
+
}
|
|
489
|
+
const decision = event.item;
|
|
490
|
+
const card = node("article", "record-card");
|
|
491
|
+
const head = node("div", "record-head");
|
|
492
|
+
const titleRow = node("div", "record-title-row");
|
|
493
|
+
titleRow.append(node("strong", "record-title", decision.title));
|
|
494
|
+
head.append(titleRow);
|
|
495
|
+
head.append(pill(t, "history", "decision"));
|
|
496
|
+
card.append(head);
|
|
497
|
+
const meta = node("div", "record-meta");
|
|
498
|
+
meta.append(node("span", "mono", decision.id));
|
|
499
|
+
if (decision.createdAt) meta.append(node("time", "", formatDateTime(decision.createdAt, locale)));
|
|
500
|
+
if (decision.status) meta.append(pill(t, "decision", decision.status));
|
|
501
|
+
card.append(meta);
|
|
502
|
+
if (decision.rationale) card.append(richText(null, decision.rationale, t, { muted: true }));
|
|
503
|
+
if (decision.supersededReason) card.append(richText(null, decision.supersededReason, t, { muted: true }));
|
|
504
|
+
return card;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function messageCard(message, t, locale) {
|
|
508
|
+
const card = node("article", "record-card");
|
|
509
|
+
const head = node("div", "record-head");
|
|
510
|
+
const titleRow = node("div", "record-title-row");
|
|
511
|
+
titleRow.append(node("strong", "record-title", messageAuthor(message, t)));
|
|
512
|
+
head.append(titleRow);
|
|
513
|
+
if (message.kind) head.append(pill(t, "messageKind", message.kind));
|
|
514
|
+
card.append(head);
|
|
515
|
+
|
|
516
|
+
const meta = node("div", "record-meta");
|
|
517
|
+
meta.append(node("span", "mono", message.id));
|
|
518
|
+
meta.append(node("time", "", formatDateTime(message.createdAt, locale)));
|
|
519
|
+
if (message.runId) meta.append(node("span", "mono", message.runId + (message.workItemId ? " · " + message.workItemId : "")));
|
|
520
|
+
card.append(meta);
|
|
521
|
+
|
|
522
|
+
card.append(richText(null, message.body, t));
|
|
523
|
+
return card;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// --- Overview + sidebar rows --------------------------------------------------
|
|
527
|
+
export function metricTile(label, value, options) {
|
|
528
|
+
const variant = options && options.hot ? " is-hot" : options && options.warning ? " is-warning" : "";
|
|
529
|
+
const tile = node("article", "metric" + variant);
|
|
530
|
+
tile.append(node("span", "metric-label", label), node("strong", "metric-value", String(value)));
|
|
531
|
+
return tile;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function attentionRow(item, t, locale, onSelect) {
|
|
535
|
+
const row = node("button", "inbox-row");
|
|
536
|
+
row.type = "button";
|
|
537
|
+
const request = item.request || {};
|
|
538
|
+
const kind = request.policy && request.policy.kind === "required" ? "required"
|
|
539
|
+
: request.policy && request.policy.kind === "recommended" ? "recommended"
|
|
540
|
+
: null;
|
|
541
|
+
|
|
542
|
+
const lead = node("span", "inbox-lead");
|
|
543
|
+
lead.append(node("span", "inbox-dot"));
|
|
544
|
+
const head = node("span", "inbox-head");
|
|
545
|
+
head.append(node("span", "inbox-task", item.taskTitle));
|
|
546
|
+
if (kind) head.append(pill(t, "input", kind));
|
|
547
|
+
lead.append(head);
|
|
548
|
+
row.append(lead);
|
|
549
|
+
|
|
550
|
+
if (request.question) {
|
|
551
|
+
const question = node("span", "inbox-question", request.question);
|
|
552
|
+
question.title = request.question;
|
|
553
|
+
row.append(question);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const foot = node("span", "inbox-foot");
|
|
557
|
+
if (request.createdAt) foot.append(node("time", "", relativeTime(request.createdAt, locale, t)));
|
|
558
|
+
foot.append(node("span", "inbox-go", t("actions.answer") + " →"));
|
|
559
|
+
row.append(foot);
|
|
560
|
+
|
|
561
|
+
row.addEventListener("click", function () { onSelect(item.taskId); });
|
|
562
|
+
return row;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export function overviewRow(task, t, locale, onSelect) {
|
|
566
|
+
const hasInputs = task.openInputCount > 0;
|
|
567
|
+
const row = node("button", "overview-row" + (hasInputs ? " has-inputs" : ""));
|
|
568
|
+
row.type = "button";
|
|
569
|
+
row.append(statusDot(task.status));
|
|
570
|
+
const main = node("span", "overview-row-title", task.title);
|
|
571
|
+
main.title = task.title;
|
|
572
|
+
row.append(main);
|
|
573
|
+
row.append(node("span", "overview-row-time", relativeTime(task.updatedAt, locale, t)));
|
|
574
|
+
row.addEventListener("click", function () { onSelect(task.id); });
|
|
575
|
+
return row;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function taskCard(task, hasAttention, state, t, locale, onSelect) {
|
|
579
|
+
const button = node("button", "task");
|
|
580
|
+
button.type = "button";
|
|
581
|
+
button.dataset.id = task.id;
|
|
582
|
+
button.setAttribute("aria-current", String(state.selected === task.id));
|
|
583
|
+
|
|
584
|
+
button.append(statusDot(task.status));
|
|
585
|
+
|
|
586
|
+
const main = node("span", "task-main");
|
|
587
|
+
main.append(node("strong", "task-title", task.title));
|
|
588
|
+
main.append(node("span", "task-meta", relativeTime(task.updatedAt, locale, t)));
|
|
589
|
+
button.append(main);
|
|
590
|
+
|
|
591
|
+
// One signal only: an open input (needs the user) outranks a running count.
|
|
592
|
+
if (hasAttention) {
|
|
593
|
+
const badge = node("span", "task-signal is-input", String(task.openInputCount || 1));
|
|
594
|
+
badge.title = t("stats.inputs");
|
|
595
|
+
button.append(badge);
|
|
596
|
+
} else if (task.workItems && task.workItems.running > 0) {
|
|
597
|
+
const badge = node("span", "task-signal is-running", String(task.workItems.running));
|
|
598
|
+
badge.title = t("stats.running");
|
|
599
|
+
button.append(badge);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
button.addEventListener("click", function () { onSelect(task.id); });
|
|
603
|
+
return button;
|
|
604
|
+
}
|
|
605
|
+
`;
|
|
@@ -0,0 +1,14 @@
|
|
|
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.
|
|
4
|
+
export function node(tagName, className, textContent) {
|
|
5
|
+
const element = document.createElement(tagName);
|
|
6
|
+
if (className) element.className = className;
|
|
7
|
+
if (textContent !== undefined && textContent !== null) element.textContent = String(textContent);
|
|
8
|
+
return element;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function clear(element) {
|
|
12
|
+
element.replaceChildren();
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const FORMAT_SCRIPT = `
|
|
2
|
+
// Date / time formatting helpers shared by cards and views.
|
|
3
|
+
export function relativeTime(iso, locale, t) {
|
|
4
|
+
const seconds = Math.round((Date.parse(iso) - Date.now()) / 1000);
|
|
5
|
+
if (!Number.isFinite(seconds) || Math.abs(seconds) < 45) return t("time.justNow");
|
|
6
|
+
const units = [
|
|
7
|
+
["year", 31536000],
|
|
8
|
+
["month", 2592000],
|
|
9
|
+
["day", 86400],
|
|
10
|
+
["hour", 3600],
|
|
11
|
+
["minute", 60]
|
|
12
|
+
];
|
|
13
|
+
const selected = units.find(function (entry) { return Math.abs(seconds) >= entry[1]; }) || ["second", 1];
|
|
14
|
+
return new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(
|
|
15
|
+
Math.round(seconds / selected[1]),
|
|
16
|
+
selected[0]
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatDateTime(iso, locale) {
|
|
21
|
+
if (!iso || !Number.isFinite(Date.parse(iso))) return "—";
|
|
22
|
+
return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(new Date(iso));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function byNewest(left, right) {
|
|
26
|
+
return Date.parse(right.updatedAt || right.createdAt) - Date.parse(left.updatedAt || left.createdAt);
|
|
27
|
+
}
|
|
28
|
+
`;
|