@zq-silk/yui 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/artifacts/artifactCapability.js +39 -10
- package/dist/artifacts/imageMetadata.js +120 -0
- package/dist/artifacts/managedGit.js +7 -4
- package/dist/artifacts/taskArtifactRepository.js +6 -0
- package/dist/cli/commandCatalog.js +18 -9
- package/dist/cli/invocationAuthority.js +1 -1
- package/dist/commands/executionAuditCommands.js +5 -0
- package/dist/commands/globalRoleCommands.js +41 -3
- package/dist/commands/projectCommands.js +89 -94
- package/dist/commands/roleSkillValidation.js +4 -8
- package/dist/commands/taskCommands.js +65 -14
- package/dist/commands/taskUsageCommand.js +38 -0
- package/dist/context/roleSessionContext.js +18 -2
- package/dist/context/runContextPack.js +5 -1
- package/dist/context/sessionBootstrapManifest.js +18 -3
- package/dist/context/skillCatalog.js +108 -0
- package/dist/context/skillPackage.js +161 -0
- package/dist/context/taskContext.js +3 -2
- package/dist/context/taskSearch.js +127 -0
- package/dist/controlPlaneCli.js +25 -20
- package/dist/controller/fileSchedulerStoreAdapter.js +39 -8
- package/dist/controller/globalInputDelivery.js +2 -4
- package/dist/controller/jobControl.js +2 -0
- package/dist/controller/nativeHostInput.js +37 -0
- package/dist/controller/providerRetryDelivery.js +4 -2
- package/dist/controller/runtime.js +93 -2
- package/dist/controller/runtimeEventInbox.js +2 -2
- package/dist/controller/runtimeEventProcessor.js +3 -3
- package/dist/doctor/doctor.js +10 -0
- package/dist/execution/reviewMainRun.js +1 -0
- package/dist/execution/workItemMainRun.js +1 -0
- package/dist/executor/agentAdapter.js +3 -5
- package/dist/executor/codexConfigConflict.js +6 -4
- package/dist/executor/effectiveLaunch.js +24 -1
- package/dist/executor/fileRoleLaunchPlanner.js +15 -7
- package/dist/integration/gitIntegrationService.js +7 -12
- package/dist/integration/integrationSourceApplication.js +4 -2
- package/dist/interaction/notificationPresentation.js +85 -0
- package/dist/interaction/operatorPresentation.js +1 -2
- package/dist/job/httpPreview.js +61 -0
- package/dist/job/jobRunner.js +12 -2
- package/dist/kernel/builtinCapabilities.js +3 -1
- package/dist/message/message.js +15 -1
- package/dist/output/rolePresentation.js +4 -1
- package/dist/repository/executionLaneGitSnapshot.js +2 -1
- package/dist/repository/gitCompatibility.js +62 -0
- package/dist/repository/gitWorkspace.js +48 -71
- package/dist/repository/gitWorktreeInventory.js +121 -0
- package/dist/repository/project.js +74 -25
- package/dist/resources/resourceDiscovery.js +2 -2
- package/dist/resources/resourceGc.js +7 -4
- package/dist/resources/resourceRegistrar.js +2 -1
- package/dist/runtime/agentEndpoint.js +8 -0
- package/dist/runtime/agentHost.js +170 -3
- package/dist/runtime/codexAppServerRuntime.js +1 -1
- package/dist/runtime/codexNativeAccess.js +175 -0
- package/dist/runtime/controlledCodexTui.js +116 -0
- package/dist/runtime/publicReply.js +23 -0
- package/dist/runtime/runtimeObservation.js +48 -0
- package/dist/runtime/structuredProviderHost.js +16 -7
- package/dist/runtime/taskCostMetrics.js +133 -0
- package/dist/runtime/taskUsageMetrics.js +11 -2
- package/dist/runtime/taskUsageQuery.js +38 -0
- package/dist/scheduler/leaderWakeupProcessor.js +1 -9
- package/dist/scheduler/taskExecutionProjection.js +2 -0
- package/dist/scheduler/taskObservabilityProjection.js +9 -1
- package/dist/storage/homeLayout.js +13 -0
- package/dist/storage/sqliteSchema.js +44 -0
- package/dist/storage/sqliteStore.js +53 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskSearch.js +35 -0
- package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
- package/dist/task/ordinaryArchive.js +42 -0
- package/dist/tmux/tmuxManager.js +8 -8
- package/dist/web/assets/assetManifest.js +11 -51
- package/dist/web/assets/client/app/catalog.js +215 -0
- package/dist/web/assets/client/app/main.js +148 -0
- package/dist/web/assets/client/app/selection.js +139 -0
- package/dist/web/assets/client/app/settings.js +326 -0
- package/dist/web/assets/client/app/taskView.js +280 -0
- package/dist/web/assets/client/domain/context.js +96 -0
- package/dist/web/assets/client/domain/records.js +126 -0
- package/dist/web/assets/client/domain/runs.js +119 -0
- package/dist/web/assets/client/domain/taskForms.js +197 -0
- package/dist/web/assets/client/domain/vocab.js +69 -0
- package/dist/web/assets/client/domain/work.js +116 -0
- package/dist/web/assets/client/i18n/en.js +900 -0
- package/dist/web/assets/client/i18n/zh-CN.js +898 -0
- package/dist/web/assets/client/index.js +114 -0
- package/dist/web/assets/client/layout/resizer.js +37 -0
- package/dist/web/assets/client/layout/router.js +35 -0
- package/dist/web/assets/client/layout/shortcuts.js +45 -0
- package/dist/web/assets/client/layout/sizing.js +95 -0
- package/dist/web/assets/client/layout/workspace.js +247 -0
- package/dist/web/assets/client/{api.js → lib/api.js} +20 -4
- package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
- package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
- package/dist/web/assets/client/lib/i18n.js +54 -0
- package/dist/web/assets/client/lib/markdown.js +167 -0
- package/dist/web/assets/client/lib/prefs.js +28 -0
- package/dist/web/assets/client/lib/theme.js +68 -0
- package/dist/web/assets/client/ui/containers.js +101 -0
- package/dist/web/assets/client/ui/controls.js +51 -0
- package/dist/web/assets/client/ui/forms.js +48 -0
- package/dist/web/assets/client/ui/metrics.js +137 -0
- package/dist/web/assets/client/ui/primitives.js +92 -0
- package/dist/web/assets/client/ui/text.js +33 -0
- package/dist/web/assets/client/ui/toast.js +12 -0
- package/dist/web/assets/client/views/bodySearch.js +172 -0
- package/dist/web/assets/client/views/createTask.js +85 -0
- package/dist/web/assets/client/views/dock/conversation.js +612 -0
- package/dist/web/assets/client/views/dock/discussion.js +173 -0
- package/dist/web/assets/client/views/dock/preview.js +129 -0
- package/dist/web/assets/client/views/dock/terminal.js +199 -0
- package/dist/web/assets/client/views/globalInput.js +67 -0
- package/dist/web/assets/client/{overview.js → views/overview.js} +61 -51
- package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
- package/dist/web/assets/client/views/skills.js +152 -0
- package/dist/web/assets/client/views/task/actions.js +36 -0
- package/dist/web/assets/client/views/task/delivery.js +185 -0
- package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
- package/dist/web/assets/client/views/task/files.js +272 -0
- package/dist/web/assets/client/views/task/observation.js +153 -0
- package/dist/web/assets/client/views/task/overview.js +161 -0
- package/dist/web/assets/client/views/task/page.js +131 -0
- package/dist/web/assets/client/views/task/records.js +163 -0
- package/dist/web/assets/client/views/task/runtime.js +130 -0
- package/dist/web/assets/client/views/task/timeline.js +190 -0
- package/dist/web/assets/shared/geometry.js +19 -0
- package/dist/web/assets/shared/taskTabs.js +8 -0
- package/dist/web/assets/shared/themes.js +93 -0
- package/dist/web/assets/shell/dialogs.js +63 -0
- package/dist/web/assets/shell/dock.js +40 -0
- package/dist/web/assets/shell/index.js +40 -0
- package/dist/web/assets/shell/settings.js +34 -0
- package/dist/web/assets/shell/sidebar.js +45 -0
- package/dist/web/assets/styles/index.js +41 -0
- package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +10 -10
- package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
- package/dist/web/assets/styles/markdown.js +22 -3
- package/dist/web/assets/styles/tokens.js +10 -63
- package/dist/web/assets/styles/ui/controls.js +76 -0
- package/dist/web/assets/styles/ui/forms.js +46 -0
- package/dist/web/assets/styles/ui/surfaces.js +122 -0
- package/dist/web/assets/styles/views/delivery.js +29 -0
- package/dist/web/assets/styles/views/dock.js +71 -0
- package/dist/web/assets/styles/views/page.js +32 -0
- package/dist/web/assets/styles/views/records.js +70 -0
- package/dist/web/assets/styles/views/settings.js +26 -0
- package/dist/web/assets/styles/views/sidebar.js +74 -0
- package/dist/web/assets/styles/views/task.js +80 -0
- package/dist/web/assets/styles/views/terminal.js +38 -0
- package/dist/web/taskPreviews.js +205 -0
- package/dist/web/tmuxWebTerminal.js +16 -9
- package/dist/web/webConversation.js +373 -0
- package/dist/web/webMaterials.js +155 -0
- package/dist/web/webServer.js +295 -18
- package/dist/web/webSettings.js +331 -0
- package/dist/web/webSkills.js +86 -0
- package/dist/web/webSnapshot.js +40 -8
- package/dist/web/webTaskSurface.js +106 -8
- package/dist/workspace/gitChangeSetCapture.js +3 -1
- package/docs/cli-information-contract.md +1 -1
- package/docs/cli-information-contract.zh-CN.md +1 -1
- package/docs/managed-turn-and-session-runtime.md +31 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +22 -0
- package/docs/release-workflow.md +54 -0
- package/docs/release-workflow.zh-CN.md +39 -0
- package/docs/storage-baseline.md +38 -1
- package/docs/storage-baseline.zh-CN.md +22 -0
- package/docs/task-discovery.md +28 -0
- package/docs/testing/verification-levels.md +2 -2
- package/i18n/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +5 -4
- package/skills/yui-leader/references/execution.md +2 -1
- package/skills/yui-operator/SKILL.md +3 -0
- package/skills/yui-reviewer/SKILL.md +4 -2
- package/skills/yui-runtime/SKILL.md +43 -2
- package/skills/yui-worker/SKILL.md +3 -2
- package/dist/web/assets/client/app.js +0 -808
- package/dist/web/assets/client/components.js +0 -477
- package/dist/web/assets/client/detail.js +0 -379
- package/dist/web/assets/client/dock.js +0 -309
- package/dist/web/assets/client/evidence.js +0 -364
- package/dist/web/assets/client/forms.js +0 -230
- package/dist/web/assets/client/i18n.js +0 -1277
- package/dist/web/assets/client/markdown.js +0 -114
- package/dist/web/assets/client/records.js +0 -198
- package/dist/web/assets/client/sections.js +0 -349
- package/dist/web/assets/client/theme.js +0 -71
- package/dist/web/assets/shell.js +0 -161
- package/dist/web/assets/styles/components.js +0 -216
- package/dist/web/assets/styles/views.js +0 -309
- /package/dist/web/assets/{icons.js → shared/icons.js} +0 -0
|
@@ -1,75 +1,87 @@
|
|
|
1
1
|
export const OVERVIEW_SCRIPT = String.raw `
|
|
2
|
-
// Workspace overview, shown in the center while no Task is selected
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
// Workspace overview, shown in the center while no Task is selected: one
|
|
3
|
+
// reading column with the work path first (answer, in progress, recent),
|
|
4
|
+
// then catalog-wide signals and the explicit Session read.
|
|
5
|
+
import { h, icon, clear } from "/assets/js/lib/dom.js";
|
|
6
|
+
import { byNewest, fill, formatDateTime, relativeTime } from "/assets/js/lib/format.js";
|
|
7
|
+
import { badge, button, dot, emptyState, note } from "/assets/js/ui/primitives.js";
|
|
8
|
+
import { card } from "/assets/js/ui/containers.js";
|
|
9
|
+
import { metricTile } from "/assets/js/ui/metrics.js";
|
|
10
|
+
import { label, sessionBadges, tone } from "/assets/js/domain/vocab.js";
|
|
11
|
+
import { ATTENTION_KINDS } from "/assets/js/views/sidebar.js";
|
|
9
12
|
|
|
10
13
|
export function renderOverview(container, state, t, locale, actions) {
|
|
11
14
|
clear(container);
|
|
12
|
-
const
|
|
15
|
+
const tasks = state.tasks || [];
|
|
16
|
+
const active = tasks.filter(function (task) { return task.status === "active"; });
|
|
17
|
+
const recent = tasks.filter(function (task) { return task.status !== "archived"; }).slice().sort(byNewest).slice(0, 8);
|
|
13
18
|
const page = h("div.page.overview");
|
|
14
|
-
page.append(
|
|
19
|
+
page.append(overviewHead(state.counts, t, actions), metricStrip(state.counts, t));
|
|
20
|
+
page.append(h("div.card-stack", null,
|
|
21
|
+
inboxCard(state, t, actions),
|
|
22
|
+
taskListCard(t("overview.activeNow"), "pulse", active, t("overview.activeEmpty"), t, locale, actions),
|
|
23
|
+
taskListCard(t("overview.recent"), "history", recent, t("sidebar.empty"), t, locale, actions),
|
|
24
|
+
catalogCard(state, t, actions),
|
|
25
|
+
sessionCard(state, t, locale, actions)));
|
|
26
|
+
page.append(h("p.page-foot", null, t("overview.pageScope")));
|
|
27
|
+
container.append(page);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function overviewHead(counts, t, actions) {
|
|
31
|
+
return h("header.page-head", null,
|
|
15
32
|
h("div.page-head-main", null,
|
|
16
33
|
h("p.kicker", null, t("overview.kicker")),
|
|
17
34
|
h("h1.page-title", null, t("overview.title")),
|
|
18
35
|
h("p.page-sub", null, counts
|
|
19
|
-
? t("overview.sub")
|
|
36
|
+
? fill(t("overview.sub"), { total: counts.total, active: counts.active || 0 })
|
|
20
37
|
: t("loading.dashboard"))),
|
|
21
38
|
h("div.page-head-actions", null,
|
|
22
|
-
button(t("actions.operator"), { icon: "terminal", variant: "ghost", onClick: actions.openOperator })))
|
|
39
|
+
button(t("actions.operator"), { icon: "terminal", variant: "ghost", onClick: actions.openOperator })));
|
|
40
|
+
}
|
|
23
41
|
|
|
24
|
-
|
|
42
|
+
function metricStrip(counts, t) {
|
|
43
|
+
return h("div.metric-strip", null,
|
|
25
44
|
metricTile(t("metrics.inputs"), counts ? counts.openInputs : "—", { tone: counts && counts.openInputs ? "warn" : "" }),
|
|
26
45
|
metricTile(t("metrics.active"), counts ? counts.active : "—", { tone: "info" }),
|
|
27
46
|
metricTile(t("metrics.draft"), counts ? counts.draft : "—"),
|
|
28
47
|
metricTile(t("metrics.completed"), counts ? counts.completed : "—", { tone: "ok" }),
|
|
29
|
-
metricTile(t("metrics.total"), counts ? counts.total : "—"))
|
|
48
|
+
metricTile(t("metrics.total"), counts ? counts.total : "—"));
|
|
49
|
+
}
|
|
30
50
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
// Open questions: the catalog names the affected Tasks, never the questions.
|
|
52
|
+
function inboxCard(state, t, actions) {
|
|
53
|
+
const counts = state.counts;
|
|
34
54
|
const inbox = card({ title: t("overview.inbox"), icon: "inbox", count: counts ? counts.openInputs : null });
|
|
35
55
|
const items = state.attention || [];
|
|
36
56
|
if (!items.length) {
|
|
37
57
|
inbox.body.append(emptyState(counts && counts.openInputs > 0 ? t("overview.inboxElsewhere") : t("overview.inboxEmpty"), "check"));
|
|
38
|
-
|
|
39
|
-
inbox.body.append(h("div.row-list", null, items.slice(0, 8).map(function (item) {
|
|
40
|
-
return h("button.list-row", { type: "button", onclick: function () { actions.select(item.taskId); } },
|
|
41
|
-
dot("warn"), h("span.list-row-title", null, item.taskTitle), h("code.id", null, item.taskId),
|
|
42
|
-
h("span.list-row-go", null, t("overview.answer"), icon("chevron", "icon-sm")));
|
|
43
|
-
})));
|
|
58
|
+
return inbox;
|
|
44
59
|
}
|
|
60
|
+
inbox.body.append(h("div.row-list", null, items.slice(0, 8).map(function (item) {
|
|
61
|
+
return h("button.list-row", { type: "button", onclick: function () { actions.select(item.taskId); } },
|
|
62
|
+
dot("warn"), h("span.list-row-title", null, item.taskTitle), h("code.id", null, item.taskId),
|
|
63
|
+
h("span.list-row-go", null, t("overview.answer"), icon("chevron", "icon-sm")));
|
|
64
|
+
})));
|
|
65
|
+
return inbox;
|
|
66
|
+
}
|
|
45
67
|
|
|
68
|
+
function catalogCard(state, t, actions) {
|
|
46
69
|
const catalog = card({ title: t("overview.catalog"), icon: "broadcast", hint: t("overview.catalogHint") });
|
|
47
|
-
if (state.catalogAttention) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const active = (state.tasks || []).filter(function (task) { return task.status === "active"; });
|
|
64
|
-
const recent = (state.tasks || []).filter(function (task) { return task.status !== "archived"; }).slice().sort(byNewest).slice(0, 8);
|
|
65
|
-
page.append(h("div.card-stack", null,
|
|
66
|
-
inbox,
|
|
67
|
-
taskListCard(t("overview.activeNow"), "pulse", active, t("overview.activeEmpty"), t, locale, actions),
|
|
68
|
-
taskListCard(t("overview.recent"), "history", recent, t("sidebar.empty"), t, locale, actions),
|
|
69
|
-
catalog,
|
|
70
|
-
sessionCard(state, t, locale, actions)));
|
|
71
|
-
page.append(h("p.page-foot", null, t("overview.pageScope")));
|
|
72
|
-
container.append(page);
|
|
70
|
+
if (!state.catalogAttention) { catalog.body.append(emptyState(t("loading.dashboard"))); return catalog; }
|
|
71
|
+
catalog.body.append(h("div.row-list", null, ATTENTION_KINDS.map(function (kind) {
|
|
72
|
+
const value = state.catalogAttention[kind];
|
|
73
|
+
if (!value) return null;
|
|
74
|
+
return h("button.list-row", {
|
|
75
|
+
type: "button", disabled: value.taskCount === 0,
|
|
76
|
+
"aria-pressed": String(state.attentionFilter === kind),
|
|
77
|
+
onclick: function () { actions.filterAttention(kind); }
|
|
78
|
+
},
|
|
79
|
+
h("span.list-row-title", null, t("catalog." + kind)),
|
|
80
|
+
h("span.list-row-count", null, String(value.count)),
|
|
81
|
+
h("span.faint", null, value.taskCount + " " + t("overview.tasks")));
|
|
82
|
+
})));
|
|
83
|
+
catalog.body.append(note(t("overview.signalsHelp")));
|
|
84
|
+
return catalog;
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
function taskListCard(title, iconName, tasks, empty, t, locale, actions) {
|
|
@@ -102,9 +114,7 @@ function sessionCard(state, t, locale, actions) {
|
|
|
102
114
|
}
|
|
103
115
|
element.body.append(h("p.faint.small", null, t("overview.readAt") + " " + formatDateTime(overview.readAt, locale)));
|
|
104
116
|
element.body.append(h("div.row-list", null, overview.tasks.map(function (task) {
|
|
105
|
-
const chips =
|
|
106
|
-
return badge(task.counts[group] + " " + t("session." + group), tone("session", group), { dot: true });
|
|
107
|
-
});
|
|
117
|
+
const chips = sessionBadges(task.counts, t);
|
|
108
118
|
return h("button.list-row.list-row-wrap", { type: "button", onclick: function () { actions.select(task.taskId); } },
|
|
109
119
|
h("span.list-row-title", null, task.title), h("code.id", null, task.taskId),
|
|
110
120
|
h("span.chip-row", null, chips.length ? chips : h("span.faint", null, t("session.noneRecorded"))));
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export const SIDEBAR_SCRIPT = String.raw `
|
|
2
2
|
// Task index: attention shortcuts, status tabs and the grouped task list.
|
|
3
|
-
import { h, icon, clear } from "/assets/js/dom.js";
|
|
4
|
-
import { relativeTime } from "/assets/js/format.js";
|
|
5
|
-
import { dot
|
|
3
|
+
import { h, icon, clear } from "/assets/js/lib/dom.js";
|
|
4
|
+
import { relativeTime } from "/assets/js/lib/format.js";
|
|
5
|
+
import { dot } from "/assets/js/ui/primitives.js";
|
|
6
|
+
import { revealTab, scrollableTabRow, syncTabsOverflow } from "/assets/js/ui/controls.js";
|
|
7
|
+
import { label, tone } from "/assets/js/domain/vocab.js";
|
|
6
8
|
|
|
7
9
|
export const STATUS_FILTERS = ["all", "active", "draft", "completed", "cancelled", "archived"];
|
|
8
10
|
export const ATTENTION_KINDS = ["openInputs", "unknownOperations", "pendingOperations", "executionSignals"];
|
|
@@ -29,56 +31,13 @@ export function renderAttentionBar(container, state, t, onPick) {
|
|
|
29
31
|
})));
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
// --- Status tabs -------------------------------------------------------------
|
|
32
35
|
// The tab row is created once and updated in place so its horizontal scroll
|
|
33
|
-
// position survives every refresh
|
|
34
|
-
// tab, the clipped edge fades out, the wheel scrolls the row sideways and a
|
|
35
|
-
// newly selected tab is scrolled into view.
|
|
36
|
-
function syncTabsOverflow(row) {
|
|
37
|
-
const max = row.scrollWidth - row.clientWidth;
|
|
38
|
-
const before = row.scrollLeft > 1;
|
|
39
|
-
const after = row.scrollLeft < max - 1;
|
|
40
|
-
row.dataset.fade = before && after ? "both" : after ? "end" : before ? "start" : "none";
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function revealTab(row, tab) {
|
|
44
|
-
const margin = 28;
|
|
45
|
-
const left = tab.offsetLeft - margin;
|
|
46
|
-
const right = tab.offsetLeft + tab.offsetWidth + margin;
|
|
47
|
-
if (left < row.scrollLeft) row.scrollLeft = Math.max(0, left);
|
|
48
|
-
else if (right > row.scrollLeft + row.clientWidth) row.scrollLeft = right - row.clientWidth;
|
|
49
|
-
}
|
|
50
|
-
|
|
36
|
+
// position survives every refresh; a newly selected tab scrolls into view.
|
|
51
37
|
export function renderFilters(container, state, t, onFilter) {
|
|
52
|
-
const
|
|
53
|
-
let row = container.querySelector(":scope > .tabs-row");
|
|
54
|
-
if (!row) {
|
|
55
|
-
row = h("div.tabs-row");
|
|
56
|
-
STATUS_FILTERS.forEach(function (status) {
|
|
57
|
-
row.append(h("button.status-tab", { type: "button", dataset: { status }, onclick: function () { onFilter(status); } },
|
|
58
|
-
h("span.status-tab-label"), h("span.status-tab-count")));
|
|
59
|
-
});
|
|
60
|
-
container.append(row);
|
|
61
|
-
row.addEventListener("scroll", function () { syncTabsOverflow(row); }, { passive: true });
|
|
62
|
-
row.addEventListener("wheel", function (event) {
|
|
63
|
-
if (Math.abs(event.deltaY) <= Math.abs(event.deltaX) || row.scrollWidth <= row.clientWidth) return;
|
|
64
|
-
event.preventDefault();
|
|
65
|
-
row.scrollLeft += event.deltaY;
|
|
66
|
-
}, { passive: false });
|
|
67
|
-
new ResizeObserver(function () { syncTabsOverflow(row); }).observe(row);
|
|
68
|
-
}
|
|
38
|
+
const row = container.querySelector(":scope > .tabs-row") || statusRow(container, onFilter);
|
|
69
39
|
STATUS_FILTERS.forEach(function (status) {
|
|
70
|
-
|
|
71
|
-
tab.setAttribute("aria-pressed", String(state.filter === status));
|
|
72
|
-
tab.querySelector(".status-tab-label").textContent = status === "all"
|
|
73
|
-
? t(state.catalogAll ? "filter.everything" : "filter.unarchived") : label(t, "status", status);
|
|
74
|
-
const count = status === "all"
|
|
75
|
-
? (counts.total === undefined ? undefined : counts.total - (state.catalogAll ? 0 : counts.archived || 0))
|
|
76
|
-
: counts[status];
|
|
77
|
-
const badge = tab.querySelector(".status-tab-count");
|
|
78
|
-
const hide = count === undefined || count === null
|
|
79
|
-
|| (status === "archived" && state.catalogScope && state.catalogScope.archived === "excluded");
|
|
80
|
-
badge.hidden = hide;
|
|
81
|
-
if (!hide) badge.textContent = String(count);
|
|
40
|
+
updateStatusTab(row.querySelector('[data-status="' + status + '"]'), status, state, t);
|
|
82
41
|
});
|
|
83
42
|
if (row.dataset.selected !== state.filter) {
|
|
84
43
|
row.dataset.selected = state.filter;
|
|
@@ -87,6 +46,34 @@ export function renderFilters(container, state, t, onFilter) {
|
|
|
87
46
|
syncTabsOverflow(row);
|
|
88
47
|
}
|
|
89
48
|
|
|
49
|
+
function statusRow(container, onFilter) {
|
|
50
|
+
const row = h("div.tabs-row");
|
|
51
|
+
STATUS_FILTERS.forEach(function (status) {
|
|
52
|
+
row.append(h("button.status-tab", { type: "button", dataset: { status }, onclick: function () { onFilter(status); } },
|
|
53
|
+
h("span.status-tab-label"), h("span.status-tab-count")));
|
|
54
|
+
});
|
|
55
|
+
container.append(row);
|
|
56
|
+
return scrollableTabRow(row);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function updateStatusTab(tab, status, state, t) {
|
|
60
|
+
const counts = state.counts || {};
|
|
61
|
+
tab.setAttribute("aria-pressed", String(state.filter === status));
|
|
62
|
+
tab.querySelector(".status-tab-label").textContent = status === "all"
|
|
63
|
+
? t(state.catalogAll ? "filter.everything" : "filter.unarchived") : label(t, "status", status);
|
|
64
|
+
const count = status === "all"
|
|
65
|
+
? (counts.total === undefined ? undefined : counts.total - (state.catalogAll ? 0 : counts.archived || 0))
|
|
66
|
+
: counts[status];
|
|
67
|
+
const badge = tab.querySelector(".status-tab-count");
|
|
68
|
+
const hide = count === undefined || count === null
|
|
69
|
+
|| (status === "archived" && state.catalogScope && state.catalogScope.archived === "excluded");
|
|
70
|
+
badge.hidden = hide;
|
|
71
|
+
if (!hide) badge.textContent = String(count);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Task list ---------------------------------------------------------------
|
|
75
|
+
const GROUP_ORDER = ["attention", "active", "draft", "completed", "cancelled", "archived"];
|
|
76
|
+
|
|
90
77
|
function groupOf(task) {
|
|
91
78
|
const attention = task.attention || {};
|
|
92
79
|
if (attention.openInputs > 0 || attention.unknownOperations > 0) return "attention";
|
|
@@ -94,10 +81,7 @@ function groupOf(task) {
|
|
|
94
81
|
return task.status;
|
|
95
82
|
}
|
|
96
83
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
export function renderTasks(container, state, t, locale, onSelect) {
|
|
100
|
-
clear(container);
|
|
84
|
+
function groupTasks(state) {
|
|
101
85
|
const groups = {};
|
|
102
86
|
(state.tasks || []).forEach(function (task) {
|
|
103
87
|
if (state.filter === "all" && !state.catalogAll && task.status === "archived") return;
|
|
@@ -105,6 +89,12 @@ export function renderTasks(container, state, t, locale, onSelect) {
|
|
|
105
89
|
const group = groupOf(task);
|
|
106
90
|
(groups[group] = groups[group] || []).push(task);
|
|
107
91
|
});
|
|
92
|
+
return groups;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function renderTasks(container, state, t, locale, onSelect) {
|
|
96
|
+
clear(container);
|
|
97
|
+
const groups = groupTasks(state);
|
|
108
98
|
const present = GROUP_ORDER.filter(function (group) { return groups[group] && groups[group].length; });
|
|
109
99
|
if (!present.length) {
|
|
110
100
|
container.append(h("div.list-empty", null, icon("search"), h("p", null, t(state.query || state.attentionFilter ? "sidebar.noMatch" : "sidebar.empty"))));
|
|
@@ -118,13 +108,8 @@ export function renderTasks(container, state, t, locale, onSelect) {
|
|
|
118
108
|
}
|
|
119
109
|
|
|
120
110
|
function taskRow(task, state, t, locale, onSelect) {
|
|
121
|
-
const attention = task.attention || {};
|
|
122
111
|
const counts = task.counts || {};
|
|
123
|
-
|
|
124
|
-
if (attention.openInputs > 0) signals.append(signal("inbox", attention.openInputs, "warn", t("catalog.openInputs")));
|
|
125
|
-
if (attention.unknownOperations > 0) signals.append(signal("alert", attention.unknownOperations, "bad", t("catalog.unknownOperations")));
|
|
126
|
-
if (counts.activeRuns > 0) signals.append(signal("pulse", counts.activeRuns, "info", t("sidebar.activeRuns")));
|
|
127
|
-
const row = h("button.task-row", {
|
|
112
|
+
return h("button.task-row", {
|
|
128
113
|
type: "button",
|
|
129
114
|
dataset: { id: task.id },
|
|
130
115
|
"aria-current": String(state.selected === task.id),
|
|
@@ -136,8 +121,17 @@ function taskRow(task, state, t, locale, onSelect) {
|
|
|
136
121
|
h("span.task-line", null, h("span.task-name", null, task.title), h("time.task-time", { dateTime: task.updatedAt }, relativeTime(task.updatedAt, locale, t))),
|
|
137
122
|
h("span.task-line.task-sub", null, h("code.task-id", null, task.id),
|
|
138
123
|
counts.workItems > 0 ? h("span.task-count", { title: t("sidebar.workItems") }, icon("layers", "icon-sm"), String(counts.workItems)) : null,
|
|
139
|
-
|
|
140
|
-
|
|
124
|
+
taskSignals(task, t))));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function taskSignals(task, t) {
|
|
128
|
+
const attention = task.attention || {};
|
|
129
|
+
const counts = task.counts || {};
|
|
130
|
+
const signals = h("span.task-signals");
|
|
131
|
+
if (attention.openInputs > 0) signals.append(signal("inbox", attention.openInputs, "warn", t("catalog.openInputs")));
|
|
132
|
+
if (attention.unknownOperations > 0) signals.append(signal("alert", attention.unknownOperations, "bad", t("catalog.unknownOperations")));
|
|
133
|
+
if (counts.activeRuns > 0) signals.append(signal("pulse", counts.activeRuns, "info", t("sidebar.activeRuns")));
|
|
134
|
+
return signals;
|
|
141
135
|
}
|
|
142
136
|
|
|
143
137
|
function signal(iconName, count, toneName, title) {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export const SKILLS_SCRIPT = String.raw `
|
|
2
|
+
import { h } from "/assets/js/lib/dom.js";
|
|
3
|
+
import { requestJson, submitMutation } from "/assets/js/lib/api.js";
|
|
4
|
+
|
|
5
|
+
export function mountSkills(root, t) {
|
|
6
|
+
if (!root) return;
|
|
7
|
+
const tr = (key, fallback) => t("skills.browser." + key, fallback);
|
|
8
|
+
const scope = h("select", null, h("option", { value: "global" }, tr("global", "Global Role")),
|
|
9
|
+
h("option", { value: "task" }, tr("task", "Task Role")));
|
|
10
|
+
const task = h("input", { placeholder: "task-123", disabled: true });
|
|
11
|
+
const roles = h("select");
|
|
12
|
+
const status = h("p", { role: "status", "aria-live": "polite" });
|
|
13
|
+
const bindings = h("div.settings-stack");
|
|
14
|
+
const frozen = h("div.settings-stack");
|
|
15
|
+
const query = h("input", { type: "search" });
|
|
16
|
+
const catalog = h("div.settings-stack");
|
|
17
|
+
const reader = h("div.settings-stack", { tabindex: "-1" });
|
|
18
|
+
const acknowledge = h("input", { type: "checkbox" });
|
|
19
|
+
const acknowledgement = h("label.settings-reset", null, acknowledge, tr("ack", "Keep any live global Session on its existing version."));
|
|
20
|
+
const save = h("button.btn.btn-primary", { type: "button", disabled: true }, tr("save", "Save bindings"));
|
|
21
|
+
const reload = h("button.btn", { type: "button" }, tr("reload", "Load / reload Role"));
|
|
22
|
+
const more = h("button.btn", { type: "button", hidden: true }, tr("more", "More Skills"));
|
|
23
|
+
const search = h("button.btn", { type: "button" }, tr("search", "Search"));
|
|
24
|
+
let state = null, draft = [], busy = false, uncertain = false, generation = 0, catalogGeneration = 0, readerGeneration = 0;
|
|
25
|
+
const selected = () => ({ scope: scope.value, role: roles.value, ...(scope.value === "task" ? { task: task.value.trim() } : {}) });
|
|
26
|
+
const params = object => new URLSearchParams(object).toString();
|
|
27
|
+
const label = (text, control) => h("label.field", null, h("span", null, text), control);
|
|
28
|
+
function dirty() { return !!state && JSON.stringify(draft) !== JSON.stringify(state.skills); }
|
|
29
|
+
function invalidate() {
|
|
30
|
+
generation++; state = null; draft = []; bindings.replaceChildren(); frozen.replaceChildren(); save.disabled = true;
|
|
31
|
+
scope.disabled = roles.disabled = false; task.disabled = scope.value !== "task";
|
|
32
|
+
acknowledgement.hidden = scope.value !== "global";
|
|
33
|
+
}
|
|
34
|
+
function renderBindings() {
|
|
35
|
+
// A draft remains attached to the exact Role baseline until saved or explicitly reloaded.
|
|
36
|
+
[scope, roles].forEach(c => { c.disabled = busy || dirty(); });
|
|
37
|
+
task.disabled = busy || dirty() || scope.value !== "task";
|
|
38
|
+
bindings.replaceChildren(h("h3", null, tr("configured", "Configured bindings")),
|
|
39
|
+
...draft.map(id => h("div.settings-actions", null, h("code", null, id),
|
|
40
|
+
h("button.btn", { type: "button", disabled: busy || uncertain, onclick: () => { draft = draft.filter(s => s !== id); renderBindings(); } }, tr("remove", "Remove binding")),
|
|
41
|
+
h("button.btn", { type: "button", onclick: () => read({ id }) }, tr("readSource", "Read current source")))));
|
|
42
|
+
if (!draft.length) bindings.append(h("p", null, tr("none", "No additional bindings. Built-in Role Skills are loaded automatically.")));
|
|
43
|
+
save.disabled = busy || uncertain || !dirty();
|
|
44
|
+
}
|
|
45
|
+
function renderEvidence(data) {
|
|
46
|
+
frozen.replaceChildren(h("h3", null, tr("frozen", "Current Session · frozen evidence")));
|
|
47
|
+
if (!data.session) frozen.append(h("p", null, tr("noSession", "No current Session. No loaded version can be claimed.")));
|
|
48
|
+
else {
|
|
49
|
+
frozen.append(h("p.mono-line", null, data.session.nativeSessionId + " · " + data.session.status +
|
|
50
|
+
" · " + tr("revision", "launch / desired revision") + " " + data.session.sourceDesiredRevision + " / " + data.launchRevision));
|
|
51
|
+
if (!data.session.packages) frozen.append(h("p", null, tr("unrecorded", "This Session has no frozen package evidence. Current source is not a substitute.")));
|
|
52
|
+
else data.session.packages.forEach(pkg => frozen.append(h("div.settings-stack", null,
|
|
53
|
+
h("strong", null, pkg.id), h("code.mono-line", null, pkg.digest),
|
|
54
|
+
h("span.mono-line", null, pkg.source.kind + " · " + pkg.source.path),
|
|
55
|
+
h("button.btn", { type: "button", onclick: () => read({ ...data.target, id: pkg.id, digest: pkg.digest, frozen: "true" }) }, tr("readFrozen", "Read frozen package")))));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function read(input) {
|
|
59
|
+
const current = ++readerGeneration;
|
|
60
|
+
reader.textContent = tr("loading", "Loading…");
|
|
61
|
+
try {
|
|
62
|
+
const data = await requestJson("/api/skills/file?" + params(input));
|
|
63
|
+
if (current !== readerGeneration) return;
|
|
64
|
+
const files = h("select", null, data.files.map(path => h("option", { value: path }, path)));
|
|
65
|
+
files.value = data.resource;
|
|
66
|
+
files.addEventListener("change", () => read({ ...input, resource: files.value }));
|
|
67
|
+
reader.replaceChildren(h("h3", null, data.id + " · " + data.resource),
|
|
68
|
+
h("p.mono-line", null, data.source.kind + " · " + data.source.path),
|
|
69
|
+
h("p.mono-line", null, tr("version", "Package version") + ": " + data.version),
|
|
70
|
+
h("p.mono-line", null, "SHA-256 (" + tr("file", "file") + "): " + data.fileDigest),
|
|
71
|
+
label(tr("resource", "Read a resource (never executed)"), files),
|
|
72
|
+
h("pre.settings-observation", { tabindex: "0" }, data.content));
|
|
73
|
+
reader.focus({ preventScroll: true }); reader.scrollIntoView({ block: "start" });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (current === readerGeneration) { reader.textContent = error.message; reader.scrollIntoView({ block: "nearest" }); }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function loadRole() {
|
|
79
|
+
if (busy) return;
|
|
80
|
+
if (dirty() && !window.confirm(tr("discard", "Discard unsaved Skill bindings and reload?"))) return;
|
|
81
|
+
const current = ++generation;
|
|
82
|
+
status.textContent = tr("loading", "Loading…");
|
|
83
|
+
try {
|
|
84
|
+
const roleNames = await requestJson("/api/skills/roles?" + params({ scope: scope.value, ...(scope.value === "task" ? { task: task.value.trim() } : {}) }));
|
|
85
|
+
if (current !== generation) return;
|
|
86
|
+
const previous = roles.value;
|
|
87
|
+
roles.replaceChildren(...roleNames.map(r => h("option", { value: r.name }, r.name)));
|
|
88
|
+
if (roleNames.some(r => r.name === previous)) roles.value = previous;
|
|
89
|
+
if (!roles.value) { invalidate(); status.textContent = tr("noRoles", "No Roles in this scope."); return; }
|
|
90
|
+
const data = await requestJson("/api/skills/role?" + params(selected()));
|
|
91
|
+
if (current !== generation) return;
|
|
92
|
+
state = data; draft = [...data.skills]; uncertain = false; renderBindings();
|
|
93
|
+
status.textContent = tr("nextLaunch", "Bindings apply to a subsequent launch; saving does not load a Skill or restart a Session. Removing a binding never deletes its package.");
|
|
94
|
+
renderEvidence(data);
|
|
95
|
+
} catch (error) { if (current === generation) { invalidate(); status.textContent = error.message; } }
|
|
96
|
+
}
|
|
97
|
+
async function loadCatalog(cursor) {
|
|
98
|
+
const current = ++catalogGeneration;
|
|
99
|
+
more.disabled = search.disabled = true;
|
|
100
|
+
try {
|
|
101
|
+
const data = await requestJson("/api/skills/catalog?" + params({ q: query.value.trim(), cursor: cursor || "0" }));
|
|
102
|
+
if (current !== catalogGeneration) return;
|
|
103
|
+
if (!cursor) catalog.replaceChildren();
|
|
104
|
+
if (!data.items.length) catalog.append(h("p", null, tr("noMatches", "No matching Skills.")));
|
|
105
|
+
data.items.forEach(item => catalog.append(h("article.settings-card", null,
|
|
106
|
+
h("h3", null, item.id), h("p", null, item.description || item.error),
|
|
107
|
+
h("small.mono-line", null, item.source.kind + " · " + item.source.path),
|
|
108
|
+
h("div.settings-actions", null,
|
|
109
|
+
h("button.btn", { type: "button", onclick: () => read({ id: item.id }) }, tr("readSource", "Read current source")),
|
|
110
|
+
h("button.btn", { type: "button", disabled: !!item.error, onclick: () => {
|
|
111
|
+
if (!state || busy || uncertain) { status.textContent = tr("choose", "Load an explicit Role before editing bindings."); return; }
|
|
112
|
+
if (!draft.includes(item.id)) draft.push(item.id);
|
|
113
|
+
renderBindings(); status.textContent = tr("unsaved", "Unsaved bindings. Review the selected Role and save.");
|
|
114
|
+
bindings.scrollIntoView({ block: "nearest" });
|
|
115
|
+
} }, tr("add", "Add to selected Role"))))));
|
|
116
|
+
more.hidden = !data.nextCursor; more.onclick = () => loadCatalog(data.nextCursor);
|
|
117
|
+
} catch (error) { catalog.replaceChildren(h("p", null, error.message)); }
|
|
118
|
+
finally { if (current === catalogGeneration) more.disabled = search.disabled = false; }
|
|
119
|
+
}
|
|
120
|
+
save.addEventListener("click", async () => {
|
|
121
|
+
if (!state || busy || uncertain || !dirty()) return;
|
|
122
|
+
busy = true; renderBindings();
|
|
123
|
+
[scope, task, roles, reload].forEach(c => { c.disabled = true; });
|
|
124
|
+
try {
|
|
125
|
+
const result = await submitMutation("skills/" + JSON.stringify(state.target), "/api/skills/role",
|
|
126
|
+
{ target: state.target, revision: state.revision, skills: draft, acknowledgeLive: acknowledge.checked });
|
|
127
|
+
state = result.role; draft = [...state.skills];
|
|
128
|
+
renderEvidence(state);
|
|
129
|
+
status.textContent = tr("saved", "Bindings saved. Existing frozen Sessions and Runs are unchanged; no package was deleted.");
|
|
130
|
+
} catch (error) {
|
|
131
|
+
uncertain = error.disposition !== "not-submitted";
|
|
132
|
+
status.textContent = error.message + (uncertain ? "\n" + tr("unknown", "Save outcome unknown. Reload and compare before making another change.") : "");
|
|
133
|
+
} finally {
|
|
134
|
+
busy = false; [scope, roles, reload].forEach(c => { c.disabled = false; }); task.disabled = scope.value !== "task"; renderBindings();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
scope.addEventListener("change", () => { invalidate(); task.disabled = scope.value !== "task"; });
|
|
138
|
+
task.addEventListener("input", invalidate);
|
|
139
|
+
roles.addEventListener("change", () => { invalidate(); void loadRole(); });
|
|
140
|
+
reload.addEventListener("click", loadRole);
|
|
141
|
+
search.addEventListener("click", () => loadCatalog());
|
|
142
|
+
query.addEventListener("keydown", event => { if (event.key === "Enter") { event.preventDefault(); void loadCatalog(); } });
|
|
143
|
+
window.addEventListener("beforeunload", event => { if (dirty()) { event.preventDefault(); event.returnValue = ""; } });
|
|
144
|
+
root.replaceChildren(h("h2", null, tr("title", "Skills · browse & bind")),
|
|
145
|
+
h("p", null, tr("intro", "Read existing packages and choose additional bindings for one explicit Role. Scripts are displayed as data, never run.")),
|
|
146
|
+
h("div.settings-grid", null, label(tr("scope", "Scope"), scope), label(tr("taskId", "Task ID"), task), label("Role", roles)),
|
|
147
|
+
h("div.settings-actions", null, reload), status, bindings, acknowledgement,
|
|
148
|
+
h("div.settings-actions", null, save), frozen, label(tr("query", "Find an existing Skill by name or purpose"), query),
|
|
149
|
+
h("div.settings-actions", null, search), catalog, more, reader);
|
|
150
|
+
void loadRole(); void loadCatalog();
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const TASK_ACTIONS_SCRIPT = String.raw `
|
|
2
|
+
import { h } from "/assets/js/lib/dom.js";
|
|
3
|
+
import { button, note } from "/assets/js/ui/primitives.js";
|
|
4
|
+
import { card } from "/assets/js/ui/containers.js";
|
|
5
|
+
import { receiptLine, setReceipt, submitWrite } from "/assets/js/ui/forms.js";
|
|
6
|
+
|
|
7
|
+
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
|
+
const action = task.status === "draft" ? "activate"
|
|
11
|
+
: ["completed", "cancelled"].includes(task.status) ? "archive" : null;
|
|
12
|
+
if (!action) return element;
|
|
13
|
+
const activation = task.activationRequest;
|
|
14
|
+
const blocked = action === "activate" && (task.executionGate.state !== "enabled"
|
|
15
|
+
|| 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);
|
|
34
|
+
return element;
|
|
35
|
+
}
|
|
36
|
+
`;
|