@songsid/agend 2.3.2-beta.13 → 2.3.2-beta.15
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/backend/codex-session-lookup.d.ts +15 -0
- package/dist/backend/codex-session-lookup.js +58 -0
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.js +15 -4
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-classic-archive.d.ts +5 -0
- package/dist/backend/kiro-classic-archive.js +187 -9
- package/dist/backend/kiro-classic-archive.js.map +1 -1
- package/dist/backend/kiro-classic-links.d.ts +24 -0
- package/dist/backend/kiro-classic-links.js +135 -0
- package/dist/backend/kiro-classic-links.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +20 -3
- package/dist/backend/kiro-identity.js +26 -39
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro-reconcile-store.d.ts +27 -0
- package/dist/backend/kiro-reconcile-store.js +344 -0
- package/dist/backend/kiro-reconcile-store.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.js +17 -3
- package/dist/backend/kiro-v2-store-worker.js.map +1 -1
- package/dist/backend/kiro-v2-store.d.ts +23 -1
- package/dist/backend/kiro-v2-store.js +22 -12
- package/dist/backend/kiro-v2-store.js.map +1 -1
- package/dist/backend/kiro.d.ts +4 -0
- package/dist/backend/kiro.js +25 -3
- package/dist/backend/kiro.js.map +1 -1
- package/dist/channel/mcp-tools.js +3 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +17 -4
- package/dist/cli.js.map +1 -1
- package/dist/fleet-manager.d.ts +9 -1
- package/dist/fleet-manager.js +100 -11
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-instance.d.ts +24 -0
- package/dist/general-instance.js +53 -0
- package/dist/general-instance.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +2 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +2 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +6 -4
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +6 -4
- package/dist/kiro-migrate.d.ts +4 -3
- package/dist/kiro-migrate.js +13 -8
- package/dist/kiro-migrate.js.map +1 -1
- package/dist/kiro-reconcile.d.ts +21 -0
- package/dist/kiro-reconcile.js +80 -0
- package/dist/kiro-reconcile.js.map +1 -0
- package/dist/kiro-tui-forensic.d.ts +7 -0
- package/dist/kiro-tui-forensic.js +17 -3
- package/dist/kiro-tui-forensic.js.map +1 -1
- package/dist/locale.js +24 -0
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -1
- package/dist/log-service.js.map +1 -1
- package/dist/office-share-page.d.ts +3 -1
- package/dist/office-share-page.js +7 -2
- package/dist/office-share-page.js.map +1 -1
- package/dist/office-share-service.d.ts +4 -3
- package/dist/office-share-service.js +6 -6
- package/dist/office-share-service.js.map +1 -1
- package/dist/office-share.d.ts +17 -6
- package/dist/office-share.js +28 -11
- package/dist/office-share.js.map +1 -1
- package/dist/outbound-handlers.js +20 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +6 -0
- package/dist/outbound-schemas.js +6 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/quickstart-api.d.ts +25 -1
- package/dist/quickstart-api.js +53 -6
- package/dist/quickstart-api.js.map +1 -1
- package/dist/service-installer.d.ts +13 -0
- package/dist/service-installer.js +15 -0
- package/dist/service-installer.js.map +1 -1
- package/dist/service-owner.d.ts +25 -0
- package/dist/service-owner.js +43 -2
- package/dist/service-owner.js.map +1 -1
- package/dist/setup-form.js +17 -3
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-i18n.js +2 -0
- package/dist/setup-i18n.js.map +1 -1
- package/dist/tool-permissions.js +1 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/ui/fleet-office-layout.js +14 -6
- package/dist/ui/fleet-office-pack.d.ts +4 -0
- package/dist/ui/fleet-office-pack.js +15 -5
- package/dist/ui/fleet-office-store.js +18 -3
- package/dist/ui/fleet-office-view.js +8 -0
- package/dist/ui/fleet-office-zones.js +10 -2
- package/dist/ui/fleet-office.js +36 -4
- package/dist/ui/office-share-dialog.js +31 -7
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/settings-strings.js +2 -0
- package/dist/ui/settings-wizard.js +15 -4
- package/dist/ui/shared/app.css +6 -0
- package/dist/ui/shared/setup-host-text.js +2 -0
- package/dist/web-api.js +74 -4
- package/dist/web-api.js.map +1 -1
- package/dist/web-office-pack-tools.d.ts +10 -1
- package/dist/web-office-pack-tools.js +36 -5
- package/dist/web-office-pack-tools.js.map +1 -1
- package/dist/web-office-skins.js +6 -0
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-office-store.d.ts +22 -3
- package/dist/web-office-store.js +44 -8
- package/dist/web-office-store.js.map +1 -1
- package/package.json +1 -1
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
// the org chart's states (fleet-org.js ORG_STATES) → an area; a
|
|
36
36
|
// state it does not name goes to "default", else the first area
|
|
37
37
|
// "door": [640, 700], optional: where someone new walks in from (default: bottom middle)
|
|
38
|
+
// "mailDrop": [640, 420], optional (#2736): where a letter from another office lands (more
|
|
39
|
+
// waiting step GAP px right of it, past any seat); default: the
|
|
40
|
+
// waypoint nearest the middle off every seat (fleet-office-zones.js
|
|
41
|
+
// dropPoint)
|
|
38
42
|
// "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …],
|
|
39
43
|
// optional: the walkable graph (else people walk straight)
|
|
40
44
|
// "straight": true, optional (#2634): every walk goes across, then up or down — each
|
|
@@ -120,7 +124,7 @@
|
|
|
120
124
|
// "seatFor": { "desks": { "0": { "for": ["atlas"], "lend": false } } },
|
|
121
125
|
// a seat's reservation by area and slot (#2691), in place of the
|
|
122
126
|
// pack's own for that seat; "for": [] is none (lends nothing)
|
|
123
|
-
// "door": [640, 700],
|
|
127
|
+
// "door": [640, 700], "mailDrop": [640, 420], (#2736: kept beside the offices on disk, as furnitureNight)
|
|
124
128
|
// "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …] }
|
|
125
129
|
// the walkable graph, replaced whole (edges need waypoints)
|
|
126
130
|
//
|
|
@@ -246,7 +250,7 @@ function states(v, at) {
|
|
|
246
250
|
export function checkSkinPack(x, { kept = false } = {}) {
|
|
247
251
|
try {
|
|
248
252
|
obj(x, "pack");
|
|
249
|
-
keys(x, ["format", "id", "scale", "background", "foreground", "matte", "areas", "stateZones", "door", "waypoints", "edges", "straight", "furniture", "characters", "byInstance", "props", "bubbles", "room", "desk"], "pack");
|
|
253
|
+
keys(x, ["format", "id", "scale", "background", "foreground", "matte", "areas", "stateZones", "door", "mailDrop", "waypoints", "edges", "straight", "furniture", "characters", "byInstance", "props", "bubbles", "room", "desk"], "pack");
|
|
250
254
|
if (x.format !== 1) throw new Refused("format", "1");
|
|
251
255
|
if (typeof x.id !== "string" || !ID.test(x.id)) throw new Refused("id", "1–40 letters, digits, _ or -");
|
|
252
256
|
if (BUILT_IN.has(x.id) || (!kept && RESERVED.has(x.id))) throw new Refused("id", "a built-in skin's id");
|
|
@@ -349,7 +353,8 @@ export function checkSkinPack(x, { kept = false } = {}) {
|
|
|
349
353
|
const opt = (v, at) => (v === undefined ? undefined : src(v, at));
|
|
350
354
|
return { ok: true, pack: {
|
|
351
355
|
format: 1, id: x.id, scale, background: opt(x.background, "background"), foreground: opt(x.foreground, "foreground"), matte: x.matte === undefined ? undefined : x.matte.toLowerCase(), areas, stateZones,
|
|
352
|
-
door: x.door === undefined ? { x: LOGICAL.w / 2, y: LOGICAL.h - 10 } : point(x.door, "door"),
|
|
356
|
+
door: x.door === undefined ? { x: LOGICAL.w / 2, y: LOGICAL.h - 10 } : point(x.door, "door"),
|
|
357
|
+
mailDrop: x.mailDrop === undefined ? null : point(x.mailDrop, "mailDrop"), waypoints, edges, straight: x.straight === true, furniture, characters, byInstance,
|
|
353
358
|
props: x.props === undefined ? {} : strips(x.props, PROPS, "props", scale), bubbles,
|
|
354
359
|
room: { floor: opt(room.floor, "room.floor"), wall: opt(room.wall, "room.wall") }, desk: { back: opt(desk.back, "desk.back"), front: opt(desk.front, "desk.front"), lit: opt(desk.lit, "desk.lit") },
|
|
355
360
|
} };
|
|
@@ -392,7 +397,7 @@ const reserve = (s, r) => { const { for: _f, lend: _l, ...p } = s; return r && r
|
|
|
392
397
|
export function checkLayoutOverrides(x) {
|
|
393
398
|
try {
|
|
394
399
|
obj(x, "overrides");
|
|
395
|
-
keys(x, ["pack", "seats", "furniture", "furnitureNight", "seatFor", "door", "waypoints", "edges"], "overrides");
|
|
400
|
+
keys(x, ["pack", "seats", "furniture", "furnitureNight", "seatFor", "door", "mailDrop", "waypoints", "edges"], "overrides");
|
|
396
401
|
if (typeof x.pack !== "string" || !ID.test(x.pack)) throw new Refused("overrides.pack", "1–40 letters, digits, _ or -");
|
|
397
402
|
const out = { pack: x.pack };
|
|
398
403
|
if (x.seats !== undefined) {
|
|
@@ -448,6 +453,7 @@ export function checkLayoutOverrides(x) {
|
|
|
448
453
|
}
|
|
449
454
|
}
|
|
450
455
|
if (x.door !== undefined) { const q = point(x.door, "overrides.door"); out.door = [q.x, q.y]; }
|
|
456
|
+
if (x.mailDrop !== undefined) { const q = point(x.mailDrop, "overrides.mailDrop"); out.mailDrop = [q.x, q.y]; }
|
|
451
457
|
if (x.edges !== undefined && x.waypoints === undefined) throw new Refused("overrides.edges", "only with waypoints");
|
|
452
458
|
if (x.waypoints !== undefined) {
|
|
453
459
|
const ids = new Set();
|
|
@@ -508,8 +514,9 @@ export function applyOverrides(pack, ov) {
|
|
|
508
514
|
return { ...f, ...(q ? { x: q[0], y: q[1] } : {}), ...(n ? { night: { ...f.night, x: n[0], y: n[1] } } : {}) };
|
|
509
515
|
});
|
|
510
516
|
const door = ov.door ? { x: ov.door[0], y: ov.door[1] } : pack.door;
|
|
517
|
+
const mailDrop = ov.mailDrop ? { x: ov.mailDrop[0], y: ov.mailDrop[1] } : pack.mailDrop ?? null;
|
|
511
518
|
const graph = ov.waypoints ? { waypoints: ov.waypoints.map((w) => ({ ...w })), edges: (ov.edges || []).map((e) => [e[0], e[1]]) } : { waypoints: pack.waypoints, edges: pack.edges };
|
|
512
|
-
return { ok: true, pack: { ...pack, areas, furniture, door, ...graph } };
|
|
519
|
+
return { ok: true, pack: { ...pack, areas, furniture, door, mailDrop, ...graph } };
|
|
513
520
|
} catch (err) {
|
|
514
521
|
if (err instanceof Refused) return { ok: false, error: err.message };
|
|
515
522
|
throw err;
|
|
@@ -575,6 +582,9 @@ export function overridesBetween(pack, edited) {
|
|
|
575
582
|
if (o.night.x !== f.night.x || o.night.y !== f.night.y) (ov.furnitureNight ||= {})[f.id] = [f.night.x, f.night.y];
|
|
576
583
|
}
|
|
577
584
|
if (pack.door.x !== edited.door.x || pack.door.y !== edited.door.y) ov.door = [edited.door.x, edited.door.y];
|
|
585
|
+
// #2736: a mail drop placed (the pack has none) or moved from the pack's.
|
|
586
|
+
const md = pack.mailDrop ?? null, ed = edited.mailDrop ?? null;
|
|
587
|
+
if (ed && (!md || md.x !== ed.x || md.y !== ed.y)) ov.mailDrop = [ed.x, ed.y];
|
|
578
588
|
if (!same(pack.waypoints, edited.waypoints) || !same(pack.edges, edited.edges)) {
|
|
579
589
|
ov.waypoints = edited.waypoints.map((w) => ({ id: w.id, x: w.x, y: w.y }));
|
|
580
590
|
ov.edges = edited.edges.map((e) => [e[0], e[1]]);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// The Office view's data, behind one small seam (fleet-office.js uses only these calls):
|
|
2
2
|
// - the offices people made: the host's (GET / PUT /ui/offices, src/web-office-store.ts — one list for every viewer);
|
|
3
3
|
// what comes back is checked again here (fleet-office-model.js officesOk), never trusted;
|
|
4
|
+
// - #2716: the whole-fleet office's style, kept on the host for everyone (GET /ui/offices's autoSkin; PUT
|
|
5
|
+
// /ui/office-style, an admin's) — this browser's own pick (skin(): an admin's copy of it, anyone else's own choice);
|
|
4
6
|
// - this browser's own choices (localStorage; blocked storage: this page only, as the canvas's Canvas/List): which
|
|
5
|
-
// office is shown, the auto office's member filter
|
|
7
|
+
// office is shown, the auto office's member filter; #2540: the view (zoom and centre) and the
|
|
6
8
|
// fit mode (Fit or Fill) per office; #2554: whether All offices shows the lobby tile.
|
|
7
9
|
// - the skin packs an admin uploaded (#2513: GET /ui/office-skins, POST to upload one, DELETE to remove one;
|
|
8
10
|
// src/web-office-skins.ts). The office editor and the layout editor call saveOffices() with the whole list and the rev
|
|
@@ -65,15 +67,26 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
65
67
|
const write = (k, v) => { try { const s = st(); if (s) s.setItem(k, v); } catch { /* this page only */ } };
|
|
66
68
|
const headers = { "Content-Type": "application/json" };
|
|
67
69
|
return {
|
|
68
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* { rev, offices, autoSkin } from the host (#2716 autoSkin: the whole-fleet office's style, or null: none kept); null
|
|
72
|
+
* when it could not be read (no offices shown but the auto one).
|
|
73
|
+
*/
|
|
69
74
|
async offices() {
|
|
70
75
|
try {
|
|
71
76
|
const r = await get("/ui/offices", { method: "GET", headers });
|
|
72
77
|
if (!r || !r.ok) return null;
|
|
73
78
|
const d = await r.json();
|
|
74
|
-
return { rev: Number.isSafeInteger(d && d.rev) ? d.rev : 0, offices: officesOk(d && d.offices) };
|
|
79
|
+
return { rev: Number.isSafeInteger(d && d.rev) ? d.rev : 0, offices: officesOk(d && d.offices), autoSkin: d && typeof d.autoSkin === "string" && PACK_ID.test(d.autoSkin) ? d.autoSkin : null };
|
|
75
80
|
} catch { return null; }
|
|
76
81
|
},
|
|
82
|
+
/** #2716: the whole-fleet office's style for everyone (an admin session): { ok, autoSkin } or { ok: false, status, error }. */
|
|
83
|
+
async saveAutoSkin(skin) {
|
|
84
|
+
try {
|
|
85
|
+
const r = await get("/ui/office-style", { method: "PUT", headers, body: JSON.stringify({ skin }) });
|
|
86
|
+
const d = await r.json().catch(() => ({}));
|
|
87
|
+
return r.ok ? { ok: true, autoSkin: typeof d.autoSkin === "string" ? d.autoSkin : null } : { ok: false, status: r.status, error: d.error || "office_style_failed" };
|
|
88
|
+
} catch { return { ok: false, status: 0, error: "office_style_failed" }; }
|
|
89
|
+
},
|
|
77
90
|
/**
|
|
78
91
|
* Replace the host's list (an admin session; `rev`: the one read — required: the host refuses a save without it,
|
|
79
92
|
* so two editors never overwrite each other unseen). { ok, rev, offices } or { ok: false, status, error }.
|
|
@@ -158,6 +171,8 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
158
171
|
saveFilter: (f) => write(FILTER_KEY, JSON.stringify(filterOk(f))),
|
|
159
172
|
/** The whole fleet's style here: a skin id ("pixel": desks, "studio" and "cozy": zones); none chosen yet: cozy (#2631); an unknown one is the default where used. */
|
|
160
173
|
skin: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : "cozy"; },
|
|
174
|
+
/** #2716: this browser's own pick of it, or null (none made here). */
|
|
175
|
+
skinChosen: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : null; },
|
|
161
176
|
saveSkin: (id) => write(SKIN_KEY, String(id)),
|
|
162
177
|
/** Whether paused and stopped people are left out of the Office here (default: shown). */
|
|
163
178
|
hideAsleep: () => read(ASLEEP_KEY) === "1",
|
|
@@ -97,6 +97,14 @@ export function miniFrame(v, size, w, h) {
|
|
|
97
97
|
const x1 = Math.min(w, (size.w - v.x) / v.k), y1 = Math.min(h, (size.h - v.y) / v.k);
|
|
98
98
|
return { x: x0, y: y0, width: Math.max(0, x1 - x0), height: Math.max(0, y1 - y0) };
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* #2737: the whole scene is in view (the overview's frame covers all of it, to half a unit), so the overview would only
|
|
102
|
+
* repeat the panel — and sit over the one corner of it a person cannot otherwise see.
|
|
103
|
+
*/
|
|
104
|
+
export function wholeInView(v, size, w, h) {
|
|
105
|
+
const f = miniFrame(v, size, w, h);
|
|
106
|
+
return f.x <= 0.5 && f.y <= 0.5 && f.x + f.width >= w - 0.5 && f.y + f.height >= h - 0.5;
|
|
107
|
+
}
|
|
100
108
|
|
|
101
109
|
// ── Kept per device, per office: the zoom and the logical point in the middle (the viewport's size may differ) ──
|
|
102
110
|
|
|
@@ -140,7 +140,7 @@ export function zonesLayout(pack, nodes, prev = new Map(), { night = false } = {
|
|
|
140
140
|
return { key: `zone/${n.name}`, node: n, x: s.x - 24, y: s.y - 52, w: 48, h: 66, seat: { x: s.x, y: s.y }, wp: null, area: s.area, slot: s.slot, night: sleepers.has(n.name) };
|
|
141
141
|
});
|
|
142
142
|
return { mode: "zones", rooms: [{ key: "\u0000zones", name: null, lobby: false, x: 0, y: 0, w: LOGICAL.w, h: LOGICAL.h, desks }], areas: pack.areas,
|
|
143
|
-
waypoints: pack.waypoints || [], edges: pack.edges || [], straight: pack.straight === true, door: pack.door, width: LOGICAL.w, height: LOGICAL.h, seats, crowd, over };
|
|
143
|
+
waypoints: pack.waypoints || [], edges: pack.edges || [], straight: pack.straight === true, door: pack.door, mailDrop: pack.mailDrop || null, width: LOGICAL.w, height: LOGICAL.h, seats, crowd, over };
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// ── Walking ──
|
|
@@ -227,7 +227,9 @@ export function walkTargets(desks, graph, t, wander = true, furniture = [], nigh
|
|
|
227
227
|
*/
|
|
228
228
|
export const MAIL = { MAX_WAIT: 60000, GAP: 16, SEARCH: 320, ASLEEP: new Set(["paused", "stopped"]), MAX: 20 };
|
|
229
229
|
/**
|
|
230
|
-
* Where the `n`th waiting letter lands: `
|
|
230
|
+
* Where the `n`th waiting letter lands. #2736: the layout's `mailDrop` when it has one — the `n`th of the slots GAP
|
|
231
|
+
* apart rightwards from it that no seat is on (never beyond the room's right edge); fewer than that, today's rule. Otherwise `n`
|
|
232
|
+
* GAPs along from the waypoint nearest the room's middle — the nearest whose
|
|
231
233
|
* spot (offset included) no seat of the office is on, taken or free (someone may sit there any moment; #2561 likewise);
|
|
232
234
|
* with none, the nearest point round the middle (rings GAP·2 apart, inside the room) that no seat is on; with none of
|
|
233
235
|
* those either, null — the letter is not delivered here (never on a seat).
|
|
@@ -236,6 +238,12 @@ export function dropPoint(L, n = 0) {
|
|
|
236
238
|
const mid = { x: L.width / 2, y: L.height * 0.55 }, d = (p, q) => Math.hypot(p.x - q.x, p.y - q.y);
|
|
237
239
|
const seats = [...Object.values(L.areas || {}).flatMap((a) => a.seats || []), ...desksIn(L).map((q) => q.seat)];
|
|
238
240
|
const onSeat = (p) => seats.some((q) => d(p, q) < 4);
|
|
241
|
+
// #2751 review: the nth off-seat slot (slots GAP apart from the mailDrop), so two letters never share one.
|
|
242
|
+
const md = L.mailDrop;
|
|
243
|
+
if (md) for (let i = 0, free = 0; md.x + i * MAIL.GAP <= L.width; i++) {
|
|
244
|
+
const p = { x: Math.round(md.x + i * MAIL.GAP), y: Math.round(md.y) };
|
|
245
|
+
if (!onSeat(p) && free++ === n) return p;
|
|
246
|
+
}
|
|
239
247
|
const spots = (L.waypoints || []).map((w) => ({ x: Math.round(w.x + n * MAIL.GAP), y: Math.round(w.y), from: w }))
|
|
240
248
|
.filter((p) => !onSeat(p.from) && !onSeat(p)).sort((p, q) => d(p.from, mid) - d(q.from, mid));
|
|
241
249
|
if (spots.length) return { x: spots[0].x, y: spots[0].y };
|
package/dist/ui/fleet-office.js
CHANGED
|
@@ -75,7 +75,7 @@ import { applyOverrides, fitNight, fitSeatFor, stripBox } from "./fleet-office-p
|
|
|
75
75
|
import { ALL_ID, LOBBY_TILE, OV, createCrossFlights, cropToRooms, fromTile, inTile, labelAt, lobbyNodes, overviewOk, tileAt, tileBox, tileLayout, visibleTiles } from "./fleet-office-overview.js";
|
|
76
76
|
import { centreOn, gestureState, gestures, miniPoint, toScreen, toWorld, wheelFactor, zoomKey, zoomTo } from "./fleet-canvas-view.js";
|
|
77
77
|
import { OK_MAX, OK_MIN, clampPan, desksK, edgeMatte, edgeStrips, fitScene, layerScale, miniBox, miniFrame, openingView, pinchOffice, savedOf, stepK, viewFromSaved,
|
|
78
|
-
zoomLabel, zoomOffice } from "./fleet-office-view.js";
|
|
78
|
+
wholeInView, zoomLabel, zoomOffice } from "./fleet-office-view.js";
|
|
79
79
|
|
|
80
80
|
register("orgo", {
|
|
81
81
|
en: {
|
|
@@ -89,6 +89,8 @@ register("orgo", {
|
|
|
89
89
|
b_paused: "Zzz…|Back later", b_stopped: "Off duty|Zzz…", b_needs: "Need you!|A question for you|Over here!",
|
|
90
90
|
b_crashed: "Ouch, crashed|Help!", b_stuck: "Hmm, stuck?|Still thinking…",
|
|
91
91
|
hideAsleep: "Hide paused and stopped", nightOn: "Bedtime 0–6: the idle sleep", layoutIgnored: "Its saved layout does not fit this style: drawn as the style's own.",
|
|
92
|
+
styleForAll: "The whole fleet's style, for everyone: every device and share link", styleHere: "The whole fleet's style in this browser",
|
|
93
|
+
styleFailed: "The style was not saved for everyone ({0}).", styleBusy: "Too many changes. Try again in a minute.",
|
|
92
94
|
fit: "Fit the office", overview: "Overview: click to move there", fullscreen: "Fullscreen", exitFullscreen: "Exit fullscreen",
|
|
93
95
|
fill: "Fill", fillTitle: "Fill the panel (the scene's overflow is cropped; drag to see it)", zoomLevel: "Zoom {0}",
|
|
94
96
|
allOffices: "All offices", labelAll: "All offices: each office is a tile, drawn in its own style. Drag to pan; the wheel, a pinch, + and − zoom, 0 fits, F is fullscreen. Double-click a tile to open that office.",
|
|
@@ -105,6 +107,8 @@ register("orgo", {
|
|
|
105
107
|
b_paused: "Zzz…|晚點回來", b_stopped: "下班了|Zzz…", b_needs: "需要你!|有個問題|在這裡!",
|
|
106
108
|
b_crashed: "糟糕,當掉了|救命!", b_stuck: "嗯,卡住了?|還在想…",
|
|
107
109
|
hideAsleep: "隱藏暫停與停止的成員", nightOn: "0–6 點就寢:閒著的人去睡", layoutIgnored: "它儲存的配置不適用這個風格:以風格本身的配置顯示。",
|
|
110
|
+
styleForAll: "整個 fleet 的風格,所有人共用:每台裝置與分享連結都一樣", styleHere: "這個瀏覽器裡整個 fleet 的風格",
|
|
111
|
+
styleFailed: "風格沒有替所有人儲存({0})。", styleBusy: "變更太頻繁,請一分鐘後再試。",
|
|
108
112
|
fit: "顯示整個辦公室", overview: "總覽:點一下移到那裡", fullscreen: "全螢幕", exitFullscreen: "結束全螢幕",
|
|
109
113
|
fill: "填滿", fillTitle: "填滿整個面板(超出的部分會被裁切,拖曳即可看到)", zoomLevel: "縮放 {0}",
|
|
110
114
|
allOffices: "所有辦公室", labelAll: "所有辦公室:每間辦公室是一格,以它自己的風格顯示。拖曳平移;滾輪、雙指、+ 和 − 縮放,0 顯示全部,F 切換全螢幕。雙擊一格即可開啟那間辦公室。",
|
|
@@ -531,7 +535,12 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
531
535
|
const [editing, setEditing] = useState(false);
|
|
532
536
|
const [choice, setChoice] = useState(() => opening ?? store.choice());
|
|
533
537
|
const [filter, setFilter] = useState(() => store.filter());
|
|
534
|
-
|
|
538
|
+
// #2716: the whole-fleet office's style — the host's (for everyone) and this browser's own pick. An admin sees the
|
|
539
|
+
// host's (their own pick only while the host keeps none) and picks for everyone; anyone else's own pick, else the host's.
|
|
540
|
+
const [hostStyle, setHostStyle] = useState(undefined); // undefined: not read yet; null: none kept
|
|
541
|
+
const [ownStyle, setOwnStyle] = useState(() => store.skinChosen());
|
|
542
|
+
const [styleErr, setStyleErr] = useState("");
|
|
543
|
+
const style = canEdit ? hostStyle ?? ownStyle ?? "cozy" : ownStyle ?? hostStyle ?? "cozy";
|
|
535
544
|
const [hideAsleep, setHideAsleep] = useState(() => store.hideAsleep());
|
|
536
545
|
// Bedtime (#2568), when it is on here: the idle sleep from NIGHT.FROM to NIGHT.TO on this device's clock (read every NIGHT.TICK).
|
|
537
546
|
const [nightOn, setNightOn] = useState(() => store.nightOn());
|
|
@@ -572,15 +581,34 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
572
581
|
// The editor's save and its 409 re-read are stamped when sent (`ask`), never when answered; `adopt` returns the
|
|
573
582
|
// snapshot in force, so a re-read the stream overtook leaves the editor's draft on the newer one.
|
|
574
583
|
const asked = useRef(0), shown = useRef(null); // a counter; shown: { rev, offices, arrived }
|
|
584
|
+
const migrated = useRef(false); // #2716: this page's own style sent up
|
|
575
585
|
const ask = () => ++asked.current;
|
|
576
586
|
const adopt = (d, askedAt) => {
|
|
577
587
|
const cur = shown.current;
|
|
578
588
|
if (!d) return cur;
|
|
579
589
|
if (cur && d.rev < cur.rev && askedAt <= cur.arrived) return cur;
|
|
580
590
|
shown.current = { rev: d.rev, offices: d.offices, arrived: ++asked.current };
|
|
581
|
-
setStored(d.offices); setStoredRev(d.rev);
|
|
591
|
+
setStored(d.offices); setStoredRev(d.rev); setHostStyle(d.autoSkin || null);
|
|
582
592
|
return shown.current;
|
|
583
593
|
};
|
|
594
|
+
// #2716: an admin's own pick from before the host kept one (or since an older host's save dropped it) goes up once.
|
|
595
|
+
useEffect(() => {
|
|
596
|
+
if (!canEdit || hostStyle !== null || !ownStyle || migrated.current || !lease) return;
|
|
597
|
+
migrated.current = true;
|
|
598
|
+
void store.saveAutoSkin(ownStyle).then((r) => { if (r.ok && lease.current()) setHostStyle(r.autoSkin); });
|
|
599
|
+
}, [canEdit, hostStyle]);
|
|
600
|
+
/** #2716: a style picked here — this browser's own; an admin's, for everyone (on failure: said, and read again). */
|
|
601
|
+
const pickStyle = (id) => {
|
|
602
|
+
store.saveSkin(id); setOwnStyle(id); setTip(null); setStyleErr("");
|
|
603
|
+
if (!canEdit || !lease) return;
|
|
604
|
+
const was = hostStyle;
|
|
605
|
+
setHostStyle(id);
|
|
606
|
+
void store.saveAutoSkin(id).then((r) => {
|
|
607
|
+
if (!lease.current() || r.ok) return;
|
|
608
|
+
setHostStyle(was);
|
|
609
|
+
setStyleErr(to(r.status === 429 ? "styleBusy" : "styleFailed", r.error));
|
|
610
|
+
});
|
|
611
|
+
};
|
|
584
612
|
useEffect(() => {
|
|
585
613
|
if (!lease) return undefined;
|
|
586
614
|
const read = async () => { const at = ask(); const d = await store.offices(); if (lease.current()) adopt(d, at); };
|
|
@@ -832,6 +860,9 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
832
860
|
if (mb && mb.style) { // the overview: inside the stage, scaled to it
|
|
833
861
|
const b = miniBox(size, L.width, L.height);
|
|
834
862
|
for (const [a, val] of [["left", b.x], ["top", b.y], ["width", b.w], ["height", b.h]]) mb.style.setProperty(a, `${val}px`);
|
|
863
|
+
// #2737: hidden while a single office is all in view (it would show what the panel shows); All offices keeps it.
|
|
864
|
+
if (!overview && wholeInView(v, size, L.width, L.height)) mb.setAttribute("data-whole", "");
|
|
865
|
+
else mb.removeAttribute("data-whole");
|
|
835
866
|
}
|
|
836
867
|
const pc = pctRef.current;
|
|
837
868
|
if (pc) { const lb = zoomLabel(v.k); if (pc.textContent !== lb) pc.textContent = lb; pc.setAttribute("aria-label", to("zoomLevel", lb)); }
|
|
@@ -1243,7 +1274,7 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
1243
1274
|
<div class="oo-bar">
|
|
1244
1275
|
<label class="oo-pick"><span class="oo-pick-label">${to("office")}</span>${officeSelect("oo-office")}</label>
|
|
1245
1276
|
${auto ? html`<label class="oo-pick"><span class="oo-pick-label">${to("styleLabel")}</span>
|
|
1246
|
-
<select class="tb-select" value=${STYLES.includes(style) || packs.some((pk) => pk.id === style) ? style : skin.id}
|
|
1277
|
+
<select class="tb-select" value=${STYLES.includes(style) || packs.some((pk) => pk.id === style) ? style : skin.id} title=${to(canEdit ? "styleForAll" : "styleHere")} onChange=${(e) => pickStyle(e.target.value)}>
|
|
1247
1278
|
${styles.map((id) => html`<option key=${id} value=${id}>${to(`style_${id}`)}</option>`)}
|
|
1248
1279
|
${packs.map((pk) => html`<option key=${`pack:${pk.id}`} value=${pk.id}>${pk.name}</option>`)}
|
|
1249
1280
|
</select></label>` : null}
|
|
@@ -1259,6 +1290,7 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
1259
1290
|
${lobbyAll.length ? null : html`<span class="muted oo-note oo-lobby-none">${to("lobbyEveryone")}</span>`}` : null}
|
|
1260
1291
|
${custom && baseSkin.id !== custom.skin && !loading ? html`<span class="muted oo-note">${to("skinUnknown", styleName(custom.skin))}</span>` : null}
|
|
1261
1292
|
${placed.ignored ? html`<span class="muted oo-note">${to("layoutIgnored")}</span>` : null}
|
|
1293
|
+
${styleErr && auto ? html`<span class="note oo-note oo-style-err" role="status">${styleErr}</span>` : null}
|
|
1262
1294
|
${L.unseated ? html`<span class="muted oo-note">${to("unseated", L.unseated)}</span>` : null}
|
|
1263
1295
|
${canEdit && storedRev !== null ? html`<button type="button" class="btn btn-sm oo-edit" onClick=${() => { if (!overview) setEditing(true); }} disabled=${!!layout || overview}
|
|
1264
1296
|
title=${overview ? to("overviewNoEdit") : undefined}>${t("orge.edit")}</button>` : null}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// #2636: Office share links on the dashboard — the Office's Share… dialog (make one: which office, how long, whether
|
|
2
2
|
// agents' own lines show; then its URL to copy) and the list of open ones (Settings → Access & security, and under the
|
|
3
3
|
// dialog's form): each with its office, when it ends, whether lines show and who made it, Revoke and Revoke all.
|
|
4
|
+
// #2716: the whole-fleet office can be shared too ("auto", the default while it is the one shown); #2715: the link's
|
|
5
|
+
// look — light or dark as this dashboard shows it now (the default, the device's when it follows the device), or the
|
|
6
|
+
// viewer's device's.
|
|
4
7
|
// GET / POST /ui/office-share, DELETE /ui/office-share/<id|all> (web-api.ts): an admin session on the dashboard reached
|
|
5
8
|
// directly only; the public link never offers these.
|
|
6
9
|
import { html, useEffect, useState } from "/assets/app-html.js";
|
|
@@ -11,7 +14,8 @@ import { toast } from "/assets/ui-toast.js";
|
|
|
11
14
|
register("oshare", {
|
|
12
15
|
en: {
|
|
13
16
|
share: "Share…", title: "Share the office", lead: "Anyone with the link can watch this office live — names and states, nothing else — until it ends. No sign-in.",
|
|
14
|
-
office: "Office", allOffices: "All offices",
|
|
17
|
+
office: "Office", allOffices: "All offices", wholeFleet: "Whole fleet", how: "For",
|
|
18
|
+
look: "Look", lookAuto: "The viewer's device (light or dark)", lookLight: "Light", lookDark: "Dark", text: "Include agents' own status lines", create: "Create link", creating: "Starting the link…",
|
|
15
19
|
made: "Anyone with this link can watch until {0}.", copy: "Copy link", copied: "Link copied", open: "Open links", none: "No share links are open.",
|
|
16
20
|
ends: "until {0}", withText: "with status lines", by: "by {0}", revoke: "Revoke", revokeAll: "Revoke all", close: "Close", failed: "The link could not be made.",
|
|
17
21
|
m15: "15 minutes", m30: "30 minutes", h1: "1 hour", h2: "2 hours", h4: "4 hours", h8: "8 hours", h24: "24 hours",
|
|
@@ -22,7 +26,8 @@ register("oshare", {
|
|
|
22
26
|
},
|
|
23
27
|
"zh-TW": {
|
|
24
28
|
share: "分享…", title: "分享辦公室", lead: "拿到連結的人都能即時觀看這間辦公室(只有名稱與狀態),直到連結結束,不需要登入。",
|
|
25
|
-
office: "辦公室", allOffices: "所有辦公室",
|
|
29
|
+
office: "辦公室", allOffices: "所有辦公室", wholeFleet: "整個 fleet", how: "時間",
|
|
30
|
+
look: "外觀", lookAuto: "依觀看者的裝置(淺色或深色)", lookLight: "淺色", lookDark: "深色", text: "包含 agent 自己的狀態文字", create: "建立連結", creating: "正在啟動連結…",
|
|
26
31
|
made: "拿到這個連結的人可以觀看到 {0}。", copy: "複製連結", copied: "已複製連結", open: "開啟中的連結", none: "目前沒有開啟中的分享連結。",
|
|
27
32
|
ends: "到 {0} 為止", withText: "含狀態文字", by: "由 {0} 建立", revoke: "撤銷", revokeAll: "全部撤銷", close: "關閉", failed: "無法建立連結。",
|
|
28
33
|
m15: "15 分鐘", m30: "30 分鐘", h1: "1 小時", h2: "2 小時", h4: "4 小時", h8: "8 小時", h24: "24 小時",
|
|
@@ -44,6 +49,16 @@ export function refusalText(d) {
|
|
|
44
49
|
return ["disabled", "office", "duration", "too_many"].includes(e) ? to(`e_${e}`) : to("failed");
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
/**
|
|
53
|
+
* #2715: the look a new link takes by default — what this dashboard shows now: its chosen light or dark, or (following
|
|
54
|
+
* the device) the device's; unknown, the viewer's own.
|
|
55
|
+
*/
|
|
56
|
+
export function defaultLook(theme = globalThis.AgendTheme && typeof globalThis.AgendTheme.get === "function" ? globalThis.AgendTheme.get() : null,
|
|
57
|
+
media = typeof globalThis.matchMedia === "function" ? globalThis.matchMedia : null) {
|
|
58
|
+
if (theme === "light" || theme === "dark") return theme;
|
|
59
|
+
try { return media ? (media("(prefers-color-scheme: dark)").matches ? "dark" : "light") : "auto"; } catch { return "auto"; }
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
/** The open shares, read (and re-read after a change): { enabled, shares } or null while unread / unavailable. */
|
|
48
63
|
export function useShares(get = globalThis.fetch, rev = 0) {
|
|
49
64
|
const [d, setD] = useState(null);
|
|
@@ -67,7 +82,7 @@ export function ShareList({ shares, names = {}, get = globalThis.fetch, onChange
|
|
|
67
82
|
if (!shares || !shares.length) return html`<p class="muted os-none">${to("none")}</p>`;
|
|
68
83
|
return html`<div class="os-list">
|
|
69
84
|
<ul class="os-rows">${shares.map((s) => html`<li key=${s.id} class="os-row">
|
|
70
|
-
<span class="os-what"><strong>${s.office === "all" ? to("allOffices") : names[s.office] || s.office_name || s.office}</strong>
|
|
85
|
+
<span class="os-what"><strong>${s.office === "all" ? to("allOffices") : s.office === "auto" ? to("wholeFleet") : names[s.office] || s.office_name || s.office}</strong>
|
|
71
86
|
<span class="muted"> · ${to("ends", when(s.expiresAt))}${s.text ? ` · ${to("withText")}` : ""}</span></span>
|
|
72
87
|
<button type="button" class="btn btn-sm" disabled=${busy} onClick=${() => revoke(s.id)}>${to("revoke")}</button>
|
|
73
88
|
</li>`)}</ul>
|
|
@@ -75,9 +90,13 @@ export function ShareList({ shares, names = {}, get = globalThis.fetch, onChange
|
|
|
75
90
|
</div>`;
|
|
76
91
|
}
|
|
77
92
|
|
|
78
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Share…: make a link for one office (the one shown, by default), all of them, or (#2716) the whole-fleet office
|
|
95
|
+
* (`current` null: the one shown). `offices`: the stored ones.
|
|
96
|
+
*/
|
|
79
97
|
export function ShareDialog({ offices = [], current = null, get = globalThis.fetch, onClose }) {
|
|
80
|
-
const [office, setOffice] = useState(current
|
|
98
|
+
const [office, setOffice] = useState(current === null ? "auto" : current === "all" || offices.some((o) => o.id === current) ? current : "all");
|
|
99
|
+
const [theme, setTheme] = useState(() => defaultLook());
|
|
81
100
|
const [minutes, setMinutes] = useState(60);
|
|
82
101
|
const [text, setText] = useState(false);
|
|
83
102
|
const [busy, setBusy] = useState(false);
|
|
@@ -89,7 +108,7 @@ export function ShareDialog({ offices = [], current = null, get = globalThis.fet
|
|
|
89
108
|
const create = async () => {
|
|
90
109
|
setBusy(true); setError("");
|
|
91
110
|
try {
|
|
92
|
-
const r = await get("/ui/office-share", { method: "POST", headers: JSON_HEADERS, body: JSON.stringify({ office, minutes, text }) });
|
|
111
|
+
const r = await get("/ui/office-share", { method: "POST", headers: JSON_HEADERS, body: JSON.stringify({ office, minutes, text, theme }) });
|
|
93
112
|
const d = await r.json().catch(() => ({}));
|
|
94
113
|
if (r.ok && d && typeof d.url === "string") setMade(d); else setError(refusalText(d));
|
|
95
114
|
} catch { setError(to("failed")); }
|
|
@@ -110,13 +129,18 @@ export function ShareDialog({ offices = [], current = null, get = globalThis.fet
|
|
|
110
129
|
</div>` : html`<div class="os-form">
|
|
111
130
|
<label class="field"><span>${to("office")}</span>
|
|
112
131
|
<select class="tb-select" value=${office} onChange=${(e) => setOffice(e.target.value)} disabled=${busy}>
|
|
113
|
-
|
|
132
|
+
<option value="auto">${to("wholeFleet")}</option>
|
|
133
|
+
${offices.length > 1 || office === "all" ? html`<option value="all">${to("allOffices")}</option>` : null}
|
|
114
134
|
${offices.map((o) => html`<option key=${o.id} value=${o.id}>${o.name}</option>`)}
|
|
115
135
|
</select></label>
|
|
116
136
|
<label class="field"><span>${to("how")}</span>
|
|
117
137
|
<select class="tb-select" value=${String(minutes)} onChange=${(e) => setMinutes(Number(e.target.value))} disabled=${busy}>
|
|
118
138
|
${DURATIONS.map(([k, m]) => html`<option key=${k} value=${String(m)}>${to(k)}</option>`)}
|
|
119
139
|
</select></label>
|
|
140
|
+
<label class="field"><span>${to("look")}</span>
|
|
141
|
+
<select class="tb-select os-look" value=${theme} onChange=${(e) => setTheme(e.target.value)} disabled=${busy}>
|
|
142
|
+
${[["auto", "lookAuto"], ["light", "lookLight"], ["dark", "lookDark"]].map(([v, k]) => html`<option key=${v} value=${v}>${to(k)}</option>`)}
|
|
143
|
+
</select></label>
|
|
120
144
|
<label class="check"><input type="checkbox" checked=${text} onChange=${(e) => setText(e.target.checked)} disabled=${busy} /> ${to("text")}</label>
|
|
121
145
|
${error ? html`<p class="err os-err" role="alert">${error}</p>` : null}
|
|
122
146
|
</div>`}
|
package/dist/ui/panel-log.js
CHANGED
|
@@ -62,7 +62,7 @@ register("log", {
|
|
|
62
62
|
ev_backend_unreachable: "CLI unreachable", ev_startup_backend_unreachable: "CLI unreachable at start", ev_backend_launch_warning: "Launch warning",
|
|
63
63
|
ev_clear_action: "Conversation cleared", ev_normal_exit_action: "Exited", ev_reply_drop_detected: "A reply went missing",
|
|
64
64
|
ev_reply_drop_recovered: "Missing reply recovered", ev_reply_drop_unrecovered: "Missing reply not recovered",
|
|
65
|
-
ev_supervision_ended: "Supervision ended", ev_tmux_server_crash: "tmux crashed", ev_malformed_tool_call: "Malformed tool call", ev_health_check_error: "Health check error", ev_context_lost: "Started fresh: context lost", ev_working_state: "Working-state journal",
|
|
65
|
+
ev_supervision_ended: "Supervision ended", ev_tmux_server_crash: "tmux crashed", ev_malformed_tool_call: "Malformed tool call", ev_health_check_error: "Health check error", ev_context_lost: "Started fresh: context lost", ev_working_state: "Working-state journal", ev_kiro_reconcile: "Kiro conversation reconciliation",
|
|
66
66
|
},
|
|
67
67
|
"zh-TW": {
|
|
68
68
|
offTitle: "紀錄未開啟",
|
|
@@ -100,7 +100,7 @@ register("log", {
|
|
|
100
100
|
ev_backend_unreachable: "連不到 CLI", ev_startup_backend_unreachable: "啟動時連不到 CLI", ev_backend_launch_warning: "啟動警告",
|
|
101
101
|
ev_clear_action: "對話已清除", ev_normal_exit_action: "已結束", ev_reply_drop_detected: "有回覆遺失",
|
|
102
102
|
ev_reply_drop_recovered: "遺失的回覆已補回", ev_reply_drop_unrecovered: "遺失的回覆沒有補回",
|
|
103
|
-
ev_supervision_ended: "監督結束", ev_tmux_server_crash: "tmux 當掉", ev_malformed_tool_call: "格式錯誤的工具呼叫", ev_health_check_error: "健康檢查錯誤", ev_context_lost: "改用全新對話:context 遺失", ev_working_state: "工作狀態紀錄",
|
|
103
|
+
ev_supervision_ended: "監督結束", ev_tmux_server_crash: "tmux 當掉", ev_malformed_tool_call: "格式錯誤的工具呼叫", ev_health_check_error: "健康檢查錯誤", ev_context_lost: "改用全新對話:context 遺失", ev_working_state: "工作狀態紀錄", ev_kiro_reconcile: "Kiro 對話接續處理",
|
|
104
104
|
},
|
|
105
105
|
});
|
|
106
106
|
const tl = (key, ...a) => t(`log.${key}`, ...a);
|
|
@@ -185,6 +185,7 @@ register("settings", {
|
|
|
185
185
|
wizardBackendFound: "Found on this host: {0}",
|
|
186
186
|
wizardBackendNone: "No backend CLI found on this host — install one first.",
|
|
187
187
|
wizardAgentName: "First agent name",
|
|
188
|
+
wizardGeneralNote: "This agent is the General: it answers in the chat's own General topic and hands work to the agents you add later.",
|
|
188
189
|
wizardAgentWhich: "Which agent answers on this bot",
|
|
189
190
|
wizardAgentNew: "A new agent",
|
|
190
191
|
wizardAgentExisting: "An agent you already have",
|
|
@@ -838,6 +839,7 @@ register("settings", {
|
|
|
838
839
|
wizardBackendFound: "在這台主機上找到:{0}",
|
|
839
840
|
wizardBackendNone: "這台主機上找不到任何後端 CLI —— 請先安裝一個。",
|
|
840
841
|
wizardAgentName: "第一個 agent 名稱",
|
|
842
|
+
wizardGeneralNote: "這個 agent 就是 General:它在聊天本身的 General 主題回覆,並把工作分給你之後加入的 agent。",
|
|
841
843
|
wizardAgentWhich: "由哪個 agent 在這個機器人上回覆",
|
|
842
844
|
wizardAgentNew: "新的 agent",
|
|
843
845
|
wizardAgentExisting: "既有的 agent",
|
|
@@ -44,7 +44,11 @@ export function SetupWizard({ ctx, onClose }) {
|
|
|
44
44
|
const agents = Array.isArray(e.agents) ? e.agents : [];
|
|
45
45
|
const names = new Set(agents.map((a) => a.name));
|
|
46
46
|
let n = 1; while (names.has(`agent-${n}`)) n++;
|
|
47
|
-
|
|
47
|
+
// M-2069: on a fleet with no chat connection and no General, the new agent is the General — named "general" (unless
|
|
48
|
+
// an agent has that name already: #2744 review).
|
|
49
|
+
const general = !!e.first_general && !names.has("general");
|
|
50
|
+
setW({ step: 1, env: e, backend: (e.backends || [])[0] || "claude-code", working_directory: "", instance_name: general ? "general" : `agent-${n}`,
|
|
51
|
+
general, defaultName: `agent-${n}`, nameTyped: false,
|
|
48
52
|
agent: agents.length ? "existing" : "new", existing_name: agents.length ? agents[0].name : "",
|
|
49
53
|
platform: "telegram", token: "", group_id: "", guild_id: "", general_channel_id: "", admin_user_id: "",
|
|
50
54
|
identity: null, guilds: [], plan: null, offset: 0 });
|
|
@@ -54,6 +58,11 @@ export function SetupWizard({ ctx, onClose }) {
|
|
|
54
58
|
// Every field the plan or a probe is about moves the revision (#1529 review r2): a plan or Detect that began before an
|
|
55
59
|
// edit — target, admin, backend, directory, name — is dropped when it lands, never committed or written over the edit.
|
|
56
60
|
const set = (k) => (v) => { bump(); setW((x) => ({ ...x, [k]: v })); };
|
|
61
|
+
// #2744 review: the General works in its own folder — a typed directory makes the new agent a worker, so a name not
|
|
62
|
+
// typed by hand goes back to agent-N (and "general" again once the directory is cleared).
|
|
63
|
+
const asGeneral = (x) => x.general && !x.working_directory;
|
|
64
|
+
const setDir = (v) => { bump(); setW((x) => { const next = { ...x, working_directory: v };
|
|
65
|
+
return x.nameTyped ? next : { ...next, instance_name: asGeneral(next) ? "general" : x.defaultName }; }); };
|
|
57
66
|
// No token env: the plan generates one (#1519 P1: AGEND_<PLATFORM>_<ID>_TOKEN, unique) and the commit carries the plan's.
|
|
58
67
|
// An existing agent (#1519 P7) is named and nothing else: the server keeps its directory and backend.
|
|
59
68
|
const input = () => ({
|
|
@@ -144,9 +153,10 @@ export function SetupWizard({ ctx, onClose }) {
|
|
|
144
153
|
<p class="note">${tn("wizardAgentExistingNote")}</p></div>` : html`${which}<div class="field"><label for="wz-be">${tn("wizardBackend")}</label>
|
|
145
154
|
<${Select} id="wz-be" value=${w.backend} onChange=${set("backend")} options=${w.env.backends && w.env.backends.length ? w.env.backends : BACKENDS} />
|
|
146
155
|
<p class="note">${w.env.backends && w.env.backends.length ? tn("wizardBackendFound", w.env.backends.join(", ")) : tn("wizardBackendNone")}</p></div>
|
|
147
|
-
<div class="field"><label for="wz-wd">${tn("wizardDir")}</label><input id="wz-wd" type="text" placeholder="/home/you/projects/app" aria-describedby="wz-wd-note" value=${dirText} onInput=${(e) =>
|
|
156
|
+
<div class="field"><label for="wz-wd">${tn("wizardDir")}</label><input id="wz-wd" type="text" placeholder="/home/you/projects/app" aria-describedby="wz-wd-note" value=${dirText} onInput=${(e) => setDir(onDirText(e.target.value))} />
|
|
148
157
|
<p class="note" id="wz-wd-note">${w.env.workspaces_dir && w.instance_name ? tn("wizardDirDefault", `${w.env.workspaces_dir}/${w.instance_name}`) : tn("wizardDirDefaultName")}</p></div>
|
|
149
|
-
<div class="field"><label for="wz-name">${agents.length ? tn("wizardAgentNewName") : tn("wizardAgentName")}</label><input id="wz-name" type="text" value=${nameText} onInput=${(e) =>
|
|
158
|
+
<div class="field"><label for="wz-name">${agents.length ? tn("wizardAgentNewName") : tn("wizardAgentName")}</label><input id="wz-name" type="text" value=${nameText} onInput=${(e) => { const v = onNameText(e.target.value); bump(); setW((x) => ({ ...x, instance_name: v, nameTyped: true })); }} />
|
|
159
|
+
${asGeneral(w) ? html`<p class="note">${tn("wizardGeneralNote")}</p>` : null}</div>`;
|
|
150
160
|
} else if (w.step === 2) {
|
|
151
161
|
body = html`<div class="seg-inline" role="group" aria-label=${tn("wizardPlatform")}>
|
|
152
162
|
${["telegram", "discord"].map((p) => html`<button key=${p} type="button" class=${`btn${w.platform === p ? " btn-primary" : ""}`} aria-pressed=${w.platform === p ? "true" : "false"}
|
|
@@ -169,7 +179,8 @@ export function SetupWizard({ ctx, onClose }) {
|
|
|
169
179
|
body = !w.plan ? html`<p class="note">${tn("wizardPlanLoading")}</p>` : html`
|
|
170
180
|
<p><strong>${tn("wizardWillWrite")}</strong></p>
|
|
171
181
|
<pre class="s-yaml">${toYaml({ channels: [w.plan.channel], instances: { [w.plan.instance.name]: { working_directory: w.plan.instance.working_directory, backend: w.plan.instance.backend, channel_id: w.plan.instance.channel_id,
|
|
172
|
-
...(w.plan.instance.topic && w.plan.instance.topic.to != null ? { topic_id: w.plan.instance.topic.to } : {})
|
|
182
|
+
...(w.plan.instance.topic && w.plan.instance.topic.to != null ? { topic_id: w.plan.instance.topic.to } : {}),
|
|
183
|
+
...(w.plan.instance.general ? { general_topic: true, ...(w.plan.instance.general.topic_id != null ? { topic_id: w.plan.instance.general.topic_id } : {}) } : {}) } } })}</pre>
|
|
173
184
|
${w.plan.instance && w.plan.instance.existing ? html`<p class="note">${tn("wizardAgentExistingNote")}</p>` : null}
|
|
174
185
|
${w.plan.instance && w.plan.instance.topic ? html`<p class="feedback warning">${w.plan.instance.topic.to == null
|
|
175
186
|
? tn("wizardTopicNotCarried", w.plan.instance.topic.from) : tn("wizardTopicGeneral")}</p>` : null}
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -1798,6 +1798,8 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1798
1798
|
.oo-zoom .oo-fill { font-size: 11px; font-weight: var(--fw-semibold); }
|
|
1799
1799
|
.oo-zoom .oo-fill.on { background: var(--accent-dim); color: var(--text); }
|
|
1800
1800
|
.oo-stage.editing .oo-mini { display: none; }
|
|
1801
|
+
/* #2737: a single office all in view (the page sets data-whole): the overview would repeat the panel. */
|
|
1802
|
+
.oo-mini[data-whole] { display: none; }
|
|
1801
1803
|
.oo-mini-scene { fill: var(--bg); }
|
|
1802
1804
|
.oo-mini-room { fill: var(--surface); stroke: var(--border-strong); stroke-width: 6; }
|
|
1803
1805
|
.oo-mini-dot { fill: var(--text-3); }
|
|
@@ -1851,6 +1853,10 @@ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
|
|
|
1851
1853
|
.ol-seat > circle { fill: var(--ok); }
|
|
1852
1854
|
.ol-furniture > rect { fill: var(--warn); }
|
|
1853
1855
|
.ol-door > rect { fill: var(--danger); }
|
|
1856
|
+
/* #2736: the mail drop — an envelope; dashed while it is the automatic spot (the pack names none). */
|
|
1857
|
+
.ol-mail > rect { fill: var(--accent-dim); }
|
|
1858
|
+
.ol-mail > .ol-flap { fill: none; stroke: var(--text); stroke-width: 1.5; pointer-events: none; }
|
|
1859
|
+
.ol-mail.auto > rect { stroke-dasharray: 3 2; fill-opacity: 0.6; }
|
|
1854
1860
|
/* #2665: an item's night place — a moon on a faint disc, a thin dashed line to its day place. */
|
|
1855
1861
|
.ol-night > circle { fill: var(--surface); fill-opacity: 0.85; }
|
|
1856
1862
|
.ol-night > .ol-moon { fill: var(--warn); stroke: none; pointer-events: none; }
|
|
@@ -35,6 +35,8 @@ export const HOST_MESSAGES = [
|
|
|
35
35
|
// Conflicts (agentConflict, newConnectionConflict, webOnlyConflict, token-env-name.ts).
|
|
36
36
|
["an agent called \"{0}\" already exists — connect it as an existing agent, or choose another name", "已經有名為「{0}」的 agent —— 請把它當作既有的 agent 連接,或換一個名稱"],
|
|
37
37
|
["there is no agent called \"{0}\"", "沒有名為「{0}」的 agent"],
|
|
38
|
+
["the General agent works in its own folder — leave the working directory empty to make \"{0}\" the General, or choose another name", "General agent 使用自己的資料夾 —— 若要讓「{0}」成為 General,請把工作目錄留空,或換一個名稱"],
|
|
39
|
+
["the name \"{0}\" is kept for the fleet's General agent — choose another name", "「{0}」這個名稱保留給 fleet 的 General agent —— 請換一個名稱"],
|
|
38
40
|
["a connection called \"{0}\" already exists — a new connection never replaces one", "已經有名為「{0}」的連線 —— 新的連線不會取代既有的連線"],
|
|
39
41
|
["this fleet already has a chat connection — add the agent with New agent instead", "這個 fleet 已經有聊天連線 —— 請改用「新增 Agent」加入 agent"],
|
|
40
42
|
["{0} is reserved", "{0} 是保留名稱"],
|
package/dist/web-api.js
CHANGED
|
@@ -450,7 +450,7 @@ function handleAppIcon(req, res, url, ctx, method) {
|
|
|
450
450
|
}
|
|
451
451
|
/**
|
|
452
452
|
* #2636: Office share links — GET /ui/office-share (the open ones, and whether this fleet offers them), POST { office,
|
|
453
|
-
* minutes?, text? } (a new one; answers its URL once its tunnel is up, or why not), DELETE /ui/office-share/<id|all>.
|
|
453
|
+
* minutes?, text?, theme? } (a new one; #2715 theme "auto" | "light" | "dark"; answers its URL once its tunnel is up, or why not), DELETE /ui/office-share/<id|all>.
|
|
454
454
|
* An admin session only — on the dashboard reached directly, or (the user, #2636 follow-up) a fleet admin signed in over
|
|
455
455
|
* the /web public link: the same check either way. Never the CLI token, a read-only session or a guest. The share's own
|
|
456
456
|
* tunnel and gateway stay apart from /web's. The person is asked again once the body is in.
|
|
@@ -515,12 +515,13 @@ function handleOfficeShare(req, res, url, ctx, path, method) {
|
|
|
515
515
|
return;
|
|
516
516
|
}
|
|
517
517
|
if (!body || typeof body !== "object" || Array.isArray(body) || typeof body.office !== "string"
|
|
518
|
-
|| (body.minutes !== undefined && typeof body.minutes !== "number") || (body.text !== undefined && typeof body.text !== "boolean")
|
|
519
|
-
|
|
518
|
+
|| (body.minutes !== undefined && typeof body.minutes !== "number") || (body.text !== undefined && typeof body.text !== "boolean")
|
|
519
|
+
|| (body.theme !== undefined && typeof body.theme !== "string")) {
|
|
520
|
+
json(res, 400, { error: "office (string), minutes? (number), text? (boolean), theme? (auto, light or dark)" });
|
|
520
521
|
return;
|
|
521
522
|
}
|
|
522
523
|
const made = await service.create({ office: body.office, owner: now.person, ...(typeof body.minutes === "number" ? { minutes: body.minutes } : {}),
|
|
523
|
-
...(typeof body.text === "boolean" ? { text: body.text } : {}) });
|
|
524
|
+
...(typeof body.text === "boolean" ? { text: body.text } : {}), ...(typeof body.theme === "string" ? { theme: body.theme } : {}) });
|
|
524
525
|
if (!made.ok) {
|
|
525
526
|
const status = made.error === "disabled" ? 404 : made.error === "tunnel" ? 503 : made.error === "too_many" ? 409 : 400;
|
|
526
527
|
json(res, status, { error: made.error, ...(made.reason ? { reason: made.reason } : {}) });
|
|
@@ -2484,6 +2485,75 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2484
2485
|
});
|
|
2485
2486
|
return true;
|
|
2486
2487
|
}
|
|
2488
|
+
// #2716: PUT /ui/office-style { skin }: the whole-fleet office's style for every viewer, device and share link (a skin
|
|
2489
|
+
// id, or null for the default; read back as GET /ui/offices's autoSkin). Who may save the offices may set it, as they
|
|
2490
|
+
// would: an admin session, never the CLI token or the public link; asked again once the body is in; counted with the
|
|
2491
|
+
// offices' saves. Every page hears `offices` and reads again.
|
|
2492
|
+
if (method === "PUT" && path === "/ui/office-style") {
|
|
2493
|
+
const store = ctx.officeStore;
|
|
2494
|
+
if (!store) {
|
|
2495
|
+
json(res, 404, { error: "Offices are not kept on this fleet" });
|
|
2496
|
+
return true;
|
|
2497
|
+
}
|
|
2498
|
+
if (gatewayRequestContext(req)) {
|
|
2499
|
+
json(res, 403, { error: "offices_not_over_public_link" });
|
|
2500
|
+
return true;
|
|
2501
|
+
}
|
|
2502
|
+
const who = () => {
|
|
2503
|
+
const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
2504
|
+
if (verdict.kind !== "allow")
|
|
2505
|
+
return { status: 401 };
|
|
2506
|
+
return verdict.via === "session" && verdict.session?.tier === "admin" ? { person: verdict.session.owner ? webPersonKey(verdict.session.owner) : "admin-session" } : { status: 403 };
|
|
2507
|
+
};
|
|
2508
|
+
const refuse = (status) => json(res, status, { error: status === 401 ? WEB_TOKEN_INVALID_MESSAGE : "offices_need_an_admin_session" });
|
|
2509
|
+
const first = who();
|
|
2510
|
+
if ("status" in first) {
|
|
2511
|
+
refuse(first.status);
|
|
2512
|
+
return true;
|
|
2513
|
+
}
|
|
2514
|
+
(async () => {
|
|
2515
|
+
let body;
|
|
2516
|
+
try {
|
|
2517
|
+
body = await parseBody(req);
|
|
2518
|
+
}
|
|
2519
|
+
catch {
|
|
2520
|
+
json(res, 400, { error: "Invalid JSON" });
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
if (!permitWebContinuation(req, res, ctx))
|
|
2524
|
+
return;
|
|
2525
|
+
const now = who();
|
|
2526
|
+
if ("status" in now) {
|
|
2527
|
+
refuse(now.status);
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !("skin" in body) || Object.keys(body).some(k => k !== "skin")) {
|
|
2531
|
+
json(res, 400, { error: "office_style_invalid", detail: "{ skin: a skin id, or null }" });
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
const limiter = ctx.officeWrites, allowed = limiter ? limiter.check(now.person) : { ok: true };
|
|
2535
|
+
if (!allowed.ok) {
|
|
2536
|
+
res.setHeader("Retry-After", String(allowed.retry_after_s));
|
|
2537
|
+
json(res, 429, { error: "offices_rate_limited", retry_after_s: allowed.retry_after_s });
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
const r = store.setAutoSkin(body.skin);
|
|
2541
|
+
if (!r.ok) {
|
|
2542
|
+
json(res, r.status, { error: r.error, ...(r.detail ? { detail: r.detail } : {}) });
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
limiter?.record(now.person);
|
|
2546
|
+
ctx.emitSseEvent("offices", { rev: r.rev });
|
|
2547
|
+
json(res, 200, { rev: r.rev, autoSkin: r.autoSkin });
|
|
2548
|
+
})().catch(err => {
|
|
2549
|
+
ctx.logger.error({ err: err.message }, "Saving the office style failed");
|
|
2550
|
+
try {
|
|
2551
|
+
json(res, 500, { error: "Saving the office style failed" });
|
|
2552
|
+
}
|
|
2553
|
+
catch { /* already answered */ }
|
|
2554
|
+
});
|
|
2555
|
+
return true;
|
|
2556
|
+
}
|
|
2487
2557
|
// #2578: GET /ui/office-acts { acts }: the agents' live acts (office_set), read as the offices are (the public link
|
|
2488
2558
|
// too, which has no stream); every change also comes as `office_acts` on the stream. Nothing here writes one.
|
|
2489
2559
|
if (method === "GET" && path === "/ui/office-acts") {
|