@zq-silk/yui 2.17.0 → 2.18.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 (70) hide show
  1. package/dist/cli/commandCatalog.js +2 -1
  2. package/dist/commands/controllerCommands.js +13 -4
  3. package/dist/commands/durableJobCommands.js +50 -3
  4. package/dist/commands/globalRoleCommands.js +18 -12
  5. package/dist/commands/inboxCommands.js +1 -0
  6. package/dist/commands/operatorCommands.js +10 -2
  7. package/dist/controlPlaneCli.js +21 -4
  8. package/dist/controller/clientRuntime.js +34 -13
  9. package/dist/controller/controller.js +35 -22
  10. package/dist/controller/diagnosticJournal.js +214 -0
  11. package/dist/controller/fileSchedulerStoreAdapter.js +139 -101
  12. package/dist/controller/globalInputDelivery.js +6 -2
  13. package/dist/controller/globalRuntimeAttention.js +11 -3
  14. package/dist/controller/jobControl.js +1 -1
  15. package/dist/controller/jobDefinitionControl.js +24 -13
  16. package/dist/controller/jobSupervisor.js +4 -0
  17. package/dist/controller/runtime.js +66 -4
  18. package/dist/controller/workerSchedulerStore.js +24 -0
  19. package/dist/core/boundedRpc.js +4 -0
  20. package/dist/core/controllerServer.js +1 -1
  21. package/dist/core/controllerTelemetry.js +92 -19
  22. package/dist/job/globalShell.js +51 -0
  23. package/dist/job/jobBusinessResult.js +4 -3
  24. package/dist/job/jobDefinition.js +6 -1
  25. package/dist/job/jobRunner.js +9 -0
  26. package/dist/job/scheduleService.js +2 -0
  27. package/dist/message/globalInboxRecovery.js +30 -19
  28. package/dist/message/globalProviderRetry.js +7 -1
  29. package/dist/runtime/builtinAgentErrorMappers.js +4 -0
  30. package/dist/runtime/globalSessionHealth.js +39 -0
  31. package/dist/scheduler/roleRunStall.js +10 -10
  32. package/dist/storage/persistenceWorker.js +41 -19
  33. package/dist/storage/sqliteSchema.js +35 -11
  34. package/dist/storage/sqliteStore.js +86 -8
  35. package/dist/storage/storageVersions.js +1 -1
  36. package/dist/storage/storeRpc.js +25 -11
  37. package/dist/storage/taskReadRevisionSchema.js +39 -0
  38. package/dist/web/assets/client/app/catalog.js +0 -29
  39. package/dist/web/assets/client/app/jobs.js +115 -0
  40. package/dist/web/assets/client/app/main.js +17 -6
  41. package/dist/web/assets/client/app/manage.js +1 -0
  42. package/dist/web/assets/client/app/settings.js +1 -5
  43. package/dist/web/assets/client/i18n/en.js +203 -18
  44. package/dist/web/assets/client/i18n/zh-CN.js +202 -17
  45. package/dist/web/assets/client/index.js +2 -0
  46. package/dist/web/assets/client/layout/manageShell.js +61 -15
  47. package/dist/web/assets/client/layout/router.js +2 -1
  48. package/dist/web/assets/client/lib/prefs.js +1 -1
  49. package/dist/web/assets/client/views/jobs.js +1042 -160
  50. package/dist/web/assets/client/views/sidebar.js +3 -0
  51. package/dist/web/assets/shared/manageNav.js +3 -1
  52. package/dist/web/assets/shell/appRail.js +2 -1
  53. package/dist/web/assets/shell/manage.js +7 -2
  54. package/dist/web/assets/shell/models.js +4 -3
  55. package/dist/web/assets/shell/settingsContent.js +0 -3
  56. package/dist/web/assets/shell/sidebar.js +0 -1
  57. package/dist/web/assets/styles/views/jobs.js +301 -12
  58. package/dist/web/assets/styles/views/sidebar.js +1 -2
  59. package/dist/web/webServer.js +28 -1
  60. package/docs/global-shell-jobs.md +78 -0
  61. package/docs/job-runs.md +6 -3
  62. package/docs/managed-turn-and-session-runtime.md +74 -0
  63. package/docs/managed-turn-and-session-runtime.zh-CN.md +51 -0
  64. package/docs/release-workflow.md +43 -0
  65. package/docs/release-workflow.zh-CN.md +29 -0
  66. package/docs/storage-baseline.md +14 -1
  67. package/docs/storage-baseline.zh-CN.md +6 -1
  68. package/docs/testing/verification-levels.md +2 -2
  69. package/docs/testing/verification-levels.zh-CN.md +2 -2
  70. package/package.json +1 -1
@@ -120,6 +120,9 @@ function taskRow(task, state, t, locale, onSelect) {
120
120
  h("span.task-body", null,
121
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))),
122
122
  h("span.task-line.task-sub", null, h("code.task-id", null, task.id),
123
+ // A Task a Job Run created is still an ordinary real Task; mark where it came from.
124
+ task.source && task.source.kind === "job-run"
125
+ ? h("span.task-source", { title: t("jobs.sourceJob") + " · " + task.source.jobId }, t("catalog.sourceJob")) : null,
123
126
  counts.workItems > 0 ? h("span.task-count", { title: t("sidebar.workItems") }, icon("layers", "icon-sm"), String(counts.workItems)) : null,
124
127
  taskSignals(task, t))));
125
128
  }
@@ -7,9 +7,11 @@ export const MANAGE_SECTIONS = Object.freeze({
7
7
  settings: [
8
8
  { page: "settings", section: "preferences", key: "settings.section.preferences", label: "Preferences" },
9
9
  { page: "settings", section: "skills", key: "settings.section.skills", label: "Skills" },
10
- { page: "settings", section: "jobs", key: "settings.section.jobs", label: "Jobs & schedules" },
11
10
  { page: "settings", section: "permissions", key: "settings.section.permissions", label: "Command permissions" },
12
11
  { page: "settings", section: "server", key: "settings.section.server", label: "Agents & server" },
13
12
  { page: "models", section: "connection", key: "models.section.connection", label: "Model connection" }
13
+ ],
14
+ jobs: [
15
+ { page: "jobs", section: "jobs", key: "nav.jobs", label: "Jobs" }
14
16
  ]
15
17
  });
@@ -6,7 +6,8 @@ export function appRail(current) {
6
6
  return `<nav class="app-rail" aria-label="Main navigation" data-i18n-aria-label="manage.nav">
7
7
  <a class="rail-brand" href="/"${brandNav} title="Yui"><span class="brand-mark" aria-hidden="true">${brandGlyphSvg()}</span><span class="sr-only">Yui</span></a>
8
8
  <div class="rail-items">
9
- ${item("workbench", "/", "workbench", "manage.workbench", "Workbench", current, current === "workbench" ? "rail-workbench" : undefined)}
9
+ ${item("workbench", "/", "workbench", "manage.workbench", "Tasks", current, current === "workbench" ? "rail-workbench" : undefined)}
10
+ ${item("jobs", "/jobs", "clock", "nav.jobs", "Jobs", current)}
10
11
  ${item("models", "/models", "layers", "models.title", "Models", current)}
11
12
  ${item("settings", "/settings", "settings", "settings.title", "Settings", current, current === "workbench" ? "settings-open" : undefined)}
12
13
  </div>
@@ -4,7 +4,7 @@ import { BRAND_ICONS } from "../shared/brand.js";
4
4
  import { iconSvg } from "../shared/icons.js";
5
5
  import { appRail } from "./appRail.js";
6
6
  import { MANAGE_SECTIONS } from "../shared/manageNav.js";
7
- // One frame and entry point for /models/* and /settings/*. The frame is the
7
+ // One frame and entry point for /jobs, /models/* and /settings/*. The frame is the
8
8
  // workbench's own: the global rail, a collapsible list (here the current
9
9
  // area's sections), and the center column under the shared top bar. Narrow
10
10
  // screens open the rail and the list together as a drawer, exactly as on the
@@ -22,9 +22,14 @@ const area = (name, key, label) => `
22
22
  const RAIL = `<aside class="manage-rail sidebar" aria-label="Sections" data-i18n-aria-label="manage.sections">
23
23
  <header class="side-head">
24
24
  <h2 id="manage-area-title" class="side-title"></h2>
25
+ <span id="jobs-sync" class="sync" data-area-only="jobs" hidden title="Last sync" data-i18n-title="sync.label"><i aria-hidden="true"></i><time id="jobs-last-sync">—</time></span>
26
+ <button id="jobs-refresh" class="icon-btn" type="button" data-area-only="jobs" hidden aria-label="Refresh" data-i18n-aria-label="actions.refresh" title="Refresh" data-i18n-title="actions.refresh">${iconSvg("refresh")}</button>
25
27
  <button id="sidebar-collapse" class="icon-btn side-collapse" type="button" aria-label="Collapse sections" data-i18n-aria-label="manage.collapseSections" title="Collapse sections · [" data-i18n-title="manage.collapseSections">${iconSvg("sidebar")}</button>
26
28
  </header>
27
29
  <nav class="manage-nav" aria-label="Sections" data-i18n-aria-label="manage.sections">${area("models", "models.title", "Models")}${area("settings", "settings.title", "Settings")}
30
+ <div class="manage-area jobs-area" data-area-group="jobs">
31
+ <div id="jobs-index" class="jobs-index" role="region" aria-label="Jobs" data-i18n-aria-label="nav.jobs"></div>
32
+ </div>
28
33
  </nav>
29
34
  </aside>`;
30
35
  const TOP_BAR = `<header class="top-bar">
@@ -47,7 +52,7 @@ export function manageDocument(options) {
47
52
  </head>
48
53
  <body class="manage-body ${options.page}-body">
49
54
  <div class="app manage-app" data-page="${options.page}" hidden>
50
- ${appRail(options.page === "models" ? "models" : "settings")}
55
+ ${appRail(options.page)}
51
56
  ${RAIL}
52
57
  <div id="sidebar-scrim" class="sidebar-scrim"></div>
53
58
  <main id="manage-scroll" class="center manage-center" tabindex="-1">
@@ -1,7 +1,8 @@
1
1
  import { manageDocument } from "./manage.js";
2
2
  import { SETTINGS_MAIN } from "./settingsContent.js";
3
- // One document for /models/* and /settings/*. Both controllers stay mounted
4
- // across history navigation, preserving drafts, observations and locale.
3
+ // One document for /jobs, /models/* and /settings/*. The controllers stay
4
+ // mounted across history navigation, preserving drafts, observations and locale.
5
5
  export const MODELS_HTML = manageDocument({ page: "models", title: "Models", main: `
6
6
  <div id="settings-root" hidden>${SETTINGS_MAIN}</div>
7
- <div id="models-root" class="models-root" hidden></div>` });
7
+ <div id="models-root" class="models-root" hidden></div>
8
+ <div id="jobs-root" class="jobs-root" hidden></div>` });
@@ -22,9 +22,6 @@ export const SETTINGS_MAIN = `
22
22
  ${manageHead("settings-skills-title", "settings.section.skills", "Skills", "skills.browser.intro", "Read existing packages and choose additional bindings for one explicit Role. Scripts are displayed as data, never run.")}
23
23
  <section id="skill-browser" class="card settings-card settings-stack"></section>
24
24
  </section>
25
- <section class="manage-section" data-section="jobs" hidden>
26
- <section id="global-jobs"></section>
27
- </section>
28
25
  <section class="manage-section" data-section="server" aria-labelledby="settings-server-title" hidden>
29
26
  ${manageHead("settings-server-title", "settings.section.server", "Agents & server", "settings.hint.server", "Desired configuration for Yui, Roles, Agents and Profiles. Saving targets later launches; it is not proof that existing Sessions adopted it.")}
30
27
  <label class="field settings-search"><span data-i18n="settings.search">Find a setting</span><input id="settings-search" type="search" autocomplete="off"></label>
@@ -24,7 +24,6 @@ export const SIDEBAR_HTML = ` <aside class="sidebar" aria-label="Tasks" data-
24
24
  </label>
25
25
  <button id="body-search-open" class="icon-btn side-search-body" type="button" aria-label="Search task contents" data-i18n-aria-label="bodySearch.title" title="Search task contents" data-i18n-title="bodySearch.title">${iconSvg("textSearch")}</button>
26
26
  </div>
27
- <div id="catalog-source" class="seg seg-sm source-seg" role="radiogroup" aria-label="Task category" data-i18n-aria-label="jobs.taskCategory"></div>
28
27
  <div id="attention-bar" class="attention-bar" aria-live="polite"></div>
29
28
  </div>
30
29
  <div id="status-filters" class="status-tabs" role="group" aria-label="Filter by status" data-i18n-aria-label="filters.label"></div>
@@ -1,19 +1,308 @@
1
+ // Jobs: one calm, dense surface. Hairline separators instead of nested cards,
2
+ // the workbench's own list head and "new" action, a two-line index, a scannable Run table and a properties
3
+ // column. Only shared tokens (ink, line, surface, tone, radius, type scale).
1
4
  export const JOBS_STYLES = `
2
- .jobs-manager { min-width: 0; display: grid; gap: 16px; container-type: inline-size; }
3
- .jobs-layout { display: grid; grid-template-columns: minmax(160px, 240px) minmax(0, 1fr); gap: 24px; align-items: start; }
4
- .jobs-list, .jobs-detail, .jobs-runs { display: grid; gap: 12px; min-width: 0; }
5
- .job-choice { display: grid; gap: 6px; text-align: left; padding: 14px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); color: inherit; cursor: pointer; overflow-wrap: anywhere; }
6
- .job-choice[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
7
- .job-choice:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
8
- .jobs-manager .settings-actions { flex-wrap: wrap; }
9
- .jobs-manager .settings-observation { max-width: 100%; max-height: 420px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
10
- .jobs-manager .btn, .job-fact { overflow-wrap: anywhere; white-space: normal; text-align: left; }
11
- .jobs-runs .btn { font-family: var(--font-mono); font-size: 12px; }
12
- .jobs-manager textarea { width: 100%; font-family: var(--font-mono); }
5
+ .jobs-manager { min-width: 0; display: grid; gap: 14px; container-type: inline-size; }
13
6
  .jobs-manager [hidden] { display: none !important; }
7
+ .jobs-manager textarea { width: 100%; font-family: var(--font-mono); font-size: var(--fs-xs); }
8
+ .jobs-manager .settings-observation { max-width: 100%; max-height: 420px; margin: 0; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--fs-xs); }
9
+ .jobs-manager .btn { height: 30px; padding: 0 11px; }
10
+ .jobs-layout { display: grid; grid-template-columns: minmax(180px, 240px) minmax(0, 1fr); gap: 20px; align-items: start; }
11
+ .jobs-list, .jobs-detail { display: grid; gap: 12px; min-width: 0; }
12
+
13
+ /* Page bar */
14
+ .jobs-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 16px; min-width: 0; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
15
+ .jobs-bar-title { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
16
+ .jobs-bar-title h1, .jobs-bar-title h2 { font-size: var(--fs-lg); font-weight: 650; letter-spacing: -.01em; }
17
+ .jobs-bar-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 6px; min-width: 0; }
18
+ .jobs-bar-actions .settings-receipt { margin: 0 6px 0 0; color: var(--ink-4); font-size: var(--fs-xs); white-space: pre-line; }
19
+ .jobs-bar-actions .settings-receipt:empty { display: none; }
20
+ .jobs-bar-actions .settings-receipt.settings-error { order: 9; flex-basis: 100%; margin: 4px 0 0; padding: 8px 10px; border-radius: var(--r-sm); background: var(--bad-soft); color: var(--bad); text-align: left; }
21
+ .jobs-receipt .settings-receipt { margin: 0; color: var(--ink-3); font-size: var(--fs-xs); white-space: pre-line; }
22
+ .jobs-receipt .settings-receipt:empty, .jobs-receipt:has(.settings-receipt:empty) { display: none; }
23
+ .jobs-receipt .settings-receipt.settings-error { padding: 8px 10px; border-radius: var(--r-sm); background: var(--bad-soft); color: var(--bad); }
24
+ .job-notice { margin: 0; padding: 8px 12px; border-radius: var(--r-sm); background: var(--surface-3); color: var(--ink-2); font-size: var(--fs-sm); }
25
+
26
+ /* Job index (left rail): two lines per Job, status at the right edge. */
27
+ .jobs-area .jobs-index { display: grid; gap: 10px; min-width: 0; padding-top: 2px; }
28
+ .jobs-area .side-create { margin-bottom: 2px; }
29
+ /* The area title stays whole beside the Jobs sync; a long clock gives way. */
30
+ .manage-rail .side-head:has(#jobs-sync:not([hidden])) .side-title { flex: none; }
31
+ #jobs-sync { min-width: 0; overflow: hidden; white-space: nowrap; }
32
+ #jobs-sync time { overflow: hidden; text-overflow: ellipsis; }
33
+ .jobs-scope { display: grid; gap: 4px; padding: 0 2px; }
34
+ .jobs-scope label { color: var(--ink-4); font-size: var(--fs-2xs); font-weight: 600; text-transform: uppercase; letter-spacing: .07em; }
35
+ .jobs-scope select { width: 100%; min-width: 0; height: 32px; padding-top: 4px; padding-bottom: 4px; font-size: var(--fs-sm); }
36
+ .jobs-scope-help { margin: 0; color: var(--ink-4); font-size: var(--fs-2xs); line-height: 1.4; }
37
+ .jobs-scope-help.is-error, .jobs-index-empty.is-error { color: var(--bad); }
38
+ .jobs-index-body { display: grid; gap: 8px; min-width: 0; }
39
+ .jobs-index-tools { display: grid; gap: 6px; }
40
+ .jobs-index-tools .search-field input { width: 100%; height: 32px; font-size: var(--fs-sm); }
41
+ .jobs-index-count { margin: 0; padding: 0 2px; color: var(--ink-4); font-size: var(--fs-2xs); font-variant-numeric: tabular-nums; }
42
+ .jobs-index-empty { margin: 0; padding: 24px 12px; color: var(--ink-4); font-size: var(--fs-sm); text-align: center; }
43
+ .jobs-list { gap: 1px; }
44
+ .job-row { position: relative; display: flex; align-items: flex-start; gap: 9px; width: 100%; min-width: 0; padding: 7px 8px; border: 0; border-radius: var(--r-sm); background: transparent; color: inherit; text-align: left; cursor: pointer; transition: background var(--t-fast); }
45
+ .job-row > .dot { margin-top: 6px; }
46
+ .job-row:hover { background: var(--surface-3); }
47
+ .job-row[aria-current="true"] { background: var(--surface-3); }
48
+ .job-row[aria-current="true"] .job-row-name { font-weight: 600; }
49
+ .job-row-body { display: grid; gap: 1px; flex: 1; min-width: 0; }
50
+ .job-row-line { display: flex; align-items: center; gap: 8px; min-width: 0; }
51
+ .job-row-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); font-size: var(--fs-sm); font-weight: 500; line-height: 1.45; }
52
+ .job-row-line > .badge { flex: none; height: 18px; padding: 0 6px; font-size: var(--fs-2xs); }
53
+ .job-row-sub { color: var(--ink-4); font-size: var(--fs-2xs); }
54
+ .job-row-sub > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
55
+ .job-row-type { flex: none; max-width: 50%; color: var(--ink-3); }
56
+ .job-row-plan { flex: 1; }
57
+ .job-row-plan::before { content: "·"; margin-right: 6px; color: var(--ink-4); }
58
+ .job-row-none { flex: none; color: var(--ink-4); font-size: var(--fs-2xs); }
59
+
60
+ /* Focus is always visible. */
61
+ .job-row:focus-visible, .job-run-row:focus-visible, .job-copy:focus-visible, .job-more:focus-visible,
62
+ .job-section-action:focus-visible, .job-raw > summary:focus-visible, .job-log > summary:focus-visible, .job-fold > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; box-shadow: none; }
63
+
64
+ /* Job header */
65
+ .job-page { display: grid; gap: 16px; min-width: 0; }
66
+ .job-header { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 10px 16px; min-width: 0; }
67
+ .job-title { display: grid; gap: 6px; flex: 1 1 360px; min-width: 0; }
68
+ .job-title h2 { font-size: var(--fs-xl); font-weight: 650; letter-spacing: -.015em; line-height: 1.25; overflow-wrap: anywhere; }
69
+ .job-badges { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
70
+ .job-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 0; color: var(--ink-3); font-size: var(--fs-xs); }
71
+ .job-meta-item { display: inline-flex; align-items: center; gap: 4px; min-width: 0; }
72
+ .job-meta-item + .job-meta-item::before { content: "·"; margin: 0 8px; color: var(--ink-4); }
73
+ .job-meta a { color: inherit; }
74
+ .job-actions { display: flex; flex-wrap: wrap; gap: 6px; flex: none; }
75
+ .job-note { margin: 0; color: var(--ink-4); font-size: var(--fs-xs); line-height: 1.5; }
76
+ .job-note.is-error { color: var(--bad); }
77
+
78
+ /* Overview strip: numbers first, the last 30 Runs as a status strip. */
79
+ .job-overview { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px 24px; padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface); }
80
+ .job-stats { display: flex; flex-wrap: wrap; gap: 10px 0; min-width: 0; }
81
+ .job-stat { display: grid; align-content: start; gap: 2px; min-width: 0; padding: 0 18px; border-left: 1px solid var(--line); }
82
+ .job-stat:first-child { padding-left: 0; border-left: 0; }
83
+ .job-stat-label { color: var(--ink-4); font-size: var(--fs-2xs); white-space: nowrap; }
84
+ .job-stat-value { color: var(--ink); font-size: var(--fs-lg); font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.3; }
85
+ .job-stat-sub { color: var(--ink-4); font-size: var(--fs-2xs); font-variant-numeric: tabular-nums; white-space: nowrap; }
86
+ .job-stat-run { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; min-height: 26px; }
87
+ .job-stat-run code { font-size: var(--fs-xs); color: var(--ink-2); }
88
+ .job-stat-run .badge { height: 20px; padding: 0 7px; font-size: var(--fs-2xs); }
89
+ .job-spark { display: flex; align-items: stretch; gap: 2px; height: 26px; }
90
+ .job-spark-bar { width: 6px; border-radius: 2px; background: var(--tone, var(--line-3)); opacity: .9; }
91
+ .job-spark-bar.tone-idle { background: var(--line-3); }
92
+
93
+ /* About the Job: description and configuration | plans and definition */
94
+ .job-purpose { display: flex; align-items: flex-start; gap: 4px; min-width: 0; padding: 10px 6px 10px 12px; border-left: 2px solid var(--line-3); border-radius: 0 var(--r-sm) var(--r-sm) 0; background: var(--surface-3); }
95
+ .job-purpose .job-prose { flex: 1; max-height: 180px; line-height: 1.6; }
96
+ .job-purpose.is-note { display: block; margin: 0; padding: 8px 12px; color: var(--ink-2); font-size: var(--fs-sm); }
97
+ .job-about { display: grid; grid-template-columns: minmax(0, 1fr) minmax(240px, 300px); gap: 24px; align-items: start; min-width: 0; }
98
+ .job-primary, .job-aside { display: grid; gap: 22px; min-width: 0; }
99
+ .job-section { display: grid; gap: 8px; min-width: 0; }
100
+ .job-section-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 26px; }
101
+ .job-section-head h4 { color: var(--ink-2); font-size: var(--fs-sm); font-weight: 600; }
102
+ .job-section-count { color: var(--ink-4); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
103
+ .job-section-title { margin: 6px 0 0; color: var(--ink-3); font-size: var(--fs-xs); font-weight: 600; }
104
+ .job-section-action { display: inline-flex; align-items: center; gap: 4px; height: 26px; padding: 0 8px; border: 0; border-radius: var(--r-sm); background: transparent; color: var(--ink-3); font-size: var(--fs-xs); font-weight: 500; }
105
+ .job-section-action:hover { background: var(--surface-3); color: var(--ink); }
106
+ .job-section-action .icon { width: 14px; height: 14px; }
107
+ .job-empty-block { margin: 0; padding: 14px 12px; border: 1px dashed var(--line-2); border-radius: var(--r-md); color: var(--ink-4); font-size: var(--fs-sm); text-align: center; }
108
+ .job-blank { display: grid; justify-items: center; gap: 10px; padding: 64px 16px; color: var(--ink-4); text-align: center; }
109
+
110
+ /* Run history: a table on wide containers, two lines on narrow ones. */
111
+ .job-history { display: grid; gap: 8px; min-width: 0; }
112
+ .job-filters { gap: 4px; }
113
+ .job-filters .chip-tab { height: 26px; padding: 0 9px; }
114
+ .job-filters .count { color: inherit; opacity: .7; font-variant-numeric: tabular-nums; }
115
+ .job-history-body { border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; background: var(--surface); }
116
+ .job-runs-head, .job-run-row { display: grid; grid-template-columns: 104px minmax(0, 1fr) 112px 72px 16px; align-items: center; gap: 12px; }
117
+ .job-runs-head { padding: 6px 12px; border-bottom: 1px solid var(--line); color: var(--ink-4); font-size: var(--fs-2xs); font-weight: 500; }
118
+ .job-runs-head > :nth-child(n+3), .job-run-when, .job-run-span { text-align: right; }
119
+ .jobs-runs { display: grid; }
120
+ .job-run-row { scroll-margin-top: calc(var(--header-h, 56px) + 12px); width: 100%; min-width: 0; padding: 7px 12px; border: 0; border-top: 1px solid var(--line); background: transparent; color: inherit; text-align: left; cursor: pointer; transition: background var(--t-fast); }
121
+ .jobs-runs > .job-run-row:first-child { border-top: 0; }
122
+ .job-run-row:hover { background: var(--surface-3); }
123
+ .job-run-row[aria-current="true"] { background: var(--surface-3); box-shadow: inset 2px 0 0 var(--accent); }
124
+ .job-run-caret { display: inline-grid; place-items: center; color: var(--ink-4); }
125
+ .job-run-caret .icon { width: 14px; height: 14px; transition: transform var(--t-fast); }
126
+ .job-run-row[aria-expanded="true"] .job-run-caret .icon { transform: rotate(90deg); color: var(--ink-2); }
127
+ .job-run-status .badge { height: 20px; padding: 0 7px; font-size: var(--fs-2xs); }
128
+ .job-run-main { display: grid; gap: 1px; min-width: 0; }
129
+ .job-run-summary { overflow: hidden; color: var(--ink); font-size: var(--fs-sm); text-overflow: ellipsis; white-space: nowrap; }
130
+ .job-run-sub { display: flex; gap: 8px; min-width: 0; color: var(--ink-4); font-size: var(--fs-2xs); white-space: nowrap; }
131
+ .job-run-sub code { color: var(--ink-3); }
132
+ .job-run-when, .job-run-span { color: var(--ink-3); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; white-space: nowrap; }
133
+ .job-more { width: 100%; padding: 8px 12px; border: 0; border-top: 1px solid var(--line); background: transparent; color: var(--ink-3); font-size: var(--fs-xs); font-weight: 500; }
134
+ .job-more:hover { background: var(--surface-3); color: var(--ink); }
135
+
136
+ /* Selected Run: opened in place, directly under its row */
137
+ .job-run-inline { padding: 4px 12px 14px; border-top: 1px dashed var(--line); background: var(--surface-3); box-shadow: inset 2px 0 0 var(--accent); }
138
+ .jobs-runs > .job-run-inline:first-child { border-top: 0; }
139
+ .job-run-inline .job-run-panel { border-color: var(--line); box-shadow: var(--shadow-1); }
140
+ .job-run-inline:empty, .job-run-panel > section:empty, .job-run-artifacts > div:empty { display: none; }
141
+ .job-run-panel > .job-kv { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 24px; }
142
+ .job-run-panel > .job-kv > .job-kv-row:nth-child(2) { border-top: 0; }
143
+ .job-run-panel { display: grid; gap: 12px; min-width: 0; padding: 14px; border: 1px solid var(--line-2); border-radius: var(--r-md); background: var(--surface); }
144
+ .job-run-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
145
+ .job-run-head > .job-badges { flex: none; flex-wrap: nowrap; }
146
+ .job-run-title { display: flex; flex: 1; flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; min-height: 28px; }
147
+ .job-run-title h4 { font-size: var(--fs-sm); font-weight: 600; }
148
+ .job-run-close { width: 28px; height: 28px; }
149
+ .job-result { margin: 0; padding: 8px 10px; border-left: 2px solid var(--tone, var(--line-3)); border-radius: 0 var(--r-sm) var(--r-sm) 0; background: var(--tone-soft, var(--surface-3)); color: var(--ink); font-size: var(--fs-sm); overflow-wrap: anywhere; }
150
+ .job-result-label { margin-right: 8px; color: var(--ink-3); font-size: var(--fs-xs); font-weight: 600; }
151
+ .job-run-steps { display: grid; margin: 0; padding: 0; list-style: none; border: 1px solid var(--line); border-radius: var(--r-sm); }
152
+ .job-run-step { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; padding: 6px 10px; border-top: 1px solid var(--line); font-size: var(--fs-xs); }
153
+ .job-run-step:first-child { border-top: 0; }
154
+ .job-run-step .badge { height: 20px; font-size: var(--fs-2xs); }
155
+ .job-run-step-meta { margin-left: auto; color: var(--ink-4); font-variant-numeric: tabular-nums; }
156
+ .job-logs { display: grid; gap: 6px; }
157
+ .job-log { border: 1px solid var(--line); border-radius: var(--r-sm); }
158
+ .job-log > summary { display: flex; align-items: center; gap: 8px; padding: 6px 10px; font-size: var(--fs-xs); cursor: pointer; }
159
+ .job-log-text { max-height: 320px; margin: 0; padding: 10px; overflow: auto; border-top: 1px solid var(--line); background: var(--sunken); font-size: var(--fs-2xs); line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; }
160
+ .job-artifact-list { display: flex; flex-wrap: wrap; gap: 6px; }
161
+ .job-inline-action { justify-self: start; height: 26px !important; padding: 0 8px !important; color: var(--ink-3); font-size: var(--fs-xs); }
162
+ .job-run-evidence, .job-run-artifacts { display: grid; gap: 6px; min-width: 0; }
163
+ .job-advanced { display: grid; gap: 6px; }
164
+
165
+ /* Label/value lists (configuration, definition, Run facts) */
166
+ .job-kv { display: grid; margin: 0; min-width: 0; }
167
+ .job-kv-row { display: grid; grid-template-columns: 132px minmax(0, 1fr); gap: 12px; padding: 6px 0; border-top: 1px solid var(--line); font-size: var(--fs-sm); }
168
+ .job-kv-row:first-child { border-top: 0; }
169
+ .job-kv dt { color: var(--ink-4); font-size: var(--fs-xs); line-height: 1.7; overflow-wrap: anywhere; }
170
+ .job-kv dd { margin: 0; min-width: 0; color: var(--ink); overflow-wrap: anywhere; }
171
+ .job-kv.is-compact .job-kv-row { grid-template-columns: 84px minmax(0, 1fr); padding: 5px 0; }
172
+ .job-kv.is-nested { padding-left: 10px; border-left: 2px solid var(--line); }
173
+ .job-kv.is-nested .job-kv-row { grid-template-columns: minmax(80px, 120px) minmax(0, 1fr); padding: 3px 0; border-top: 0; }
174
+ .job-key { color: var(--ink-3); font-size: var(--fs-xs); }
175
+ .job-empty { color: var(--ink-4); }
176
+ .job-muted { color: var(--ink-4); font-size: var(--fs-xs); }
177
+ .job-text { white-space: pre-wrap; }
178
+ .job-kind { font-weight: 600; }
179
+ .job-prose { max-height: 240px; margin: 0; overflow: auto; color: var(--ink); font-family: inherit; font-size: var(--fs-sm); white-space: pre-wrap; overflow-wrap: anywhere; }
180
+ .job-chips { display: flex; flex-wrap: wrap; gap: 4px; }
181
+ .job-chip { padding: 1px 6px; border-radius: var(--r-xs); background: var(--surface-3); color: var(--ink-2); font-size: var(--fs-2xs); overflow-wrap: anywhere; }
182
+ .job-chip-key { color: var(--ink); font-weight: 600; }
183
+ .job-list { margin: 0; padding-left: 18px; }
184
+ .job-fold > summary { color: var(--ink-3); font-size: var(--fs-xs); cursor: pointer; }
185
+ .job-time { display: inline-flex; flex-wrap: wrap; gap: 0 8px; font-variant-numeric: tabular-nums; }
186
+ .job-time-ago { color: var(--ink-4); font-size: var(--fs-xs); }
187
+ .job-id, .job-path { display: inline-flex; align-items: center; gap: 2px; max-width: 100%; min-width: 0; }
188
+ .job-path code { min-width: 0; overflow-wrap: anywhere; font-size: var(--fs-xs); }
189
+ .job-copy { display: inline-grid; place-items: center; width: 22px; height: 22px; flex: none; padding: 0; border: 0; border-radius: var(--r-xs); background: transparent; color: var(--ink-4); }
190
+ .job-copy:hover { background: var(--surface-3); color: var(--ink); }
191
+ .job-copy .icon { width: 13px; height: 13px; }
192
+ .job-copy.is-copied { color: var(--ok); }
193
+ .job-copy-text { display: inline-flex; align-items: center; width: auto; gap: 4px; padding: 0 6px; font-size: var(--fs-2xs); white-space: nowrap; }
194
+
195
+ /* Configuration: steps as numbered rows with their command */
196
+ .job-config { display: grid; gap: 14px; min-width: 0; }
197
+ .job-steps { display: grid; margin: 0; padding: 0; list-style: none; border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }
198
+ .job-step { display: grid; gap: 6px; padding: 10px 12px; border-top: 1px solid var(--line); min-width: 0; }
199
+ .job-step:first-child { border-top: 0; }
200
+ .job-step-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; min-width: 0; }
201
+ .job-step-index { display: inline-grid; place-items: center; width: 18px; height: 18px; border-radius: var(--r-pill); background: var(--surface-3); color: var(--ink-3); font-size: var(--fs-2xs); font-weight: 600; font-variant-numeric: tabular-nums; }
202
+ .job-step-name { color: var(--ink); font-size: var(--fs-xs); font-weight: 600; }
203
+ .job-tag { max-width: 100%; padding: 0 6px; border: 1px solid var(--line-2); border-radius: var(--r-xs); color: var(--ink-3); font-size: var(--fs-2xs); line-height: 18px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
204
+ .job-command { display: flex; align-items: flex-start; gap: 4px; min-width: 0; padding: 6px 6px 6px 10px; border-radius: var(--r-sm); background: var(--sunken); }
205
+ .job-command pre { flex: 1; min-width: 0; margin: 0; padding-top: 2px; color: var(--ink); font-size: var(--fs-xs); line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; }
206
+ .job-command.is-prose pre { font-family: inherit; font-size: var(--fs-sm); }
207
+ .job-step-env { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; }
208
+ .job-step-label { color: var(--ink-4); font-size: var(--fs-2xs); }
209
+
210
+ /* Exact JSON, one disclosure away */
211
+ .job-raw { position: relative; min-width: 0; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--surface); overflow: hidden; }
212
+ .job-raw > summary { display: flex; align-items: center; gap: 8px; min-height: 34px; padding: 7px 110px 7px 12px; color: var(--ink-3); font-size: var(--fs-xs); cursor: pointer; list-style: none; }
213
+ .job-raw > summary::before { content: "›"; font-size: 16px; line-height: 1; transform-origin: center; }
214
+ .job-raw[open] > summary::before { transform: rotate(90deg); }
215
+ .job-raw > summary:hover { color: var(--ink); }
216
+ .job-raw-hint { padding: 0 5px; border: 1px solid var(--line-2); border-radius: var(--r-xs); color: var(--ink-4); font-family: var(--font-mono); font-size: 10px; line-height: 16px; }
217
+ .job-raw-body { min-width: 0; border-top: 1px solid var(--line); }
218
+ .job-raw-tools { position: absolute; right: 8px; top: 4px; display: flex; }
219
+ .job-raw .settings-observation { display: block; margin: 0; padding: 14px; max-height: 320px; overflow: auto; background: var(--sunken); color: var(--ink-2); font: var(--fs-xs)/1.65 var(--font-mono); white-space: pre; tab-size: 2; }
220
+ .job-form-advanced { min-width: 0; color: var(--ink-3); font-size: var(--fs-xs); }
221
+ .job-form-advanced > summary { cursor: pointer; padding: 8px 0; }
222
+ .job-form-advanced[open] > :not(summary) { margin-top: 8px; }
223
+ .job-form-advanced .job-raw { margin-top: 10px; }
224
+ .job-form-advanced:has(:focus-visible), .job-raw > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
225
+
226
+ /* Plans */
227
+ .job-plans { display: grid; margin: 0; padding: 0; list-style: none; border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface); }
228
+ .job-plan { display: grid; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--line); min-width: 0; }
229
+ .job-plan:first-child { border-top: 0; }
230
+ .job-plan-main { display: grid; gap: 4px; min-width: 0; }
231
+ .job-plan-line { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; min-width: 0; }
232
+ .job-plan-line .badge { height: 20px; padding: 0 7px; font-size: var(--fs-2xs); }
233
+ .job-plan-rule { font-size: var(--fs-sm); font-weight: 600; }
234
+ .job-plan-exact { color: var(--ink-4); font-family: var(--font-mono); font-size: var(--fs-2xs); overflow-wrap: anywhere; }
235
+ .job-plan-sub { color: var(--ink-4); font-size: var(--fs-2xs); }
236
+ .job-plan-sub code { color: var(--ink-3); overflow-wrap: anywhere; }
237
+ .job-plan-actions { display: flex; flex-wrap: wrap; gap: 4px; }
238
+ .job-plan-actions .btn { height: 26px; padding: 0 8px; font-size: var(--fs-xs); }
239
+
240
+ /* Editor (a modal dialog, as the workbench's new Task) */
241
+ .job-dialog { width: min(760px, calc(100vw - 32px)); }
242
+ .job-dialog .job-form { gap: 14px; }
243
+ .job-dialog .dialog-actions { position: sticky; bottom: -20px; margin: 0 -20px -20px; padding: 12px 20px; border-top: 1px solid var(--line); background: var(--surface); }
244
+ .job-dialog-receipt { margin: 0; color: var(--ink-3); font-size: var(--fs-xs); white-space: pre-line; }
245
+ .job-dialog-receipt:empty { display: none; }
246
+ .job-dialog-receipt.settings-error { padding: 8px 10px; border-radius: var(--r-sm); background: var(--bad-soft); color: var(--bad); }
247
+ .job-fieldset { display: grid; gap: 10px; min-width: 0; margin: 0; padding: 12px 0 0; border: 0; border-top: 1px solid var(--line); }
248
+ .job-fieldset > legend { padding: 0 8px 0 0; color: var(--ink-2); font-size: var(--fs-sm); font-weight: 600; }
249
+ .job-field-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px 12px; }
250
+ .job-field-stack { display: grid; gap: 10px; }
251
+ .job-field-stack[hidden], .job-form [hidden] { display: none; }
252
+ .job-bound-target { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 10px 12px; border-radius: var(--r-sm); background: var(--sunken); color: var(--ink-3); font-size: var(--fs-sm); }
253
+ .job-bound-target strong { color: var(--ink); font-weight: 500; overflow-wrap: anywhere; }
254
+ .job-weekdays { display: flex; flex-wrap: wrap; gap: 6px; }
255
+ .job-weekdays .btn { min-width: 38px; height: 34px; padding: 0 10px; }
256
+ .job-weekdays .btn[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--surface); }
257
+ .job-rule-preview { padding: 10px 12px; margin: 0; border-left: 2px solid var(--accent); background: var(--sunken); color: var(--ink-2); font-size: var(--fs-sm); overflow-wrap: anywhere; }
258
+ .job-rule-preview { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; border-radius: var(--r-sm); }
259
+ .job-rule-preview.is-error { border-color: var(--bad); color: var(--bad); background: var(--bad-soft); }
260
+ .job-plan-expression { color: var(--ink-3); font-size: var(--fs-xs); overflow-wrap: anywhere; }
261
+ .job-clock input { font-variant-numeric: tabular-nums; }
262
+ .job-time-shortcuts { display: flex; flex-wrap: wrap; gap: 6px; }
263
+ .job-time-shortcuts .btn { height: 28px; padding: 0 10px; color: var(--ink-3); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
264
+ .job-cron-guide { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); margin: 0; padding: 12px; gap: 8px; border: 1px solid var(--line); border-radius: var(--r-sm); }
265
+ .job-cron-guide dt { color: var(--ink-3); font-size: var(--fs-xs); }
266
+ .job-cron-guide dd { display: grid; gap: 6px; margin: 6px 0 0; overflow-wrap: anywhere; }
267
+ .job-cron-guide dd span { color: var(--ink-4); font-size: var(--fs-2xs); }
268
+ @media (max-width: 480px) {
269
+ .job-cron-guide { grid-template-columns: repeat(3, minmax(0, 1fr)); }
270
+ }
271
+ .job-edit-row { display: grid; gap: 10px; padding: 12px; border: 1px solid var(--line); border-radius: var(--r-sm); min-width: 0; }
272
+ .job-edit-row > .btn, .job-field-stack > .btn { justify-self: start; }
273
+ .job-form textarea { width: 100%; resize: vertical; min-height: 88px; font-size: var(--fs-sm); }
274
+ .job-form input, .job-form select, .job-form textarea { min-width: 0; }
275
+ .jobs-manager form .field > span { color: var(--ink-3); font-size: var(--fs-xs); font-weight: 500; }
276
+ .jobs-manager form input, .jobs-manager form select { height: 32px; padding-top: 4px; padding-bottom: 4px; font-size: var(--fs-sm); }
277
+ .job-config-preview { display: grid; gap: 8px; padding: 12px; border: 1px dashed var(--line-2); border-radius: var(--r-md); }
278
+ .job-preview-label { color: var(--ink-4); font-size: var(--fs-2xs); font-weight: 600; text-transform: uppercase; letter-spacing: .06em; }
279
+
14
280
  @media (max-width: 720px) { .jobs-layout { grid-template-columns: minmax(0, 1fr); } }
281
+ @container (max-width: 860px) {
282
+ .job-about { grid-template-columns: minmax(0, 1fr); }
283
+ }
15
284
  @container (max-width: 640px) {
16
285
  .jobs-layout { grid-template-columns: minmax(0, 1fr); }
17
- .jobs-list { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
286
+ .jobs-bar-refresh > span { display: none; }
287
+ .job-actions { flex-wrap: wrap; }
288
+ .job-overview { padding: 10px 12px; }
289
+ .job-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 12px; width: 100%; }
290
+ .job-stat { padding: 0; border-left: 0; }
291
+ .job-spark { width: 100%; }
292
+ .job-spark-bar { flex: 1; width: auto; max-width: 8px; }
293
+ .job-runs-head { display: none; }
294
+ .job-run-row { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "main status" "main when"; gap: 2px 10px; padding: 8px 10px; }
295
+ .job-run-caret { display: none; }
296
+ .job-run-inline { padding: 2px 8px 10px; }
297
+ .job-run-status { grid-area: status; justify-self: end; }
298
+ .job-run-main { grid-area: main; }
299
+ .job-run-when { grid-area: when; }
300
+ .job-run-span { display: none; }
301
+ .job-run-summary { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
302
+ .job-kv-row, .job-kv.is-compact .job-kv-row { grid-template-columns: 92px minmax(0, 1fr); gap: 10px; }
303
+ .job-kv.is-nested .job-kv-row { grid-template-columns: minmax(0, 1fr); }
304
+ .job-run-panel { padding: 12px; }
305
+ .job-run-panel > .job-kv { grid-template-columns: minmax(0, 1fr); }
306
+ .job-run-panel > .job-kv > .job-kv-row:nth-child(2) { border-top: 1px solid var(--line); }
18
307
  }
19
308
  `;
@@ -30,8 +30,6 @@ export const SIDEBAR_STYLES = `
30
30
  .search-field:focus-within kbd{opacity:0}
31
31
  .side-search-body{width:34px;height:34px;border:1px solid var(--line-2);border-radius:var(--r-md);background:var(--surface-2)}
32
32
  .side-search-body:hover:not(:disabled){border-color:var(--line-3)}
33
- .source-seg{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));width:100%}
34
- .source-seg .seg-btn{justify-content:center;min-width:0;overflow:hidden;text-overflow:ellipsis}
35
33
  .attention-bar:empty{display:none}
36
34
  .attention-list{display:flex;flex-wrap:wrap;gap:6px}
37
35
  .attention-pill{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 9px;border:1px solid transparent;border-radius:var(--r-pill);background:var(--tone-soft);color:var(--tone);font-size:var(--fs-xs);font-weight:500}
@@ -63,6 +61,7 @@ export const SIDEBAR_STYLES = `
63
61
  .task-time{flex:none;color:var(--ink-4);font-size:var(--fs-2xs);font-variant-numeric:tabular-nums}
64
62
  .task-sub{gap:8px;height:18px;color:var(--ink-4);font-size:var(--fs-2xs)}
65
63
  .task-id{font-size:var(--fs-2xs);line-height:1;color:var(--ink-3)}
64
+ .task-source{flex:none;padding:0 5px;border:1px solid var(--line-2);border-radius:var(--r-pill);color:var(--ink-3);font-size:10px;line-height:15px}
66
65
  .task-count{display:inline-flex;align-items:center;gap:3px;line-height:1;white-space:nowrap}
67
66
  .task-count .icon{width:11px;height:11px}
68
67
  .task-signals{display:inline-flex;gap:4px;margin-left:auto}
@@ -720,7 +720,7 @@ async function handleHttpRequest(request, response, store, dependencies, token,
720
720
  try {
721
721
  const assetVersion = new URL(request.url, "http://localhost").searchParams.get("v");
722
722
  const asset = findWebAsset(pathname, assetVersion);
723
- const managementPage = /^\/(?:models(?:\/(?:providers|catalog|quota))?|settings(?:\/(?:preferences|skills|jobs|permissions|server|connection))?)\/?$/.test(pathname);
723
+ const managementPage = /^\/(?:models(?:\/(?:providers|catalog|quota))?|settings(?:\/(?:preferences|skills|jobs|permissions|server|connection))?|jobs)\/?$/.test(pathname);
724
724
  if (pathname === "/" || pathname === "/index.html" || managementPage) {
725
725
  sendText(response, 200, "text/html; charset=utf-8", prepareWebDocument(managementPage ? MODELS_HTML.replace("__YUI_WEB_TOKEN__", token) : dashboardHtml(token), managementPage ? "/assets/js/app/manage.js" : "/assets/app.js"), method === "HEAD");
726
726
  }
@@ -735,6 +735,11 @@ async function handleHttpRequest(request, response, store, dependencies, token,
735
735
  ...[...query].filter(([key]) => key !== "query").flatMap(([key, value]) => [`--${key}`, value])]);
736
736
  sendJson(response, 200, store.transaction(reader => searchTaskBodies(reader, options)), method === "HEAD");
737
737
  }
738
+ else if (pathname === "/api/jobs/scopes") {
739
+ if (new URL(request.url, "http://localhost").search !== "")
740
+ throw usageError("Job scope discovery takes no options.");
741
+ sendJson(response, 200, store.readTransaction(reader => readWebJobScopes(reader)), method === "HEAD");
742
+ }
738
743
  else if (pathname === "/api/dashboard" || pathname === "/api/dashboard/sessions") {
739
744
  const query = new URL(request.url, "http://localhost").searchParams;
740
745
  const options = parseTaskCatalogOptions([...query].flatMap(([key, value]) => key === "all"
@@ -1281,6 +1286,28 @@ function rejectUpgrade(socket, status, message) {
1281
1286
  + "Connection: close\r\n"
1282
1287
  + "Content-Length: 0\r\n\r\n");
1283
1288
  }
1289
+ /** Bounded owner discovery for the Jobs entry: counts per owning scope only.
1290
+ * Records are still listed through the scope's own Job/Schedule reads, with
1291
+ * their existing authority checks; this never lists a definition itself. */
1292
+ const JOB_SCOPE_TASK_LIMIT = 200;
1293
+ export function readWebJobScopes(reader) {
1294
+ const empty = { definitions: 0, schedules: 0, enabledSchedules: 0 };
1295
+ let global = empty;
1296
+ const tasks = [];
1297
+ for (const summary of reader.listJobScopeSummaries()) {
1298
+ const counts = { definitions: summary.definitions, schedules: summary.schedules, enabledSchedules: summary.enabledSchedules };
1299
+ if (summary.scope.kind === "global") {
1300
+ global = counts;
1301
+ continue;
1302
+ }
1303
+ const task = reader.getTask(summary.scope.taskId);
1304
+ if (task !== null)
1305
+ tasks.push({ taskId: task.id, title: task.title, status: task.status, ...counts });
1306
+ }
1307
+ tasks.sort((a, b) => Number(b.taskId.slice(5)) - Number(a.taskId.slice(5)));
1308
+ return { global, tasks: tasks.slice(0, JOB_SCOPE_TASK_LIMIT), total: tasks.length,
1309
+ omitted: Math.max(0, tasks.length - JOB_SCOPE_TASK_LIMIT), scope: "owner-counts-only; records use each scope's authorized reads" };
1310
+ }
1284
1311
  function dashboardHtml(token) {
1285
1312
  return DASHBOARD_HTML.replace('content="__YUI_WEB_TOKEN__"', `content="${escapeHtmlAttribute(token)}"`);
1286
1313
  }
@@ -0,0 +1,78 @@
1
+ # Global Shell Jobs
2
+
3
+ A global Shell Job owns its command steps, environment and text script files.
4
+ It needs neither a Task nor a Project. The Web Jobs editor and CLI edit the
5
+ same revisioned definition. Saving never starts a Run.
6
+
7
+ Commands execute as the local Yui user, **not in a security sandbox**. Do not
8
+ store credentials in commands, scripts or environment variables. Task-scoped
9
+ Shell Jobs retain their existing managed Project/workspace authority.
10
+
11
+ ## CLI
12
+
13
+ Create a command-only Job:
14
+
15
+ ```sh
16
+ yui job define --global --job hello --title "Hello" --command 'printf "hello\n"'
17
+ ```
18
+
19
+ Import a local script into a Job-owned relative path (the file contents are
20
+ copied, not linked to the mutable source file):
21
+
22
+ ```sh
23
+ yui job define --global --job check --title "Daily check" \
24
+ --command 'sh scripts/check.sh' --script scripts/check.sh=./check.sh
25
+ yui job show --global --job check --json
26
+ ```
27
+
28
+ Edit the local script and upload a replacement using the revision returned by
29
+ `show`. Existing commands and other script files are preserved:
30
+
31
+ ```sh
32
+ yui job update --global --job check --revision 1 \
33
+ --script scripts/check.sh=./check.sh
34
+ yui job update --global --job hello --revision 1 --command 'printf "updated\n"'
35
+ ```
36
+
37
+ `--script` can be repeated for different paths. To edit multiple steps,
38
+ environment variables, or remove files, use `--input-file config.json` with
39
+ the complete configuration:
40
+
41
+ ```json
42
+ {
43
+ "kind": "shell",
44
+ "env": {},
45
+ "steps": [{"name": "check", "command": "sh scripts/check.sh", "timeoutMs": 60000}],
46
+ "files": [{"path": "scripts/check.sh", "content": "printf 'ok\\n'\n"}]
47
+ }
48
+ ```
49
+
50
+ Pass `--title` with complete-configuration define/update. Re-read the current
51
+ revision after a conflict instead of blindly retrying. A full configuration
52
+ replaces the previous one; shorthand edits preserve unrelated fields.
53
+
54
+ Run explicitly and inspect the exact returned Run:
55
+
56
+ ```sh
57
+ yui job run --global --job check --request-id check-20261023-1
58
+ yui job runs --global --job check
59
+ yui job logs --global --job check --run <returned-run-id>
60
+ ```
61
+
62
+ ## Files, history and schedules
63
+
64
+ Each admitted Run captures the exact configuration and script contents. The
65
+ runner materializes files in a fresh `artifacts/jobs/global/<run-id>/workspace`
66
+ directory inside the selected Yui Home. Earlier Runs are not changed by later
67
+ edits. Script paths are relative, cannot traverse directories, and are limited
68
+ to 64 files / 512 KiB total. Use `sh scripts/check.sh` or another installed
69
+ interpreter; script names do not choose an interpreter automatically.
70
+
71
+ Schedules use the existing explicit service authorization, bound to the Job's
72
+ revision. Editing commands or scripts invalidates the old schedule binding;
73
+ inspect and explicitly re-enable/re-authorize the schedule before future runs.
74
+ Saving a new script never silently changes an already authorized command.
75
+
76
+ Storage 1.23 declares this additive payload change. Upgrade an existing Home
77
+ through the normal authorized upgrade path; do not run a new build against an
78
+ older shared Home or rewrite its records by hand.
package/docs/job-runs.md CHANGED
@@ -176,9 +176,12 @@ identities. Ordinary readers use only the current contract.
176
176
  start is not confirmation. The snapshot is derived exclusively from the immutable
177
177
  Run fields, never a mutable current Job definition. Scope, executor (`shell` or
178
178
  controlled `builtin`, or `task`), target and trigger are separate concepts. Task
179
- delegation is available in either management scope; shell remains Task-only and
180
- builtin remains global-only. There is no extensible empty
181
- executor/plugin configuration or arbitrary global shell.
179
+ delegation is available in either management scope; builtin remains global-only.
180
+ Global Shell Jobs own revisioned command steps and script files, without a Task
181
+ or Project. Task Shell Jobs retain managed-workspace authority.
182
+ See [Global Shell Jobs](global-shell-jobs.md) for Web/CLI editing, per-Run file
183
+ snapshots and schedule reauthorization. There is no extensible empty executor
184
+ or plugin configuration.
182
185
 
183
186
  ## Delegating delivery to a Task
184
187