@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.
- package/dist/cli/commandCatalog.js +8 -7
- package/dist/cli/updateCommand.js +3 -0
- package/dist/cli/updatePorts.js +20 -0
- package/dist/commands/configCommands.js +1 -0
- package/dist/commands/globalRoleCommands.js +42 -35
- package/dist/commands/inboxCommands.js +60 -40
- package/dist/commands/taskCommands.js +7 -3
- package/dist/commands/taskRoleRuntimeStatus.js +2 -0
- package/dist/context/runContextPack.js +48 -3
- package/dist/context/taskContext.js +36 -12
- package/dist/controlPlaneCli.js +64 -17
- package/dist/controller/controller.js +5 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +9 -4
- package/dist/controller/globalInputDelivery.js +17 -5
- package/dist/controller/globalRuntimeAttention.js +135 -5
- package/dist/controller/runtime.js +4 -0
- package/dist/controller/structuredProviderObservation.js +51 -15
- package/dist/controller/taskHostTerminalRecovery.js +33 -0
- package/dist/executor/fileRoleLaunchPlanner.js +4 -9
- package/dist/message/inboxReceipt.js +30 -0
- package/dist/message/taskInboxOwnership.js +25 -3
- package/dist/models/modelManagement.js +469 -0
- package/dist/models/modelManagementConfig.js +46 -0
- package/dist/models/modelManagementFiles.js +90 -0
- package/dist/models/modelManagementHttp.js +90 -0
- package/dist/models/modelManagementTypes.js +1 -0
- package/dist/output/boundedRead.js +43 -14
- package/dist/runtime/agentEndpointIdentity.js +8 -4
- package/dist/runtime/agentEndpointOwnership.js +5 -6
- package/dist/runtime/agentHost.js +62 -10
- package/dist/runtime/agentHostImplementation.js +18 -0
- package/dist/runtime/managedCaller.js +25 -1
- package/dist/runtime/managedIdentity.js +1 -1
- package/dist/runtime/providerRetry.js +19 -1
- package/dist/runtime/sessionCaller.js +53 -0
- package/dist/storage/contextRecords.js +28 -0
- package/dist/storage/maintenanceCaller.js +69 -0
- package/dist/storage/sqliteSchema.js +12 -2
- package/dist/storage/sqliteStore.js +9 -5
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/taskStore.js +4 -0
- package/dist/task/completionReadiness.js +3 -3
- package/dist/web/assets/assetManifest.js +28 -2
- package/dist/web/assets/client/app/manage.js +6 -0
- package/dist/web/assets/client/app/models.js +20 -0
- package/dist/web/assets/client/app/settings.js +41 -9
- package/dist/web/assets/client/i18n/en.js +180 -2
- package/dist/web/assets/client/i18n/zh-CN.js +178 -2
- package/dist/web/assets/client/index.js +8 -0
- package/dist/web/assets/client/layout/manageShell.js +94 -0
- package/dist/web/assets/client/lib/api.js +13 -1
- package/dist/web/assets/client/lib/i18n.js +5 -3
- package/dist/web/assets/client/views/dock/conversation.js +231 -80
- package/dist/web/assets/client/views/models.js +777 -0
- package/dist/web/assets/client/views/skills.js +22 -15
- package/dist/web/assets/shared/icons.js +2 -0
- package/dist/web/assets/shared/manageNav.js +13 -0
- package/dist/web/assets/shell/manage.js +54 -0
- package/dist/web/assets/shell/models.js +7 -0
- package/dist/web/assets/shell/settings.js +1 -35
- package/dist/web/assets/shell/settingsContent.js +30 -0
- package/dist/web/assets/shell/sidebar.js +1 -0
- package/dist/web/assets/styles/index.js +6 -0
- package/dist/web/assets/styles/views/conversation.js +133 -0
- package/dist/web/assets/styles/views/manage.js +71 -0
- package/dist/web/assets/styles/views/models.js +129 -0
- package/dist/web/assets/styles/views/settings.js +28 -14
- package/dist/web/assets/styles/views/sidebar.js +1 -0
- package/dist/web/webServer.js +59 -9
- package/docs/cli-information-contract.md +42 -11
- package/docs/cli-information-contract.zh-CN.md +28 -7
- package/docs/managed-turn-and-session-runtime.md +47 -1
- package/docs/managed-turn-and-session-runtime.zh-CN.md +32 -0
- package/docs/release-workflow.md +80 -7
- package/docs/release-workflow.zh-CN.md +55 -4
- package/docs/storage-baseline.md +6 -1
- package/docs/storage-baseline.zh-CN.md +4 -1
- package/docs/testing/review-evaluation.md +148 -0
- package/docs/testing/review-evaluation.zh-CN.md +117 -0
- package/docs/testing/verification-levels.md +6 -2
- package/docs/testing/verification-levels.zh-CN.md +3 -0
- package/package.json +1 -1
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/execution.md +29 -0
- package/skills/yui-leader/references/replicated-execution.md +7 -0
- package/skills/yui-operator/SKILL.md +5 -1
- package/skills/yui-operator/VERSION +1 -1
- package/skills/yui-reviewer/SKILL.md +35 -0
- package/skills/yui-reviewer/VERSION +1 -1
- package/skills/yui-runtime/SKILL.md +53 -7
- package/skills/yui-runtime/VERSION +1 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
1031
|
-
: this.#parse(existing.payload)
|
|
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
|
-
|
|
2057
|
-
: this.#parse(existing.payload)
|
|
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,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
|
|
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: `
|
|
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
|
-
|
|
26
|
-
|
|
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,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
|
-
|
|
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
|
|
52
|
-
const label = h("label.field", null,
|
|
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(
|
|
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(
|
|
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": "
|
|
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",
|