@zq-silk/yui 2.7.1 → 2.8.1

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 (92) hide show
  1. package/dist/cli/commandCatalog.js +8 -7
  2. package/dist/cli/updateCommand.js +3 -0
  3. package/dist/cli/updatePorts.js +20 -0
  4. package/dist/commands/configCommands.js +1 -0
  5. package/dist/commands/globalRoleCommands.js +42 -35
  6. package/dist/commands/inboxCommands.js +60 -40
  7. package/dist/commands/taskCommands.js +7 -3
  8. package/dist/commands/taskRoleRuntimeStatus.js +2 -0
  9. package/dist/context/runContextPack.js +48 -3
  10. package/dist/context/taskContext.js +36 -12
  11. package/dist/controlPlaneCli.js +64 -17
  12. package/dist/controller/controller.js +5 -0
  13. package/dist/controller/fileSchedulerStoreAdapter.js +9 -4
  14. package/dist/controller/globalInputDelivery.js +17 -5
  15. package/dist/controller/globalRuntimeAttention.js +135 -5
  16. package/dist/controller/runtime.js +4 -0
  17. package/dist/controller/structuredProviderObservation.js +51 -15
  18. package/dist/controller/taskHostTerminalRecovery.js +33 -0
  19. package/dist/executor/fileRoleLaunchPlanner.js +4 -9
  20. package/dist/message/inboxReceipt.js +30 -0
  21. package/dist/message/taskInboxOwnership.js +25 -3
  22. package/dist/models/modelManagement.js +469 -0
  23. package/dist/models/modelManagementConfig.js +46 -0
  24. package/dist/models/modelManagementFiles.js +90 -0
  25. package/dist/models/modelManagementHttp.js +90 -0
  26. package/dist/models/modelManagementTypes.js +1 -0
  27. package/dist/output/boundedRead.js +43 -14
  28. package/dist/runtime/agentEndpointIdentity.js +8 -4
  29. package/dist/runtime/agentEndpointOwnership.js +5 -6
  30. package/dist/runtime/agentHost.js +62 -10
  31. package/dist/runtime/agentHostImplementation.js +18 -0
  32. package/dist/runtime/managedCaller.js +25 -1
  33. package/dist/runtime/managedIdentity.js +1 -1
  34. package/dist/runtime/providerRetry.js +19 -1
  35. package/dist/runtime/sessionCaller.js +53 -0
  36. package/dist/storage/contextRecords.js +28 -0
  37. package/dist/storage/maintenanceCaller.js +69 -0
  38. package/dist/storage/sqliteSchema.js +12 -2
  39. package/dist/storage/sqliteStore.js +9 -5
  40. package/dist/storage/storageVersions.js +1 -1
  41. package/dist/storage/taskStore.js +4 -0
  42. package/dist/task/completionReadiness.js +3 -3
  43. package/dist/web/assets/assetManifest.js +28 -2
  44. package/dist/web/assets/client/app/manage.js +6 -0
  45. package/dist/web/assets/client/app/models.js +20 -0
  46. package/dist/web/assets/client/app/settings.js +41 -9
  47. package/dist/web/assets/client/i18n/en.js +180 -2
  48. package/dist/web/assets/client/i18n/zh-CN.js +178 -2
  49. package/dist/web/assets/client/index.js +8 -0
  50. package/dist/web/assets/client/layout/manageShell.js +94 -0
  51. package/dist/web/assets/client/lib/api.js +13 -1
  52. package/dist/web/assets/client/lib/i18n.js +5 -3
  53. package/dist/web/assets/client/views/dock/conversation.js +231 -80
  54. package/dist/web/assets/client/views/models.js +777 -0
  55. package/dist/web/assets/client/views/skills.js +22 -15
  56. package/dist/web/assets/shared/icons.js +2 -0
  57. package/dist/web/assets/shared/manageNav.js +13 -0
  58. package/dist/web/assets/shell/manage.js +54 -0
  59. package/dist/web/assets/shell/models.js +7 -0
  60. package/dist/web/assets/shell/settings.js +1 -35
  61. package/dist/web/assets/shell/settingsContent.js +30 -0
  62. package/dist/web/assets/shell/sidebar.js +1 -0
  63. package/dist/web/assets/styles/index.js +6 -0
  64. package/dist/web/assets/styles/views/conversation.js +133 -0
  65. package/dist/web/assets/styles/views/manage.js +71 -0
  66. package/dist/web/assets/styles/views/models.js +129 -0
  67. package/dist/web/assets/styles/views/settings.js +28 -14
  68. package/dist/web/assets/styles/views/sidebar.js +1 -0
  69. package/dist/web/webServer.js +59 -9
  70. package/docs/cli-information-contract.md +42 -11
  71. package/docs/cli-information-contract.zh-CN.md +28 -7
  72. package/docs/managed-turn-and-session-runtime.md +47 -1
  73. package/docs/managed-turn-and-session-runtime.zh-CN.md +32 -0
  74. package/docs/release-workflow.md +80 -7
  75. package/docs/release-workflow.zh-CN.md +55 -4
  76. package/docs/storage-baseline.md +6 -1
  77. package/docs/storage-baseline.zh-CN.md +4 -1
  78. package/docs/testing/review-evaluation.md +148 -0
  79. package/docs/testing/review-evaluation.zh-CN.md +117 -0
  80. package/docs/testing/verification-levels.md +6 -2
  81. package/docs/testing/verification-levels.zh-CN.md +3 -0
  82. package/package.json +1 -1
  83. package/skills/yui-leader/VERSION +1 -1
  84. package/skills/yui-leader/references/execution.md +29 -0
  85. package/skills/yui-leader/references/replicated-execution.md +7 -0
  86. package/skills/yui-operator/SKILL.md +5 -1
  87. package/skills/yui-operator/VERSION +1 -1
  88. package/skills/yui-reviewer/SKILL.md +35 -0
  89. package/skills/yui-reviewer/VERSION +1 -1
  90. package/skills/yui-runtime/SKILL.md +53 -7
  91. package/skills/yui-runtime/VERSION +1 -1
  92. package/skills/yui-runtime/references/recovery.md +24 -5
@@ -0,0 +1,69 @@
1
+ import { join } from "node:path";
2
+ import Database from "../sqlite/driver.js";
3
+ import { hasManagedIdentity } from "../runtime/managedIdentity.js";
4
+ import { ManagedRuntimeDriftError, requireManagedGlobalCaller } from "../runtime/managedCaller.js";
5
+ import { sessionCallerEnvironment } from "../runtime/sessionCaller.js";
6
+ import { validateStoredRecord } from "./recordValidation.js";
7
+ import { inspectSqliteSchema, storageMinorUpgradePlan, validateSqliteSchemaObjects } from "./sqliteSchema.js";
8
+ import { CURRENT_DATABASE_FILENAME } from "./storageSchema.js";
9
+ /**
10
+ * Maintenance must authenticate before migration, without opening a current-only
11
+ * business Store. The declared 1.x migrations preserve these Role/Session
12
+ * envelopes and physical tables. Read only those records in one SQLite snapshot;
13
+ * this grants no old-format business reads, writes or implicit migration.
14
+ */
15
+ export function maintenanceCallerEnvironment(home, env, operatorOnly = true) {
16
+ if (!hasManagedIdentity(env))
17
+ return env;
18
+ const db = new Database(join(home, CURRENT_DATABASE_FILENAME), { readonly: true, fileMustExist: true });
19
+ try {
20
+ return db.transaction(() => {
21
+ const schema = inspectSqliteSchema(db);
22
+ if (storageMinorUpgradePlan(schema.currentVersion) === null) {
23
+ throw new ManagedRuntimeDriftError("Maintenance cannot authenticate an unsupported Home storage version.");
24
+ }
25
+ validateSqliteSchemaObjects(db);
26
+ const read = (table, where, params) => {
27
+ const row = db.prepare(`SELECT payload FROM ${table} WHERE ${where}`).get(...params);
28
+ if (!row)
29
+ return null;
30
+ const value = JSON.parse(row.payload);
31
+ validateStoredRecord(table, value);
32
+ return value;
33
+ };
34
+ const registry = {
35
+ getGlobalRole: (name) => read("global_roles", "name=?", [name]),
36
+ getGlobalRoleSessionSet: (name) => read("global_role_session_sets", "name=?", [name]),
37
+ getTaskRoleSessionSet: (task, role) => read("role_session_sets", "task_id=? AND role_name=?", [task, role]),
38
+ listRuntimeSessionCandidates: () => {
39
+ // Read canonical sets, not a derived index that could be stale. Only
40
+ // active bindings participate, exactly as in normal caller discovery.
41
+ const rows = db.prepare(`
42
+ SELECT 'global_role_session_sets' AS source, payload FROM global_role_session_sets
43
+ UNION ALL SELECT 'role_session_sets' AS source, payload FROM role_session_sets
44
+ `).all();
45
+ return rows.flatMap(row => {
46
+ const set = JSON.parse(row.payload);
47
+ validateStoredRecord(row.source, set);
48
+ const session = set.sessions[set.activeAgentId];
49
+ if (!session || session.status !== "active")
50
+ return [];
51
+ return [{ owner: set.owner, agentId: session.agentId, adapterId: session.adapterId,
52
+ nativeSessionId: session.nativeSessionId, sessionUpdatedAt: set.updatedAt }];
53
+ });
54
+ }
55
+ };
56
+ const current = sessionCallerEnvironment(registry, env);
57
+ if (current.YUI_SESSION_SCOPE !== "global" || current.YUI_ROLE !== "operator") {
58
+ if (!operatorOnly)
59
+ return current; // Doctor keeps normal scoped authorization below.
60
+ throw new ManagedRuntimeDriftError("Home maintenance requires the user or current Operator, not a Task or custom Role.");
61
+ }
62
+ requireManagedGlobalCaller(registry, current);
63
+ return current;
64
+ })();
65
+ }
66
+ finally {
67
+ db.close();
68
+ }
69
+ }
@@ -86,6 +86,16 @@ const MINOR_UPGRADES = Object.freeze([{
86
86
  // Additive handling receipts on canonical Messages and wakes. Never invent
87
87
  // historical processing or Provider acceptance; existing records stay intact.
88
88
  sql: ""
89
+ }, {
90
+ fromVersion: "1.7",
91
+ toVersion: "1.8",
92
+ name: "model-management-connection",
93
+ introducedIn: "next",
94
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
95
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
96
+ // Optional non-secret connection references. Existing Homes remain unconfigured;
97
+ // never infer local credentials or adopt an existing external service.
98
+ sql: ""
89
99
  }]);
90
100
  export function storageMinorUpgradePlan(from) {
91
101
  if (from === CURRENT_STORAGE_VERSION)
@@ -109,7 +119,7 @@ function objects(db) {
109
119
  WHERE name NOT GLOB 'sqlite_*' ORDER BY type,name`).all();
110
120
  }
111
121
  let expectedObjects;
112
- function validateSchemaObjects(db) {
122
+ export function validateSqliteSchemaObjects(db) {
113
123
  if (expectedObjects === undefined) {
114
124
  const reference = new Database(":memory:");
115
125
  try {
@@ -151,7 +161,7 @@ export function inspectSqliteSchema(db) {
151
161
  throw new SqliteSchemaError("baseline checksum does not match its version.");
152
162
  }
153
163
  if (version === CURRENT_STORAGE_VERSION)
154
- validateSchemaObjects(db);
164
+ validateSqliteSchemaObjects(db);
155
165
  return {
156
166
  currentVersion: version, currentChecksum: row.checksum,
157
167
  targetVersion: CURRENT_STORAGE_VERSION, targetChecksum: CURRENT_SCHEMA_CHECKSUM,
@@ -43,7 +43,7 @@ import { consumePendingBatch, mailboxTargetKey, validateWorkMailbox } from "../c
43
43
  import { enqueueDurableOperatorFact } from "../scheduler/operatorEvent.js";
44
44
  import { validateDurableJob, validDurableJobTransition } from "../job/durableJob.js";
45
45
  import { validateGlobalRoleMessage } from "../message/message.js";
46
- import { assertInboxReceiptPreserved } from "../message/inboxReceipt.js";
46
+ import { assertInboxOriginalPreserved, assertInboxReceiptPreserved } from "../message/inboxReceipt.js";
47
47
  import { validatePluginIntent, validatePluginIntentFailure } from "../plugins/pluginIntent.js";
48
48
  import { validatePluginValidation } from "../plugins/pluginPackage.js";
49
49
  import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeIdentity.js";
@@ -1027,8 +1027,8 @@ export class SqliteTaskStore {
1027
1027
  this.#mutate(() => {
1028
1028
  const existing = this.#db.prepare("SELECT payload FROM global_role_messages WHERE name = ? AND message_id = ?")
1029
1029
  .get(message.roleName, message.id);
1030
- assertInboxReceiptPreserved(existing === undefined ? undefined
1031
- : this.#parse(existing.payload).inbox, message.inbox);
1030
+ assertInboxOriginalPreserved(existing === undefined ? undefined
1031
+ : this.#parse(existing.payload), message);
1032
1032
  const seq = this.#globalMessageSequence(message.id);
1033
1033
  const result = this.#db.prepare(`UPDATE global_role_messages SET seq = ?, payload = ? WHERE name = ? AND message_id = ?`).run(seq, this.#recordJson("global_role_messages", message), message.roleName, message.id);
1034
1034
  if (result.changes === 0) {
@@ -2053,8 +2053,8 @@ export class SqliteTaskStore {
2053
2053
  this.#mutate(() => {
2054
2054
  const existing = this.#db.prepare("SELECT payload FROM messages WHERE task_id = ? AND message_id = ?")
2055
2055
  .get(taskId, message.id);
2056
- assertInboxReceiptPreserved(existing === undefined ? undefined
2057
- : this.#parse(existing.payload).inbox, message.inbox);
2056
+ assertInboxOriginalPreserved(existing === undefined ? undefined
2057
+ : this.#parse(existing.payload), message);
2058
2058
  const result = this.#db.prepare(`UPDATE messages SET payload = ? WHERE task_id = ? AND message_id = ?`).run(this.#recordJson("messages", message), taskId, message.id);
2059
2059
  if (result.changes !== 1) {
2060
2060
  throw new StorageRecordError(`Message does not exist: ${taskId}/${message.id}`);
@@ -2170,6 +2170,10 @@ export class SqliteTaskStore {
2170
2170
  this.#mutate(() => {
2171
2171
  const existing = this.#db.prepare("SELECT payload FROM task_wakes WHERE task_id = ? AND wake_id = ?")
2172
2172
  .get(taskId, wake.id);
2173
+ const previous = existing === undefined ? undefined : this.#parse(existing.payload);
2174
+ if (previous !== undefined && !isDeepStrictEqual(previous.refs, wake.refs)) {
2175
+ throw new StorageRecordError("A notification window's original revision references are immutable.");
2176
+ }
2173
2177
  assertInboxReceiptPreserved(existing === undefined ? undefined
2174
2178
  : this.#parse(existing.payload).inbox, wake.inbox);
2175
2179
  const seq = this.#idSequence(wake.id, "taskWake");
@@ -1,4 +1,4 @@
1
- export const CURRENT_STORAGE_VERSION = "1.7";
1
+ export const CURRENT_STORAGE_VERSION = "1.8";
2
2
  export const MIN_SUPPORTED_STORAGE_VERSION = "1.0";
3
3
  export const STORAGE_FORMAT = "yui-home";
4
4
  export function isStorageVersion(value) {
@@ -1,5 +1,6 @@
1
1
  import { lstatSync, mkdirSync, realpathSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
+ import { validateModelConnection } from "../models/modelManagementConfig.js";
3
4
  import { basename, dirname, join, resolve } from "node:path";
4
5
  import { isDeepStrictEqual } from "node:util";
5
6
  import { resolveTimeZone } from "../config/timeZone.js";
@@ -117,6 +118,7 @@ export function validateYuiConfig(config) {
117
118
  try {
118
119
  const optionalFields = [
119
120
  "defaultAgent",
121
+ "modelManagement",
120
122
  "defaultWorkspace",
121
123
  "timeZone",
122
124
  "currentTaskId",
@@ -146,6 +148,8 @@ export function validateYuiConfig(config) {
146
148
  }
147
149
  reconciliationIntervalMilliseconds(config.reconciliationIntervalSeconds);
148
150
  resolveTimeZone(config.timeZone);
151
+ if (config.modelManagement !== undefined)
152
+ validateModelConnection(config.modelManagement);
149
153
  if (config.review !== undefined)
150
154
  validateReviewConfig(config.review);
151
155
  resolveResourcesGcMode(config.resourcesGcMode);
@@ -7,7 +7,7 @@ export function pendingCompletionMessages(store, taskId) {
7
7
  const mailbox = store.getWorkMailbox({ kind: "role", taskId, roleName: "leader" });
8
8
  const refs = [...(mailbox?.pending?.refs ?? [])];
9
9
  for (const wake of store.listTaskWakes(taskId)) {
10
- if (wake.inbox !== undefined && wake.inbox.ack === undefined)
10
+ if (wake.inbox?.ack === undefined)
11
11
  refs.push(...(wake.refs ?? []));
12
12
  }
13
13
  const processing = mailbox?.processing;
@@ -41,8 +41,8 @@ export function projectCompletionReadiness(facts) {
41
41
  blockers.push({
42
42
  code: "pending-user-input",
43
43
  ref: ref("message", message.id),
44
- reason: `User message ${message.id} is still awaiting Leader delivery.`,
45
- fix: `finish the current native turn, read the next notification and message ${message.id}, then reconsider completion`
44
+ reason: `User message ${message.id} is still awaiting Leader delivery or explicit handling.`,
45
+ fix: `read message ${message.id} and its inbox window; claim and acknowledge after handling, or finish this turn if delivery is still pending`
46
46
  });
47
47
  }
48
48
  // A pending/running Task-final Review must be resumed or blocked first.
@@ -3,6 +3,7 @@
3
3
  // fonts and xterm from their packages; the shell links them in this order.
4
4
  import { readFileSync } from "node:fs";
5
5
  import { createRequire } from "node:module";
6
+ import { createHash } from "node:crypto";
6
7
  import { APP_SCRIPT, CLIENT_MODULES } from "./client/index.js";
7
8
  import { THEME_BOOTSTRAP_SCRIPT } from "./client/lib/theme.js";
8
9
  import { FONT_WOFF2_BASE64 } from "./fontData.js";
@@ -22,8 +23,33 @@ export const WEB_ASSETS = Object.freeze({
22
23
  "/assets/vendor/xterm.css": vendorAsset("@xterm/xterm/css/xterm.css", CSS)
23
24
  });
24
25
  export { DASHBOARD_HTML };
25
- export function findWebAsset(pathname) {
26
- return WEB_ASSETS[pathname] ?? null;
26
+ // One content revision covers the complete import graph: changing a dependency
27
+ // invalidates its importers too. Only these static assets may be cached.
28
+ export const WEB_ASSET_VERSION = createHash("sha256")
29
+ .update(JSON.stringify(WEB_ASSETS)).digest("hex").slice(0, 20);
30
+ function versionReferences(body) {
31
+ return body.replace(/\/assets\/[\w./-]+/g, path => WEB_ASSETS[path] ? `${path}?v=${WEB_ASSET_VERSION}` : path);
32
+ }
33
+ const VERSIONED_ASSETS = Object.fromEntries(Object.entries(WEB_ASSETS).map(([path, asset]) => [path, asset.encoding === "base64" ? asset :
34
+ { ...asset, body: versionReferences(asset.body) }]));
35
+ export function findWebAsset(pathname, version) {
36
+ if (version && version !== WEB_ASSET_VERSION)
37
+ return null;
38
+ return (version ? VERSIONED_ASSETS : WEB_ASSETS)[pathname] ?? null;
39
+ }
40
+ export function prepareWebDocument(html, entry) {
41
+ const modules = new Set();
42
+ function visit(path) {
43
+ if (modules.has(path) || !WEB_ASSETS[path])
44
+ return;
45
+ modules.add(path);
46
+ for (const match of WEB_ASSETS[path].body.matchAll(/^import (?:.*? from )?["'](\/assets\/[^"']+)["'];/gm)) {
47
+ visit(match[1]);
48
+ }
49
+ }
50
+ visit(entry);
51
+ const preload = [...modules].map(path => `<link rel="modulepreload" href="${path}?v=${WEB_ASSET_VERSION}">`).join("\n");
52
+ return versionReferences(html).replace("</head>", `${preload}\n</head>`);
27
53
  }
28
54
  function vendorAsset(specifier, contentType) {
29
55
  return {
@@ -0,0 +1,6 @@
1
+ export const MANAGE_SCRIPT = String.raw `
2
+ import "/assets/js/app/settings.js";
3
+ import "/assets/js/app/models.js";
4
+ import { startManagement } from "/assets/js/layout/manageShell.js";
5
+ startManagement();
6
+ `;
@@ -0,0 +1,20 @@
1
+ export const MODELS_SCRIPT = String.raw `
2
+ import { api } from "/assets/js/lib/api.js";
3
+ import { i18n, createManageShell, registerManagePage, navigateManage, readManageRoute } from "/assets/js/layout/manageShell.js";
4
+ import { mountModels } from "/assets/js/views/models.js";
5
+ const shell = createManageShell({ page: "models", t: i18n.t });
6
+ const initial = readManageRoute(new URL(location.href));
7
+ const page = mountModels(document.querySelector("#models-root"), {
8
+ t: i18n.t, locale: i18n.getLocale, api, route: initial,
9
+ navigate: next => navigateManage({ page: "models", ...next }),
10
+ onChange: flags => shell.setDirty({ providers: !!flags.provider, connection: !!flags.connection })
11
+ });
12
+ registerManagePage("models", { show: next => page.show(next) });
13
+ window.setInterval(() => page.updateFreshness(), 1000);
14
+ document.addEventListener("visibilitychange", () => { if (!document.hidden) page.updateFreshness(); });
15
+ i18n.subscribe(() => page.translate());
16
+ window.addEventListener("beforeunload", event => {
17
+ if (!page.dirty()) return;
18
+ event.preventDefault(); event.returnValue = "";
19
+ });
20
+ `;
@@ -1,15 +1,42 @@
1
1
  export const SETTINGS_SCRIPT = String.raw `
2
+ // Settings controller in the shared management document. Sections stay
3
+ // mounted across model/settings navigation. Leaving the document protects
4
+ // unsaved server groups; Skills guard their own bindings.
2
5
  import { h } from "/assets/js/lib/dom.js";
3
6
  import { requestJson, submitMutation } from "/assets/js/lib/api.js";
4
- import { createI18n } from "/assets/js/lib/i18n.js";
5
7
  import { createThemeController } from "/assets/js/lib/theme.js";
6
8
  import { clearPreference, readSessionAccessMode, writeSessionAccessMode } from "/assets/js/lib/prefs.js";
9
+ import { i18n, createManageShell, pageSections, registerManagePage, navigateManage } from "/assets/js/layout/manageShell.js";
7
10
  import { mountSkills } from "/assets/js/views/skills.js";
8
11
 
9
12
  const $ = selector => document.querySelector(selector);
10
- const i18n = createI18n($("#locale-select"));
11
13
  const t = i18n.t;
12
- mountSkills($("#skill-browser"), t);
14
+ const SECTIONS = pageSections("settings");
15
+ const dirtySections = { skills: false, server: false };
16
+ function readSection() {
17
+ const section = new URLSearchParams(window.location.search).get("section");
18
+ return SECTIONS.includes(section) ? section : SECTIONS[0];
19
+ }
20
+ let section = readSection();
21
+ const shell = createManageShell({ page: "settings", t, onNavigate: navigate });
22
+ function showSection(next) {
23
+ section = next;
24
+ document.querySelectorAll("#settings-root > .manage-section[data-section]").forEach(panel => { panel.hidden = panel.dataset.section !== next; });
25
+ shell.set(next);
26
+ }
27
+ function navigate(next) {
28
+ navigateManage({ page: "settings", section: next });
29
+ }
30
+ function markDirty(name, value) {
31
+ if (dirtySections[name] === value) return;
32
+ dirtySections[name] = value;
33
+ shell.setDirty(dirtySections);
34
+ }
35
+ registerManagePage("settings", { show: next => showSection(next.section) });
36
+ i18n.subscribe(() => shell.render());
37
+ showSection(section);
38
+ const skills = mountSkills($("#skill-browser"), t, { heading: false, onDirty: value => markDirty("skills", value) });
39
+ i18n.subscribe(() => skills?.translate());
13
40
  const theme = createThemeController($("#theme-options"), t);
14
41
  i18n.subscribe(() => theme.render());
15
42
  const access = $("#access-mode");
@@ -48,8 +75,8 @@ function inputValue(field, control) {
48
75
  }
49
76
 
50
77
  function editor(field, group, state) {
51
- const labelText = t("settings.field." + field.key, field.label);
52
- const label = h("label.field", null, h("span", null, labelText));
78
+ const fieldLabel = () => h("span", { "data-i18n": "settings.field." + field.key, "data-i18n-fallback": field.label }, t("settings.field." + field.key, field.label));
79
+ const label = h("label.field", null, fieldLabel());
53
80
  let control;
54
81
  if (field.kind === "boolean" || field.choices && field.choices.length) {
55
82
  const values = field.kind === "boolean" ? ["true", "false"] : field.choices;
@@ -74,6 +101,7 @@ function editor(field, group, state) {
74
101
  function mark() {
75
102
  control.disabled = reset.checked || unavailable;
76
103
  state.dirty = state.editors.some(e => e.changed());
104
+ markDirty("server", groups.some(g => g.dirty));
77
105
  state.save.disabled = !state.dirty || state.pending || state.capabilityPending || state.unknown;
78
106
  if (!state.unknown && !state.pending) state.receipt.textContent = state.dirty ? t("settings.unsaved") : "";
79
107
  }
@@ -102,7 +130,7 @@ function editor(field, group, state) {
102
130
  control.value = value;
103
131
  control.disabled = reset.checked || unavailable;
104
132
  listen();
105
- label.replaceChildren(h("span", null, labelText), control);
133
+ label.replaceChildren(fieldLabel(), control);
106
134
  choiceActions.replaceChildren();
107
135
  if (choices.length && options.allowCustom && !unavailable) {
108
136
  const custom = h("button.btn", { type: "button" }, t("settings.custom"));
@@ -110,7 +138,7 @@ function editor(field, group, state) {
110
138
  const value = control.value;
111
139
  control = h("input", { value });
112
140
  control.disabled = reset.checked;
113
- listen(); label.replaceChildren(h("span", null, labelText), control); choiceActions.replaceChildren();
141
+ listen(); label.replaceChildren(fieldLabel(), control); choiceActions.replaceChildren();
114
142
  control.focus();
115
143
  });
116
144
  choiceActions.append(custom);
@@ -153,9 +181,10 @@ function renderGroup(state, group) {
153
181
  const generation = state.renderGeneration = (state.renderGeneration || 0) + 1;
154
182
  state.group = group; state.editors = []; state.dirty = false; state.unknown = false;
155
183
  state.capabilityPending = false;
184
+ markDirty("server", groups.some(g => g.dirty));
156
185
  const form = h("form.settings-stack");
157
186
  const receipt = h("p.settings-receipt", { role: "status", "aria-live": "polite" });
158
- const save = h("button.btn.btn-primary", { type: "submit", disabled: true }, t("settings.save"));
187
+ const save = h("button.btn.btn-primary", { type: "submit", disabled: true, "data-i18n": "settings.save" }, t("settings.save"));
159
188
  state.save = save; state.receipt = receipt;
160
189
  form.append(h("p", null, group.notice));
161
190
  const isRole = group.id.startsWith("role/");
@@ -172,7 +201,7 @@ function renderGroup(state, group) {
172
201
  const acknowledge = h("input", { type: "checkbox" });
173
202
  if (group.id.startsWith("role/") || group.id.startsWith("agent/")) form.append(h("label.settings-reset", null,
174
203
  acknowledge, t("settings.acknowledge")));
175
- const compare = h("button.btn", { type: "button" }, t("settings.compare"));
204
+ const compare = h("button.btn", { type: "button", "data-i18n": "settings.compare" }, t("settings.compare"));
176
205
  compare.addEventListener("click", async () => {
177
206
  compare.disabled = true;
178
207
  try {
@@ -322,5 +351,8 @@ async function start(cursor) {
322
351
  } catch (error) { status.textContent = error.message; }
323
352
  finally { if (generation === searchGeneration) more.disabled = false; }
324
353
  }
354
+ i18n.subscribe(() => {
355
+ more.textContent = t("settings.more");
356
+ });
325
357
  void start();
326
358
  `;
@@ -3,6 +3,172 @@
3
3
  // domain vocabulary (statuses, owners, actions) keeps the exact product terms.
4
4
  // Placeholders such as {count} are filled by fill() in lib/format.
5
5
  export const EN = {
6
+ "models.subscriptionAuth": "Subscription",
7
+ "models.account": "Account",
8
+ "models.providerReadOnlyHelp": "Managed by its provider. Edit the configuration in the owning service.",
9
+ "models.readOnlyService": "Read-only connection. Configuration is managed by the running service.",
10
+ "models.window.5h": "5 hours",
11
+ "models.window.weekly": "Weekly",
12
+ "models.window.monthly": "Monthly",
13
+ "manage.nav": "Main navigation",
14
+ "manage.workbench": "Workbench",
15
+ "settings.section.preferences": "Preferences",
16
+ "settings.section.skills": "Skills",
17
+ "settings.section.server": "Agents & server",
18
+ "settings.hint.preferences": "Theme, language and workspace layout for this browser. Changes apply here immediately and are never sent to the server.",
19
+ "settings.hint.server": "Desired configuration for Yui, Roles, Agents and Profiles. Saving targets later launches; it is not proof that existing Sessions adopted it.",
20
+ "models.title": "Models",
21
+ "models.scope": "Manage the metabridge and CLIProxyAPI connection Yui reads. Saved configuration is not applied until metabridge is reloaded, and nothing here runs inference.",
22
+ "models.section.providers": "Access",
23
+ "models.section.catalog": "Model catalog",
24
+ "models.section.quota": "Quota",
25
+ "models.section.connection": "Model connection",
26
+ "models.openConnection": "Open model connection",
27
+ "models.connectionGroup.bridge": "metabridge",
28
+ "models.connectionGroup.cpa": "CLIProxyAPI (quota)",
29
+ "models.unsavedDraft": "Unsaved changes",
30
+ "models.applicationTitle": "Saving is not applying",
31
+ "models.column.provider": "Provider",
32
+ "models.column.baseUrl": "Base URL",
33
+ "models.column.prefix": "Prefix",
34
+ "models.column.route": "Route",
35
+ "models.column.state": "State",
36
+ "models.column.actions": "Actions",
37
+ "models.providerCount": "{shown} of {total} providers",
38
+ "models.noProviderMatches": "No provider matches these filters.",
39
+ "models.searchProviders": "Search name, ID, URL or prefix",
40
+ "models.searchCatalog": "Search loaded model IDs",
41
+ "models.filter.route": "Filter by route",
42
+ "models.filter.state": "Filter by state",
43
+ "models.filter.source": "Filter by source",
44
+ "models.filter.all": "All",
45
+ "models.filter.enabled": "Enabled",
46
+ "models.filter.disabled": "Disabled",
47
+ "models.filter.readonly": "Read-only",
48
+ "models.catalogMatches": "{count} match",
49
+ "models.noCatalogMatches": "No loaded model matches. Load more pages to search further.",
50
+ "models.resetUnknown": "Reset time unknown",
51
+ "models.accountIdHelp": "Identifier derived by Yui from the upstream account; not an account name.",
52
+ "models.loading": "Loading…",
53
+ "models.loadFailed": "Could not read model configuration:",
54
+ "models.retry": "Retry",
55
+ "models.unavailable": "Model configuration is unavailable until it can be read.",
56
+ "models.unknown": "unknown",
57
+ "models.connected": "Connection configured",
58
+ "models.disconnected": "Not connected",
59
+ "models.application.unverified": "Application unverified",
60
+ "models.applicationHelp": "Saving here changes desired configuration only. Reload metabridge with its existing deployment flow to apply it; this page never restarts services, rewrites Codex configuration, switches Sessions or validates inference.",
61
+ "models.configurationError": "Bridge configuration problem:",
62
+ "models.unknownBlocked": "An earlier save has an unknown outcome. It will not be resent. Read current facts before making another change.",
63
+ "models.readFacts": "Read current facts",
64
+ "models.reconciledChanged": "Current facts read; configuration has changed since the uncertain save. Review it before saving again.",
65
+ "models.reconciledUnchanged": "Current facts read; no change is visible yet. The earlier save may still be unresolved — review before saving again.",
66
+ "models.agentSettings": "Agent and model selection stay in Settings; models are never imported automatically.",
67
+ "models.openSettings": "Open Settings",
68
+ "models.connection": "Connection",
69
+ "models.connectionHelp": "Paths name local files; secrets are read by the server and never shown here. Disconnecting stops nothing and deletes no credentials.",
70
+ "models.notConnected": "No connection is configured. Enter at least the metabridge URL.",
71
+ "models.connection.bridgeUrl": "metabridge URL (required)",
72
+ "models.connection.bridgeUrlHelp": "Base URL of the metabridge management endpoint.",
73
+ "models.connection.bridgeConfigFile": "metabridge configuration file",
74
+ "models.connection.bridgeConfigFileHelp": "Optional. Enables provider management through this local file.",
75
+ "models.connection.cpaUrl": "CLIProxyAPI URL",
76
+ "models.connection.cpaUrlHelp": "Optional. Needed with the management key file for quota refresh.",
77
+ "models.connection.managementKeyFile": "CPA management key file",
78
+ "models.connection.managementKeyFileHelp": "Path to a private file holding the management key. The key itself is never returned.",
79
+ "models.connection.catalogKeyFile": "Catalog key file",
80
+ "models.connection.catalogKeyFileHelp": "Optional. Independent from the management key.",
81
+ "models.saveConnection": "Save connection",
82
+ "models.disconnect": "Disconnect",
83
+ "models.disconnectConfirm": "Disconnect Yui from metabridge? Services keep running and credentials are not deleted.",
84
+ "models.discard": "Discard changes",
85
+ "models.bridgeUrlRequired": "The metabridge URL is required.",
86
+ "models.providers": "Providers",
87
+ "models.providersHelp": "Providers configured in metabridge. Official, subscription and unprefixed shared routes are read-only.",
88
+ "models.noProviders": "No providers are configured.",
89
+ "models.addProvider": "Add API-key provider",
90
+ "models.configFileRequired": "Set the metabridge configuration file in the connection to manage providers.",
91
+ "models.newProvider": "New provider",
92
+ "models.editProvider": "Edit",
93
+ "models.removeProvider": "Remove",
94
+ "models.removeConfirm": "Remove this provider from metabridge? Its file and credential are retained.",
95
+ "models.discardProvider": "Discard the provider being edited?",
96
+ "models.readOnly": "Read-only",
97
+ "models.noPrefix": "no prefix",
98
+ "models.enabled": "Enabled",
99
+ "models.disabled": "Disabled",
100
+ "models.credentialStored": "Credential stored",
101
+ "models.credentialMissing": "No credential",
102
+ "models.via.direct": "Direct",
103
+ "models.via.cpa": "Through CPA",
104
+ "models.via.unknown": "Unknown route",
105
+ "models.provider.name": "Name",
106
+ "models.provider.via": "Route",
107
+ "models.provider.viaDirectHelp": "metabridge calls the provider directly with this provider's API key.",
108
+ "models.provider.viaCpaHelp": "metabridge calls CLIProxyAPI. Use a CPA client inference key — not an upstream account key or the management key.",
109
+ "models.provider.baseUrl": "Base URL",
110
+ "models.provider.prefix": "Model prefix",
111
+ "models.provider.prefixHelp": "Explicit model namespace ending in /, e.g. mine/ for mine/model: 1–64 letters, digits, _ or -, then /.",
112
+ "models.prefixInvalid": "Prefix must be 1–64 letters, digits, _ or - followed by /, e.g. mine/.",
113
+ "models.provider.enabled": "Enabled",
114
+ "models.provider.apiKey": "API key",
115
+ "models.provider.apiKeyCpa": "CPA client inference key",
116
+ "models.provider.apiKeyNewHelp": "Required. Sent once on save and cleared; it is never shown again.",
117
+ "models.provider.apiKeyKeepHelp": "Leave empty to keep the stored credential; a new value replaces it.",
118
+ "models.provider.apiKeyMissingHelp": "No credential is stored. Enter one to set it.",
119
+ "models.apiKeyKeep": "Unchanged",
120
+ "models.createProvider": "Create provider",
121
+ "models.saveProvider": "Save provider",
122
+ "models.cancel": "Cancel",
123
+ "models.providerNewRequired": "Name, base URL, prefix and API key are required for a new provider.",
124
+ "models.providerRequired": "Name, base URL and a direct or CPA route are required.",
125
+ "models.saving": "Saving…",
126
+ "models.saved": "Saved. Not applied until metabridge reloads.",
127
+ "models.rejected": "Not saved; your draft is kept. If the configuration changed, read current facts and save again.",
128
+ "models.rereadChanged": "Current facts read; configuration has changed. Your draft is kept — review it against the new facts before saving.",
129
+ "models.rereadUnchanged": "Current facts read; configuration is unchanged. Your draft is kept.",
130
+ "models.unknownWrite": "Save outcome unknown; it will not be resent.",
131
+ "models.catalog": "Model catalog",
132
+ "models.catalogHelp": "Cached metabridge catalog. Source comes from the model ID namespace; it is not proof of account attribution.",
133
+ "models.catalogFailed": "Catalog read failed:",
134
+ "models.catalogNotQueried": "The catalog has not been queried yet. Refresh to read it from metabridge.",
135
+ "models.catalogEmpty": "The observation contains no models.",
136
+ "models.catalogItemsUnknown": "No models were observed; the catalog is unknown, not empty.",
137
+ "models.catalogCount": "Showing {shown} of {total}",
138
+ "models.catalogChanged": "The catalog changed while paging; showing the newest first page.",
139
+ "models.moreCatalog": "Load more models",
140
+ "models.refreshCatalog": "Refresh catalog",
141
+ "models.quota": "Subscription quota",
142
+ "models.quotaHelp": "Each source is read separately. Refresh is explicit and may contact the upstream billing service through the CPA extension; it never renews credentials, resets quota or runs inference.",
143
+ "models.source": "Source",
144
+ "models.source.factory": "Factory",
145
+ "models.source.command-code": "Command Code",
146
+ "models.refreshQuota": "Refresh quota",
147
+ "models.quotaNeedsCpa": "Quota refresh needs the CPA URL and management key file.",
148
+ "models.quotaUnavailable": "The quota route is unavailable, so quota is unknown. This does not prove the extension is uninstalled.",
149
+ "models.quotaFailed": "Quota is unknown:",
150
+ "models.quotaNotQueried": "Quota has not been queried for this source.",
151
+ "models.quotaUnsupported": "This source does not report quota; it is unknown, not zero.",
152
+ "models.quotaEmpty": "The observation contains no accounts.",
153
+ "models.quotaItemsUnknown": "No accounts were observed; quota is unknown, not empty.",
154
+ "models.noWindows": "No quota windows reported.",
155
+ "models.remaining": "Remaining",
156
+ "models.used": "Used / cap",
157
+ "models.resets": "Resets",
158
+ "models.balance": "Balance",
159
+ "models.balance.extra-usage-usd": "Extra usage balance (USD)",
160
+ "models.balance.monthly-credits": "Monthly credits",
161
+ "models.balance.purchased-credits": "Purchased credits",
162
+ "models.balance.free-credits": "Free credits",
163
+ "models.periodEnd": "Period ends",
164
+ "models.status.not-queried": "Not queried",
165
+ "models.status.success": "Observed",
166
+ "models.status.partial": "Partial",
167
+ "models.status.failed": "Failed",
168
+ "models.status.unsupported": "Unsupported",
169
+ "models.stale": "Stale",
170
+ "models.attempted": "Attempted",
171
+ "models.observed": "observed",
6
172
  "preview.title": "Preview",
7
173
  "preview.refresh": "Refresh",
8
174
  "preview.loading": "Checking registered services…",
@@ -76,7 +242,6 @@ export const EN = {
76
242
  "workbench.reviewExecution": "Review execution (not acceptance):",
77
243
  "workbench.noEvidence": "No recorded review or integration checks yet. See the Leader summary and original results.",
78
244
  "workbench.openEvidence": "View results, reviews, validation and files",
79
- "settings.scope": "Browser preferences apply here immediately. Server settings are desired configuration, not proof that existing Sessions adopted them.",
80
245
  "settings.browser": "This browser",
81
246
  "settings.more": "Load more groups",
82
247
  "settings.groups": "configuration groups",
@@ -606,7 +771,7 @@ export const EN = {
606
771
  "conversation.linkedOmitted": "More linked inputs are available in the source records.",
607
772
  "conversation.stop": "Stop this Turn",
608
773
  "conversation.receipt": "Check original receipt",
609
- "conversation.older": "Older page",
774
+ "conversation.older": "Load earlier messages",
610
775
  "conversation.latest": "Latest / reconnect",
611
776
  "conversation.current": "Current Session",
612
777
  "conversation.previous": "Previous Session",
@@ -622,6 +787,19 @@ export const EN = {
622
787
  "conversation.empty": "No public items on this page. Older pages may contain messages.",
623
788
  "conversation.liveUnavailable": "Live reply observation unavailable; native history only",
624
789
  "conversation.truncated": "Long item excerpt; open native access for the full item.",
790
+ "conversation.placeholder": "Message this Session…",
791
+ "conversation.more": "More",
792
+ "conversation.copy": "Copy",
793
+ "conversation.copied": "Copied",
794
+ "conversation.jump": "Jump to latest",
795
+ "conversation.selectModel": "Select model",
796
+ "conversation.worked": "Worked",
797
+ "conversation.activity.commandExecution": "Ran",
798
+ "conversation.activity.fileChange": "Edited files",
799
+ "conversation.activity.mcpToolCall": "Used tool",
800
+ "conversation.activity.dynamicToolCall": "Used tool",
801
+ "conversation.activity.webSearch": "Searched the web",
802
+ "conversation.activity.collabAgentToolCall": "Asked another agent",
625
803
  "dock.swap": "Swap with task details",
626
804
  "dock.hide": "Hide session panel",
627
805
  "dock.resize": "Resize session panel",