@songsid/agend 2.3.2-beta.11 → 2.3.2-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +4 -0
  2. package/README.zh-TW.md +4 -0
  3. package/dist/auth-api.js +2 -0
  4. package/dist/auth-api.js.map +1 -1
  5. package/dist/backend/kiro-identity.d.ts +55 -0
  6. package/dist/backend/kiro-identity.js +218 -28
  7. package/dist/backend/kiro-identity.js.map +1 -1
  8. package/dist/backend/kiro.d.ts +2 -0
  9. package/dist/backend/kiro.js +25 -4
  10. package/dist/backend/kiro.js.map +1 -1
  11. package/dist/fleet-manager.js +2 -0
  12. package/dist/fleet-manager.js.map +1 -1
  13. package/dist/general-knowledge/skills/docker-install/SKILL.md +25 -0
  14. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +3 -0
  15. package/dist/general-knowledge/skills/web-appearance/SKILL.md +3 -0
  16. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +15 -1
  17. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +15 -1
  18. package/dist/kiro-migrate.d.ts +5 -0
  19. package/dist/kiro-migrate.js +42 -4
  20. package/dist/kiro-migrate.js.map +1 -1
  21. package/dist/locale.js +28 -0
  22. package/dist/locale.js.map +1 -1
  23. package/dist/quickstart-api.d.ts +14 -0
  24. package/dist/quickstart-api.js +62 -11
  25. package/dist/quickstart-api.js.map +1 -1
  26. package/dist/settings-effect.js +5 -4
  27. package/dist/settings-effect.js.map +1 -1
  28. package/dist/setup-form.d.ts +2 -2
  29. package/dist/setup-form.js +124 -92
  30. package/dist/setup-form.js.map +1 -1
  31. package/dist/setup-i18n.d.ts +25 -0
  32. package/dist/setup-i18n.js +224 -0
  33. package/dist/setup-i18n.js.map +1 -0
  34. package/dist/ui/backend-signin.js +4 -3
  35. package/dist/ui/fleet-office-pack.d.ts +3 -2
  36. package/dist/ui/fleet-office-pack.js +16 -4
  37. package/dist/ui/fleet-office-zones.js +28 -4
  38. package/dist/ui/fleet-office.js +98 -14
  39. package/dist/ui/panel-settings.js +4 -2
  40. package/dist/ui/settings-discord.js +3 -2
  41. package/dist/ui/settings-strings.js +8 -2
  42. package/dist/ui/settings-token.js +3 -2
  43. package/dist/ui/settings-wizard.js +11 -6
  44. package/dist/ui/shared/setup-host-text.js +105 -0
  45. package/dist/ui/structured-signin.js +89 -38
  46. package/dist/web-office-store.d.ts +16 -0
  47. package/dist/web-office-store.js +44 -10
  48. package/dist/web-office-store.js.map +1 -1
  49. package/package.json +1 -1
  50. package/templates/windows-shortcuts.ps1 +18 -17
@@ -0,0 +1,224 @@
1
+ /**
2
+ * #2726: the setup pages in English and 繁體中文.
3
+ *
4
+ * Both pages are inline (setup-form.ts) and run before any fleet — no dashboard, no /assets — so they carry their own
5
+ * small dictionary and the few lines that use it (SETUP_I18N_RUNTIME). The choice works as the dashboard's does
6
+ * (src/ui/shared/app-i18n.js): the language chosen in this browser (`agend_lang`, the dashboard's own key, so the
7
+ * dashboard opened afterwards on this address keeps it), else the browser's language; a toggle on the page changes it.
8
+ * The wording is the dashboard's where it already says the same thing (settings-strings.js wizard*).
9
+ *
10
+ * What the host says (its errors) is English, or a code: src/ui/shared/setup-host-text.js says each one the page can show
11
+ * in both languages — the same module the dashboard's Setup wizard uses. One it does not know is shown as it came.
12
+ */
13
+ import { readFileSync } from "node:fs";
14
+ /** The code-entry page: before the code, nothing about this machine (setup-form.ts SETUP_CODE_PAGE_HTML). */
15
+ export const SETUP_CODE_STRINGS = {
16
+ en: {
17
+ title: "Set up AgEnD",
18
+ otherLang: "中文",
19
+ langTitle: "Language",
20
+ lead: "Enter the setup code shown in the terminal where you ran {0}.",
21
+ continue: "Continue",
22
+ unreachable: "Could not reach the setup page.",
23
+ notAccepted: "That code was not accepted.",
24
+ },
25
+ "zh-TW": {
26
+ title: "設定 AgEnD",
27
+ otherLang: "English",
28
+ langTitle: "語言",
29
+ lead: "請輸入執行 {0} 的終端機上顯示的設定代碼。",
30
+ continue: "繼續",
31
+ unreachable: "無法連到設定頁。",
32
+ notAccepted: "這個代碼不正確。",
33
+ },
34
+ };
35
+ /** The setup form (setup-form.ts SETUP_FORM_HTML). */
36
+ export const SETUP_FORM_STRINGS = {
37
+ en: {
38
+ title: "Set up AgEnD",
39
+ otherLang: "中文",
40
+ langTitle: "Language",
41
+ intro: "This page is open only while setup is running. It closes itself when you finish, or after 15 minutes.",
42
+ signinsLabel: "AI backend sign-ins",
43
+ stepAgent: "1 · Agent",
44
+ backend: "Backend",
45
+ backendFound: "Found on this host: {0}",
46
+ backendNone: "No backend CLI found on this host — install one first.",
47
+ dir: "Working directory (optional)",
48
+ dirPlaceholder: "/home/you/projects/app",
49
+ dirDefault: "Leave it empty and the agent gets its own folder, {0}, which AgEnD creates. A path you enter must be absolute.",
50
+ dirDefaultName: "Leave it empty and the agent gets its own folder in AgEnD's workspaces, which AgEnD creates. A path you enter must be absolute.",
51
+ name: "First agent name",
52
+ stepPlatform: "2 · Platform",
53
+ webOnly: "Web only (no bot)",
54
+ hintTelegram: "Create a bot with @BotFather, then add it to a group.",
55
+ hintDiscord: "Create an application in the Discord developer portal and invite the bot to your server.",
56
+ hintWeb: "No bot: you use AgEnD from its web dashboard (chat, sign-ins, settings). Discord or Telegram can be added later in Settings.",
57
+ stepCredentials: "3 · Credentials",
58
+ token: "Bot token",
59
+ tokenNote: "Written to ~/.agend/.env and never shown again.",
60
+ tokenEnv: "Environment variable",
61
+ verify: "Verify",
62
+ verifying: "Asking the provider…",
63
+ verified: "verified",
64
+ rejected: "rejected",
65
+ guild: "Server (guild)",
66
+ general: "General channel id",
67
+ discordAdmin: "Your Discord user id",
68
+ telegramAdmin: "Your Telegram user id",
69
+ firstAllowed: "Becomes the first allowed user.",
70
+ group: "Group id",
71
+ groupHint: "Or post any message in the group and press Detect.",
72
+ detect: "Detect from a message",
73
+ detectWaiting: "Waiting for a message in the group…",
74
+ detectFound: "Found.",
75
+ detectNone: "No group message yet — post one and press Detect again.",
76
+ failed: "Failed.",
77
+ stepReview: "4 · Review and start",
78
+ preview: "Show what will be written",
79
+ finish: "Create and start AgEnD",
80
+ start: "Start AgEnD",
81
+ reopen: "Reopen it where you started setup: in the Windows installer, press Enter in its window; otherwise run agend setup again on the computer running AgEnD.",
82
+ closed: "This setup page has closed.",
83
+ closedFinish: "This setup page has closed. {0} Backends you already signed in stay signed in.",
84
+ closesIn: "This page closes in {0} at the latest (sooner after 10 minutes without activity).",
85
+ closesSoon: "{0} Finish soon. If it closes first: {1}",
86
+ approveHost: "Approve this on the computer running AgEnD. The Windows installer opens a confirmation window for it, and shows a command to paste if no window appears. Otherwise run there: agend settings confirm {0}",
87
+ confirmGone: "Setup confirmation is no longer available. Review and enter the token again.",
88
+ notApplied: "Change was not applied. Review and enter the token again.",
89
+ confirmed: "Configuration confirmed. Select Start AgEnD to finish setup.",
90
+ lostContact: "Lost contact with the setup page; still trying. If this stays, the page has closed. {0}",
91
+ finishSignins: "Finish backend sign-in or choose Sign in later before continuing.",
92
+ notCurrent: "Confirmation is no longer current.",
93
+ startingWeb: "AgEnD is starting. Its dashboard link shows here once it is up.",
94
+ startingChat: "AgEnD is starting. Continue in the channel you configured.",
95
+ requesting: "Requesting host confirmation…",
96
+ expectedPending: "Expected a pending host confirmation.",
97
+ connectionFailed: "Connection failed. Retry to find the same confirmation.",
98
+ upWeb: "AgEnD is up. Open its dashboard: {0} — sign in with the code from agend web --code on the machine running AgEnD. Sign your agents' CLIs in from Settings → Agents → Sign-ins.",
99
+ upChat: "AgEnD is up. Talk to it in the channel you just set up, or open its dashboard: {0} (sign in with agend web --code on the machine running AgEnD, or send /web to your bot).",
100
+ notUp: "AgEnD has not come up within 60 seconds. Run `agend start` on the machine itself and check fleet.log — this page cannot tell you any more.",
101
+ // A host confirmation's outcome (settings-confirmation.ts reason_code; the words of locale.ts settings_confirm.result.*).
102
+ outcome_apply_failed: "Change could not be applied. Review the configuration and try again.",
103
+ outcome_fleet_stopped: "Fleet stopped. Review and apply again.",
104
+ outcome_expired: "Confirmation expired. Review and apply again.",
105
+ outcome_withdrawn: "Change withdrawn.",
106
+ outcome_rejected: "Change rejected.",
107
+ outcome_request_no_longer_current: "The requesting session or fleet changed. Review and apply again.",
108
+ outcome_settings_changed: "Settings changed since this request. Review and apply again.",
109
+ },
110
+ "zh-TW": {
111
+ title: "設定 AgEnD",
112
+ otherLang: "English",
113
+ langTitle: "語言",
114
+ intro: "這個頁面只在設定進行時開啟。完成設定,或 15 分鐘後,它會自動關閉。",
115
+ signinsLabel: "AI 後端登入",
116
+ stepAgent: "1 · Agent",
117
+ backend: "後端",
118
+ backendFound: "在這台主機上找到:{0}",
119
+ backendNone: "這台主機上找不到任何後端 CLI —— 請先安裝一個。",
120
+ dir: "工作目錄(選填)",
121
+ dirPlaceholder: "/home/you/projects/app",
122
+ dirDefault: "留空的話,agent 會有自己的資料夾 {0},由 AgEnD 建立。若要填寫,必須是絕對路徑。",
123
+ dirDefaultName: "留空的話,agent 會在 AgEnD 的 workspaces 裡有自己的資料夾,由 AgEnD 建立。若要填寫,必須是絕對路徑。",
124
+ name: "第一個 agent 名稱",
125
+ stepPlatform: "2 · 平台",
126
+ webOnly: "只用網頁(不接 bot)",
127
+ hintTelegram: "用 @BotFather 建立 bot,然後把它加進一個群組。",
128
+ hintDiscord: "在 Discord 開發者後台建立應用程式,並把 bot 邀請到你的伺服器。",
129
+ hintWeb: "不接 bot:從 AgEnD 的網頁儀表板使用它(聊天、登入、設定)。之後可以在設定裡再加上 Discord 或 Telegram。",
130
+ stepCredentials: "3 · 憑證",
131
+ token: "Bot token",
132
+ tokenNote: "會寫入 ~/.agend/.env,之後不會再顯示。",
133
+ tokenEnv: "環境變數",
134
+ verify: "驗證",
135
+ verifying: "正在向平台確認…",
136
+ verified: "已驗證",
137
+ rejected: "被拒絕",
138
+ guild: "伺服器(guild)",
139
+ general: "General 頻道 id",
140
+ discordAdmin: "你的 Discord 使用者 ID",
141
+ telegramAdmin: "你的 Telegram 使用者 ID",
142
+ firstAllowed: "會成為第一個允許使用者。",
143
+ group: "群組 id",
144
+ groupHint: "或在群組裡發任一則訊息,然後按「從訊息偵測」。",
145
+ detect: "從訊息偵測",
146
+ detectWaiting: "等待群組訊息中…",
147
+ detectFound: "找到了。",
148
+ detectNone: "還沒收到群組訊息 —— 發一則之後再按一次。",
149
+ failed: "失敗。",
150
+ stepReview: "4 · 檢查並啟動",
151
+ preview: "顯示將寫入的內容",
152
+ finish: "建立並啟動 AgEnD",
153
+ start: "啟動 AgEnD",
154
+ reopen: "請在開始設定的地方重新開啟:Windows 安裝程式請在它的視窗按 Enter;否則在執行 AgEnD 的電腦上再執行一次 agend setup。",
155
+ closed: "這個設定頁已關閉。",
156
+ closedFinish: "這個設定頁已關閉。{0}已經登入的後端會維持登入。",
157
+ closesIn: "這個頁面最晚在 {0} 後關閉(閒置 10 分鐘會更早關閉)。",
158
+ closesSoon: "{0}請盡快完成。如果它先關閉了:{1}",
159
+ approveHost: "請在執行 AgEnD 的電腦上核准。Windows 安裝程式會為它開一個確認視窗;沒有出現視窗時,會顯示一行可以貼上的指令。否則請在那台電腦上執行:agend settings confirm {0}",
160
+ confirmGone: "設定確認已不存在。請重新檢查並再輸入一次 token。",
161
+ notApplied: "變更沒有套用。請重新檢查並再輸入一次 token。",
162
+ confirmed: "設定已確認。請按「啟動 AgEnD」完成設定。",
163
+ lostContact: "與設定頁失去連線,仍在重試。如果一直如此,表示頁面已關閉。{0}",
164
+ finishSignins: "請先完成後端登入,或選擇「稍後登入」再繼續。",
165
+ notCurrent: "這個確認已不是最新的。",
166
+ startingWeb: "AgEnD 正在啟動。啟動後,儀表板連結會顯示在這裡。",
167
+ startingChat: "AgEnD 正在啟動。請到你設定的頻道繼續。",
168
+ requesting: "正在請主機確認…",
169
+ expectedPending: "預期會有一個等待主機確認的變更。",
170
+ connectionFailed: "連線失敗。重試會找回同一個確認。",
171
+ upWeb: "AgEnD 已啟動。打開它的儀表板:{0} —— 用執行 AgEnD 的電腦上 agend web --code 顯示的代碼登入。agent 的 CLI 請在 設定 → Agents → 登入 裡登入。",
172
+ upChat: "AgEnD 已啟動。在你剛設定的頻道和它對話,或打開它的儀表板:{0}(在執行 AgEnD 的電腦上用 agend web --code 登入,或對你的 bot 傳 /web)。",
173
+ notUp: "AgEnD 在 60 秒內沒有啟動。請在那台電腦上執行 `agend start` 並查看 fleet.log —— 這個頁面無法告訴你更多。",
174
+ outcome_apply_failed: "變更無法套用。請檢查設定後再試一次。",
175
+ outcome_fleet_stopped: "Fleet 已停止。請重新檢查後再套用。",
176
+ outcome_expired: "確認已過期。請重新檢查後再套用。",
177
+ outcome_withdrawn: "變更已撤回。",
178
+ outcome_rejected: "變更已被拒絕。",
179
+ outcome_request_no_longer_current: "提出請求的工作階段或 fleet 已改變。請重新檢查後再套用。",
180
+ outcome_settings_changed: "提出請求後設定已被修改。請重新檢查後再套用。",
181
+ },
182
+ };
183
+ /** #2726: the host's words and codes in both languages — one module, shared with the dashboard (pasted in below). */
184
+ const HOST_TEXT = readFileSync(new URL("./ui/shared/setup-host-text.js", import.meta.url), "utf8").replace(/^export /gm, "");
185
+ /**
186
+ * The few lines both pages run first: the dictionary (JSON, `<` escaped so no string can end the script), the language,
187
+ * `tr` (a key in it), `trHost` (a host message in it), `show` (a text that follows the language when it changes) and
188
+ * `applyLang`. Plain script, no `${` and no template literal: it is pasted into the pages' own template strings.
189
+ */
190
+ export function setupI18nRuntime(strings) {
191
+ const tables = JSON.stringify({ en: strings.en, "zh-TW": strings["zh-TW"] }).replace(/</g, "\\u003c");
192
+ return "const I18N = " + tables + ";\n" + HOST_TEXT + String.raw `
193
+ let LANG = (() => {
194
+ try { const chosen = localStorage.getItem("agend_lang"); if (chosen === "en" || chosen === "zh-TW") return chosen; } catch { /* none kept */ }
195
+ return typeof navigator !== "undefined" && /^zh/i.test(navigator.language || "") ? "zh-TW" : "en";
196
+ })();
197
+ const tr = (key, ...values) => {
198
+ let text = I18N[LANG][key] ?? I18N.en[key] ?? key;
199
+ values.forEach((value, i) => { text = text.split("{" + i + "}").join(String(value)); });
200
+ return text;
201
+ };
202
+ // The host's own words or code in this page's language (setup-host-text.js); anything else as it came.
203
+ const trHost = (text) => hostText(text, LANG);
204
+ // A text that is redrawn in the new language when it changes: its last render per element.
205
+ const shown = new Map();
206
+ const show = (element, render) => { if (!element) return; shown.set(element, render); render(element); };
207
+ const applyLang = () => {
208
+ if (document.documentElement) document.documentElement.lang = LANG === "zh-TW" ? "zh-Hant" : "en";
209
+ document.title = tr("title");
210
+ for (const node of document.querySelectorAll("[data-i18n]")) { const key = node.getAttribute && node.getAttribute("data-i18n"); if (key) node.textContent = tr(key); }
211
+ for (const node of document.querySelectorAll("[data-i18n-placeholder]")) { const key = node.getAttribute && node.getAttribute("data-i18n-placeholder"); if (key) node.placeholder = tr(key); }
212
+ for (const node of document.querySelectorAll("[data-i18n-label]")) { const key = node.getAttribute && node.getAttribute("data-i18n-label"); if (key) node.setAttribute("aria-label", tr(key)); }
213
+ for (const [element, render] of shown) render(element);
214
+ };
215
+ const langButton = document.getElementById("lang");
216
+ if (langButton) langButton.onclick = () => {
217
+ LANG = LANG === "zh-TW" ? "en" : "zh-TW";
218
+ try { localStorage.setItem("agend_lang", LANG); } catch { /* this page only */ }
219
+ applyLang();
220
+ };
221
+ applyLang();
222
+ `;
223
+ }
224
+ //# sourceMappingURL=setup-i18n.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-i18n.js","sourceRoot":"","sources":["../src/setup-i18n.ts"],"names":[],"mappings":"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"}
@@ -6,7 +6,7 @@
6
6
  // one runs, on which device, and may cancel it. The stream's `backend_login` event carries nothing — it only says to
7
7
  // read again. Starting is two steps, as in chat: check (the server keeps what it found) and start (with its id).
8
8
  import { html, useEffect, useState, useRef } from "/assets/app-html.js";
9
- import { t, register } from "/assets/app-i18n.js";
9
+ import { t, register, lang, onLang } from "/assets/app-i18n.js";
10
10
  import { appStore, createStore, useStore } from "/assets/app-store.js";
11
11
  import { closeDialog, shellStore, showDialog } from "/assets/app-shell.js";
12
12
  import { Dialog } from "/assets/ui-dialog.js";
@@ -217,13 +217,14 @@ export function BrowserSignInDialog({ backend, backends, onClose }) {
217
217
  const root = useRef(null), close = onClose || closeDialog;
218
218
  useEffect(() => {
219
219
  let cards, last = "";
220
- cards = mountStructuredSignIns(root.current, { endpoint: "/ui/browser-backend-signins", changed: next => {
220
+ cards = mountStructuredSignIns(root.current, { endpoint: "/ui/browser-backend-signins", lang, changed: next => {
221
221
  if (backends) { if (backends.some(b => !next.selected.includes(b))) void cards.addBackends(backends); }
222
222
  else if (!next.selected.includes(backend)) void cards.addBackend(backend);
223
223
  const signature = JSON.stringify([next.login, next.pending]);
224
224
  if (signature !== last) { last = signature; void refreshSignIn(); }
225
225
  } });
226
- return () => cards.dispose();
226
+ const unlisten = onLang(() => cards.relabel());
227
+ return () => { unlisten(); cards.dispose(); };
227
228
  }, [backend, backends]);
228
229
  const fallback = async (chosen = backend) => {
229
230
  const result = await post("/ui/browser-backend-signins", { operation: "cancel" });
@@ -6,8 +6,9 @@ export interface PackPoint { x: number; y: number }
6
6
  /** #2691: a seat reserved for `for` (lent while they are all elsewhere when `lend`); a plain seat has neither. */
7
7
  export interface PackSeat extends PackPoint { for?: string[]; lend?: boolean }
8
8
  export interface PackArea { name: string; rect: { x: number; y: number; w: number; h: number }; seats: PackSeat[] }
9
- /** `label` (#2574): its name tag, or null. */
10
- export interface PackFurniture { id: string; sprite: PackStrip; x: number; y: number; depth: number | "y"; activeWhen: string[] | null; activeSprite: PackStrip | null; label: string | null }
9
+ /** `label` (#2574): its name tag, or null. `activeOn`/`hides` (#2698): when it turns active, and whether it hides its people then. */
10
+ export interface PackFurniture { id: string; sprite: PackStrip; x: number; y: number; depth: number | "y"; activeWhen: string[] | null; activeSprite: PackStrip | null;
11
+ activeFor?: string[] | null; loop?: boolean; activeOn?: "state" | "seated"; hides?: boolean; label: string | null }
11
12
  export interface CheckedPack {
12
13
  format: 1; id: string; scale: number; background?: string; foreground?: string; matte?: string;
13
14
  areas: Record<string, PackArea>; stateZones: Record<string, string> | null; door: PackPoint;
@@ -42,7 +42,7 @@
42
42
  // always fit (graph edges laid out across and up/down put the
43
43
  // corners on the aisles); default false
44
44
  // "furniture": [ { "id": "rack", "sprite": STRIP, "x": 1020, "y": 140, "depth": 2,
45
- // "activeWhen": "crashed", "activeSprite": STRIP, "activeFor": "atlas", "loop": true, "label": "Rack",
45
+ // "activeWhen": "crashed", "activeSprite": STRIP, "activeFor": "atlas", "loop": true, "activeOn": "seated", "hides": true, "label": "Rack",
46
46
  // "night": { "sprite": STRIP, "x": 300, "y": 420 },
47
47
  // "walk": { "walk": STRIP, "walkDown": STRIP, "walkUp": STRIP, "walkSide": STRIP } } ],
48
48
  // "label" (#2574): a name tag under it, like a person's
@@ -62,7 +62,14 @@
62
62
  // strip (or `activeSprite`); otherwise it holds the first frame —
63
63
  // or, with "loop": true (#2570), plays its strip all along.
64
64
  // "activeFor" (an instance's name, or a list): only their state
65
- // counts, not anyone's (a room several people take turns in)
65
+ // counts, not anyone's (a room several people take turns in).
66
+ // "activeOn" (#2698): "state" (the default) turns it active as the
67
+ // state changes; "seated" only once one of them in that state is
68
+ // at the seat zones gave them, not walking — back as they walk off.
69
+ // "hides": true (#2698, only with activeFor): while active, the
70
+ // people it is active for are not drawn (walking, they always
71
+ // are); their name tags and bubbles are drawn on it, in one row at
72
+ // its tag point, and hovering or clicking it hits them
66
73
  // "characters": { "default": { "working": STRIP, "idle": STRIP, "paused": STRIP, "needs": STRIP, "walk": STRIP,
67
74
  // "walkDown": STRIP, "walkUp": STRIP, "walkSide": STRIP },
68
75
  // "fox": { … } }, "default" with at least "idle" is required; a missing pose is idle.
@@ -288,17 +295,22 @@ export function checkSkinPack(x, { kept = false } = {}) {
288
295
  const fids = new Set();
289
296
  const furniture = list(x.furniture ?? [], LIMIT.furniture, "furniture").map((f, i) => {
290
297
  const at = `furniture[${i}]`;
291
- obj(f, at); keys(f, ["id", "sprite", "x", "y", "depth", "activeWhen", "activeSprite", "activeFor", "loop", "label", "night", "walk"], at);
298
+ obj(f, at); keys(f, ["id", "sprite", "x", "y", "depth", "activeWhen", "activeSprite", "activeFor", "loop", "activeOn", "hides", "label", "night", "walk"], at);
292
299
  if (!nameOk(f.id) || fids.has(f.id)) throw new Refused(`${at}.id`, "a new name of letters, digits, _ or - (not an inherited one)");
293
300
  fids.add(f.id);
294
301
  if (f.activeSprite !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeSprite`, "only with activeWhen");
295
302
  if (f.activeFor !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeFor`, "only with activeWhen");
296
303
  if (f.loop !== undefined && typeof f.loop !== "boolean") throw new Refused(`${at}.loop`, "true or false");
304
+ if (f.activeOn !== undefined && f.activeOn !== "state" && f.activeOn !== "seated") throw new Refused(`${at}.activeOn`, '"state" or "seated"');
305
+ if (f.activeOn !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeOn`, "only with activeWhen");
306
+ if (f.hides !== undefined && typeof f.hides !== "boolean") throw new Refused(`${at}.hides`, "true or false");
307
+ if (f.hides !== undefined && f.activeFor === undefined) throw new Refused(`${at}.hides`, "only with activeFor");
297
308
  if (f.label !== undefined && (typeof f.label !== "string" || !f.label.trim() || [...f.label].length > LIMIT.line || /[\u0000-\u001f\u007f]/.test(f.label))) throw new Refused(`${at}.label`, `1–${LIMIT.line} characters`);
298
309
  return { id: f.id, sprite: checkStrip(f.sprite, `${at}.sprite`, scale), x: num(f.x, 0, LOGICAL.w, `${at}.x`), y: num(f.y, 0, LOGICAL.h, `${at}.y`),
299
310
  depth: f.depth === "y" ? "y" : depth(f.depth ?? 0, `${at}.depth`), activeWhen: f.activeWhen === undefined ? null : states(f.activeWhen, `${at}.activeWhen`),
300
311
  activeSprite: f.activeSprite === undefined ? null : checkStrip(f.activeSprite, `${at}.activeSprite`, scale),
301
- activeFor: f.activeFor === undefined ? null : names(f.activeFor, `${at}.activeFor`), loop: f.loop === true, label: f.label === undefined ? null : f.label.trim(), night: nightOf(f.night, `${at}.night`, scale),
312
+ activeFor: f.activeFor === undefined ? null : names(f.activeFor, `${at}.activeFor`), loop: f.loop === true,
313
+ activeOn: f.activeOn === "seated" ? "seated" : "state", hides: f.hides === true, label: f.label === undefined ? null : f.label.trim(), night: nightOf(f.night, `${at}.night`, scale),
302
314
  walk: walkOf(f.walk, `${at}.walk`, scale) };
303
315
  }).sort((a, b) => order(a) - order(b));
304
316
  const sets = obj(x.characters, "characters");
@@ -437,14 +437,38 @@ export function linesFor(state, lang, pack, defaults) {
437
437
 
438
438
  // ── Furniture ──
439
439
 
440
- /** The furniture active now: an item whose `activeWhen` names a state someone shown is in. */
441
- export function activeFurniture(pack, states, byName = new Map()) {
440
+ /**
441
+ * The people an item with `activeFor` is active for now (#2570): those of them shown in an `activeWhen` state — with
442
+ * "activeOn": "seated" (#2698), only those `posture(name)` says are "seated" (at the seat zones gave them, not walking).
443
+ * `posture` null: no walkers, everyone at their seat.
444
+ */
445
+ function activeForNow(f, byName, posture) {
446
+ const seated = (n) => f.activeOn !== "seated" || !posture || posture(n) === "seated";
447
+ return (f.activeFor || [...byName.keys()]).filter((n) => byName.has(n) && f.activeWhen.includes(byName.get(n)) && seated(n));
448
+ }
449
+ /**
450
+ * The furniture active now: an item whose `activeWhen` names a state someone shown is in. With activeFor (#2570),
451
+ * only those people's states count; with "activeOn": "seated" (#2698), only once one of them is seated (`posture`,
452
+ * name → "walking" | "seated" | "standing"; null: everyone seated) — it turns back as they walk off.
453
+ */
454
+ export function activeFurniture(pack, states, byName = new Map(), posture = null) {
442
455
  const on = new Set();
443
456
  for (const f of (pack && pack.furniture) || []) {
444
457
  if (!f.activeWhen) continue;
445
- // activeFor (#2570): only those people's states count; else anyone's shown.
446
- const hit = f.activeFor ? f.activeFor.some((n) => byName.has(n) && f.activeWhen.includes(byName.get(n))) : f.activeWhen.some((s) => states.has(s));
458
+ const hit = f.activeFor || f.activeOn === "seated" ? activeForNow(f, byName, posture).length > 0 : f.activeWhen.some((s) => states.has(s));
447
459
  if (hit) on.add(f.id);
448
460
  }
449
461
  return on;
450
462
  }
463
+ /**
464
+ * #2698: who is drawn on an item instead of themselves (name → the item's id): for each `active` item with "hides",
465
+ * the people it is active for (activeFor's order) who are not walking — someone walking is always drawn. One item each.
466
+ */
467
+ export function hiddenBy(pack, active, byName = new Map(), posture = null) {
468
+ const out = new Map();
469
+ for (const f of (pack && pack.furniture) || []) {
470
+ if (!f.hides || !f.activeWhen || !active.has(f.id)) continue;
471
+ for (const n of activeForNow(f, byName, posture)) if (!out.has(n) && (!posture || posture(n) !== "walking")) out.set(n, f.id);
472
+ }
473
+ return out;
474
+ }
@@ -15,7 +15,8 @@
15
15
  // goes through the same view transform.
16
16
  // - Speech bubbles: now and then a short line for its state (typed out; at once with reduced motion) — the page's own
17
17
  // lines or the skin's, never anything an agent said. Furniture can react: an item with `activeWhen` animates while
18
- // anyone shown is in that state.
18
+ // anyone shown is in that state — or (#2698, "activeOn": "seated") once its person sits down; one that "hides" its
19
+ // people carries their name tags and bubbles while it is active, and hovering it hits them (furnitureAt).
19
20
  // - Each occupant is a little pixel person in a pose that loops a few frames: working (typing, the lamp on), idle (tea),
20
21
  // paused or stopped (asleep, a closed sign), needs you / crashed / looks stuck (a hand up, a blinking light).
21
22
  // - Traffic: each new message, delegation or report is an item carried from the sender's desk to the receiver's along
@@ -64,13 +65,13 @@ import { hits, needle } from "./fleet-canvas-layout.js";
64
65
  import { AUTO_ID, LOBBY, autoLayout, backingScale, coatOf, createFlights, customLayout, deskAt, desksOf, filterOk, lampOf, layoutOk, lightOf, membersOf,
65
66
  officePose, officeRooms, signOf, workOn } from "./fleet-office-model.js";
66
67
  import { BUILT_IN_SKINS, CHAR_DEPTH, DEFAULT_PAL, bubbleBox, loadCozySkin, loadPackSkin, modeOf, skinFor, tagBox } from "./fleet-office-skin.js";
67
- import { CALLING, activeFurniture, bubbleAt, bubbleRank, NIGHT, WANDER, createMail, createWalkers, isNight, linesFor, npcKey, personAt, placeBubbles, thinBubbles, walkTargets, zonesLayout } from "./fleet-office-zones.js";
68
+ import { CALLING, activeFurniture, bubbleAt, hiddenBy, bubbleRank, NIGHT, WANDER, createMail, createWalkers, isNight, linesFor, npcKey, personAt, placeBubbles, thinBubbles, walkTargets, zonesLayout } from "./fleet-office-zones.js";
68
69
  import { actsOk, createOfficeStore } from "./fleet-office-store.js";
69
70
  import { POLL_MS } from "./fleet-traffic.js";
70
71
  import { OfficeEditor, officeForSave } from "./fleet-office-editor.js";
71
72
  import { LayoutEditor, handlesOf } from "./fleet-office-layout.js";
72
73
  import { ShareButton } from "./office-share-dialog.js";
73
- import { applyOverrides, fitNight, fitSeatFor } from "./fleet-office-pack.js";
74
+ import { applyOverrides, fitNight, fitSeatFor, stripBox } from "./fleet-office-pack.js";
74
75
  import { ALL_ID, LOBBY_TILE, OV, createCrossFlights, cropToRooms, fromTile, inTile, labelAt, lobbyNodes, overviewOk, tileAt, tileBox, tileLayout, visibleTiles } from "./fleet-office-overview.js";
75
76
  import { centreOn, gestureState, gestures, miniPoint, toScreen, toWorld, wheelFactor, zoomKey, zoomTo } from "./fleet-canvas-view.js";
76
77
  import { OK_MAX, OK_MIN, clampPan, desksK, edgeMatte, edgeStrips, fitScene, layerScale, miniBox, miniFrame, openingView, pinchOffice, savedOf, stepK, viewFromSaved,
@@ -203,7 +204,7 @@ function officeScene(office, nodes, skin, zones, deskW, prevSeats, night = false
203
204
  return lay ? customLayout({ ...office, layout: lay }, nodes) : autoLayout([{ key: office.id, name: office.name, lobby: !!office.lobby, nodes }], { width: deskW });
204
205
  }
205
206
  /** What drawPart needs of a scene; its layers are painted on first draw (and again at another resolution). */
206
- function partOf(L, looks, skin, zones, walkers, flights) {
207
+ export function partOf(L, looks, skin, zones, walkers, flights) {
207
208
  const all = L.rooms.flatMap((r) => r.desks);
208
209
  const shown = new Set([...looks.values()].map((l) => l.state)), byName = new Map([...looks.values()].filter((l) => l.name).map((l) => [l.name, l.state]));
209
210
  return { L, looks, skin, zones, walkers, flights, all, desks: all.filter((d) => d.node), layers: null,
@@ -214,6 +215,43 @@ function partOf(L, looks, skin, zones, walkers, flights) {
214
215
  * (Edit layout, #2665: applyOverrides puts it on `night`), else the pack's night x/y, else its (moved) day place.
215
216
  */
216
217
  export const nightFurniture = (list) => list.map((f) => (f.night ? { ...f, sprite: f.night.sprite, activeSprite: null, x: f.night.x ?? f.x, y: f.night.y ?? f.y, asleep: true } : f));
218
+ /** #2698: where someone is at `t` against their seat: "walking"; "seated" (still, at the seat zones gave them — or no
219
+ * walker yet: drawn there); "standing" (still elsewhere: a stroll's spot, an act's point, the seat left before a walk). */
220
+ const postureAt = (p, t) => {
221
+ const seats = new Map(p.all.filter((d) => d.node).map((d) => [d.node.name, d.seat]));
222
+ return (name) => {
223
+ const at = p.walkers && p.walkers.at(name, t), s = seats.get(name);
224
+ if (!at) return "seated";
225
+ if (at.walking) return "walking";
226
+ return s && Math.abs(at.x - s.x) < 1 && Math.abs(at.y - s.y) < 1 ? "seated" : "standing";
227
+ };
228
+ };
229
+ /**
230
+ * The furniture active at `t` and who is drawn on an item instead of themselves (`hidden`: name → item id). A pack
231
+ * with no "activeOn": "seated" nor "hides" (#2698): the scene's `active`, as before, and nobody hidden; else worked out
232
+ * from where everyone is at `t` (their walkers). An item asleep at bedtime (a `night`) hides nobody.
233
+ */
234
+ export function furnitureAt(p, t) {
235
+ const pk = p.zones && p.skin && p.skin.pack, list = (pk && pk.furniture) || [];
236
+ if (!list.some((f) => f.activeOn === "seated" || f.hides)) return { active: p.active || new Set(), hidden: new Map() };
237
+ const looks = [...p.looks.values()], byName = new Map(looks.filter((l) => l.name).map((l) => [l.name, l.state]));
238
+ const posture = postureAt(p, t), active = activeFurniture(pk, new Set(looks.map((l) => l.state)), byName, posture);
239
+ const hidden = hiddenBy(pk, active, byName, posture);
240
+ if (p.night) for (const [n, id] of [...hidden]) if (list.some((f) => f.id === id && f.night)) hidden.delete(n);
241
+ return { active, hidden };
242
+ }
243
+ /** #2698: the box an item hiding its people is hit in — its active picture, down to its name tags' row. */
244
+ const hidingBox = (f) => {
245
+ const b = stripBox(f.activeSprite || f.sprite, f.x, f.y);
246
+ return { ...b, h: Math.max(b.h, f.y + 17 - b.y) };
247
+ };
248
+ /** #2698: name tags in one row centred on (x, y) — each { look, x } (its centre), TAG_GAP apart. */
249
+ const TAG_GAP = 4;
250
+ export function tagRow(ctx, x, y, looks) {
251
+ const ws = looks.map((l) => tagBox(ctx, x, y, l.label).w), total = ws.reduce((a, w) => a + w, 0) + TAG_GAP * Math.max(0, ws.length - 1);
252
+ let left = x - total / 2;
253
+ return looks.map((look, i) => { const cx = Math.round(left + ws[i] / 2); left += ws[i] + TAG_GAP; return { look, x: cx }; });
254
+ }
217
255
  /** #2596: each NPC with a `walk` where its walker is now (`at(key)`): moved there — its tag and depth with it — and, while
218
256
  * it walks, `walking` and its `heading` (its walk strip: walkStrip); asleep, or with no walker (not strolling), where it is. */
219
257
  export const strollFurniture = (list, at) => list.map((f) => {
@@ -309,7 +347,7 @@ function paintLayers(p, env, ls, tagsOff = false) {
309
347
  * hovered (its name tag shows even in a crowd); `tagsOff` (#2554: a tile too small to read them): the name tags show
310
348
  * only for the one hovered and the Find box's matches. Whether anything in it is moving (in the air, or someone walking).
311
349
  */
312
- function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer = null) {
350
+ export function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer = null) {
313
351
  const sk = p.skin;
314
352
  const ls = layerScale(a, p.L.width, p.L.height);
315
353
  if (!p.layers || p.layers.ls !== ls || p.layers.tagsOff !== tagsOff) p.layers = paintLayers(p, env, ls, tagsOff);
@@ -326,7 +364,7 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
326
364
  // Furniture under the people; the people (nearest last) with the furniture whose depth is "y" among them, by base
327
365
  // y (a tie: the item first, the person in front); furniture over them; things in the air; the pack's foreground
328
366
  // (door frames, pillars, table fronts); then what must stay readable: crowd counts and name tags (bubbles below).
329
- const t = ms > 0 ? ms : now();
367
+ const t = ms > 0 ? ms : now(), live = furnitureAt(p, t);
330
368
  const people = p.desks.map((d) => {
331
369
  const at = p.walkers.at(d.node.name, t) || { ...d.seat, walking: false };
332
370
  if (at.walking) walking = true;
@@ -339,12 +377,13 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
339
377
  // #2665: by day, each item's night place too (at night the items are there already).
340
378
  ...(p.ghosts && !p.night ? nightGhostsOf(sk.pack).map((n) => ({ y: n.y, k: 1, n })) : [])]
341
379
  .sort((x, y) => x.y - y.y || x.k - y.k);
342
- for (const f of furniture) if (f.depth !== "y" && f.depth < CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep || p.active.has(f.id), ms, env);
380
+ for (const f of furniture) if (f.depth !== "y" && f.depth < CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep || live.active.has(f.id), ms, env);
343
381
  for (const it of post) if (it.t === 0) sk.drawProp(ctx, it, ms, env); // a letter on the floor: under the people
344
382
  for (const { f, p: q, g, n } of band) {
345
- if (f) { sk.drawFurniture(ctx, f, f.asleep || p.active.has(f.id), ms, env); continue; }
383
+ if (f) { sk.drawFurniture(ctx, f, f.asleep || live.active.has(f.id), ms, env); continue; }
346
384
  if (n) { ctx.save(); ctx.globalAlpha = GHOST_ALPHA; sk.drawFurniture(ctx, n, true, 0, env); ctx.restore(); continue; }
347
385
  if (g) { ctx.save(); ctx.globalAlpha = GHOST_ALPHA; sk.drawCharacter(ctx, { x: g.x - 44, y: g.y - 53, w: 88, h: 53, seat: g }, g.look, 0, env); ctx.restore(); continue; }
386
+ if (live.hidden.has(q.look.name)) continue; // #2698: drawn as the item (never while walking)
348
387
  const box = { ...q.d, x: q.at.x - 44, y: q.at.y - 53, w: 88, h: 53, seat: { x: q.at.x, y: q.at.y } };
349
388
  // An agent's own pose (office_set, #2578) while its act lasts and nothing calls it; walking is walking.
350
389
  const posed = actPosed(p, q.look);
@@ -352,7 +391,7 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
352
391
  sk.drawCharacter(ctx, box, look, ms, env);
353
392
  if (!q.at.walking) speakers.push({ look: q.look, ...speakerAt(sk, look, q.at), area: q.d.area });
354
393
  }
355
- for (const f of furniture) if (f.depth !== "y" && f.depth >= CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep || p.active.has(f.id), ms, env);
394
+ for (const f of furniture) if (f.depth !== "y" && f.depth >= CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep || live.active.has(f.id), ms, env);
356
395
  for (const it of up) sk.drawProp(ctx, it, ms, env);
357
396
  for (const it of post) if (it.t > 0) sk.drawProp(ctx, it, ms, env);
358
397
  if (sk.drawForeground) sk.drawForeground(ctx, p.L, env);
@@ -360,13 +399,38 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
360
399
  for (const [ar, n] of p.L.crowd || []) if (p.L.areas[ar] && sk.drawCrowd) sk.drawCrowd(ctx, p.L.areas[ar], n, env);
361
400
  // In an area over capacity the name tags show on hover or focus only (#2513: a crowd's tags covered each other).
362
401
  const over = p.L.over || new Set();
402
+ const tagged = (q) => !((tagsOff || over.has(q.d.area)) && q.d.key !== hot && !q.look.match);
363
403
  for (const q of people) {
364
- if ((tagsOff || over.has(q.d.area)) && q.d.key !== hot && !q.look.match) continue;
404
+ if (live.hidden.has(q.look.name) || !tagged(q)) continue;
365
405
  sk.drawTag(ctx, q.at.x, q.at.y, q.look, env);
366
406
  tags.push(tagBox(ctx, q.at.x, q.at.y, q.look.label));
367
407
  }
368
408
  // A furniture item's own name (#2574: an NPC drawn as furniture), tagged like a person; none on a tile too small to read.
369
- if (!tagsOff) for (const f of furniture) if (f.label) { sk.drawTag(ctx, f.x, f.y, { label: f.label }, env); tags.push(tagBox(ctx, f.x, f.y, f.label)); }
409
+ // #2698: an item hiding its people carries their tags too, in one row at its tag point (its own first; each keeps
410
+ // its place in the row while another's tag is left out), and their bubbles, over its picture. The tags drawn are
411
+ // kept on the scene (`heldTags`): hitPart hits each person on theirs, past the picture's edge too (#2699 review).
412
+ const held = [];
413
+ for (const f of furniture) {
414
+ const on = people.filter((q) => live.hidden.get(q.look.name) === f.id);
415
+ if (!on.length) {
416
+ if (!tagsOff && f.label) { sk.drawTag(ctx, f.x, f.y, { label: f.label }, env); tags.push(tagBox(ctx, f.x, f.y, f.label)); }
417
+ continue;
418
+ }
419
+ const order = new Map((f.activeFor || []).map((n, i) => [n, i]));
420
+ on.sort((x, y) => (order.get(x.look.name) ?? 0) - (order.get(y.look.name) ?? 0));
421
+ const row = tagRow(ctx, f.x, f.y, [...(f.label ? [{ label: f.label }] : []), ...on.map((q) => q.look)]);
422
+ const top = f.sprite ? Math.min(f.y - 50, stripBox(f.activeSprite || f.sprite, f.x, f.y).y + 2) : f.y - 50;
423
+ for (const [i, r] of row.entries()) {
424
+ const q = f.label ? on[i - 1] : on[i];
425
+ if (q) speakers.push({ look: q.look, x: r.x + 10, y: top, area: q.d.area });
426
+ if (q ? !tagged(q) : tagsOff) continue;
427
+ sk.drawTag(ctx, r.x, f.y, r.look, env);
428
+ const box = tagBox(ctx, r.x, f.y, r.look.label);
429
+ tags.push(box);
430
+ if (q) held.push({ name: q.look.name, id: f.id, box });
431
+ }
432
+ }
433
+ p.heldTags = held;
370
434
  } else {
371
435
  for (const d of p.desks) { const look = p.looks.get(d.key); sk.drawCharacter(ctx, d, actPosed(p, look), ms, env); speakers.push({ look, x: d.x + 58, y: d.y + 12 }); }
372
436
  blit(p.layers.front);
@@ -398,10 +462,30 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
398
462
  * The person at (x, y) in a scene's units, as it draws them: on zones where they walk, in the box the skin draws them
399
463
  * in (an uploaded character's strip, anchor and size: #2513 review), nearest first; on desks, the desk.
400
464
  */
401
- function hitPart(p, x, y, t) {
465
+ export function hitPart(p, x, y, t) {
402
466
  if (!p.zones) return deskAt(p.L, x, y);
403
- return personAt(p.all, x, y, (d) => p.walkers.at(d.node.name, t),
404
- (d, at) => (p.skin.charBox && p.looks.get(d.key) ? p.skin.charBox(p.looks.get(d.key), at) : null));
467
+ // #2698: someone an item hides is hit on it, after everyone drawn as themselves: on their tag as drawPart last drew
468
+ // it (the row can run past the picture: a long label, long names), else on its picture and the tag row under it
469
+ // (several share its width, left to right in the row's order).
470
+ const { hidden } = furnitureAt(p, t);
471
+ const d = personAt(hidden.size ? p.all.filter((dk) => !dk.node || !hidden.has(dk.node.name)) : p.all, x, y, (dk) => p.walkers.at(dk.node.name, t),
472
+ (dk, at) => (p.skin.charBox && p.looks.get(dk.key) ? p.skin.charBox(p.looks.get(dk.key), at) : null));
473
+ if (d || !hidden.size) return d;
474
+ for (const h of [...(p.heldTags || [])].reverse()) {
475
+ const b = h.box;
476
+ if (hidden.get(h.name) !== h.id || x < b.x || x >= b.x + b.w || y < b.y || y >= b.y + b.h) continue;
477
+ return p.all.find((dk) => dk.node && dk.node.name === h.name) || null;
478
+ }
479
+ const sk = p.skin.pack, drawn = strollFurniture(p.night ? nightFurniture(sk.furniture) : sk.furniture, (k) => p.walkers.at(k, t));
480
+ for (const f of [...drawn].reverse()) {
481
+ if (!f.sprite || ![...hidden.values()].includes(f.id)) continue;
482
+ const b = hidingBox(f);
483
+ if (x < b.x || x >= b.x + b.w || y < b.y || y >= b.y + b.h) continue;
484
+ const order = new Map((f.activeFor || []).map((n, i) => [n, i]));
485
+ const on = p.all.filter((dk) => dk.node && hidden.get(dk.node.name) === f.id).sort((m, n) => (order.get(m.node.name) ?? 0) - (order.get(n.node.name) ?? 0));
486
+ return on[Math.min(on.length - 1, Math.floor(((x - b.x) / b.w) * on.length))] || null;
487
+ }
488
+ return null;
405
489
  }
406
490
 
407
491
  function People({ nodes, pinned, onChange }) {
@@ -9,7 +9,8 @@
9
9
  // - Not the Apply: confirming hands the staged changes to the app's runner (settings-apply.js) before the first write.
10
10
  // This panel renders that operation from the store, and finds it again when it mounts.
11
11
  import { html, useEffect, useMemo, useRef, useState } from "/assets/app-html.js";
12
- import { t } from "/assets/app-i18n.js";
12
+ import { t, lang } from "/assets/app-i18n.js";
13
+ import { hostText } from "/assets/setup-host-text.js";
13
14
  import { appStore, useStore } from "/assets/app-store.js";
14
15
  import { useLease } from "/assets/app-ctx.js";
15
16
  import { DevicePrefs, PanelHeader, PendingChanges, setTitle } from "/assets/app-shell.js";
@@ -55,7 +56,8 @@ const tn = (k, ...v) => t(`settings.${k}`, ...v);
55
56
  /** What a refused change says to a person: the server's code in words (the code itself only for one not listed). */
56
57
  const APPLY_ERRORS = new Set(["unsupported_sensitive_effect", "confirmation_diff_too_large", "session_required", "admin_required", "settings_changed",
57
58
  "request_no_longer_current", "too_many_pending_changes", "fleet_unavailable", "invalid_settings_effect", "settings_confirmation_unavailable", "failed"]);
58
- export const applyErrorText = (code) => (APPLY_ERRORS.has(code) ? tn(`applyError_${code}`) : tn("applyErrorOther", String(code)));
59
+ // #2726: a code or words of the setup host (the wizard's commit) in the page's language; anything else as it came.
60
+ export const applyErrorText = (code) => (APPLY_ERRORS.has(code) ? tn(`applyError_${code}`) : tn("applyErrorOther", hostText(String(code), lang())));
59
61
  const ICONS = { preferences: "user", look: "palette", agents: "bot", "agent-defaults": "sliders", chat: "chat", "web-dashboard": "globe", access: "key", advanced: "code" };
60
62
  const RELEASES = "https://github.com/songsid/AgEnD/releases";
61
63
 
@@ -8,7 +8,8 @@
8
8
  // Stop, the 2-minute limit or leaving the page each retire what was asked before — an answer about A never lands after
9
9
  // B, nor after A again (an older A), nor once the page is gone.
10
10
  import { html, useEffect, useRef, useState } from "/assets/app-html.js";
11
- import { t } from "/assets/app-i18n.js";
11
+ import { t, lang } from "/assets/app-i18n.js";
12
+ import { hostText } from "/assets/setup-host-text.js";
12
13
  import { useTypedText } from "/assets/text-undo.js";
13
14
  import { api } from "./settings-confirm.js";
14
15
  import { Select } from "./settings-dialogs.js";
@@ -129,6 +130,6 @@ export function DiscordServerPicker({ idPrefix, token, bot, invite, portal, guil
129
130
  ${channels && channels.list && channels.list.length
130
131
  ? html`<${Select} id=${`${idPrefix}-gen`} value=${channel} onChange=${pickChannel} options=${["", ...channels.list.map((c) => ({ value: c.id, label: `#${c.name}` }))]} />`
131
132
  : html`<input id=${`${idPrefix}-gen`} type="text" value=${channelText} onInput=${(e) => pickChannel(onChannelText(e.target.value))} />`}
132
- ${channels && channels.error ? html`<p class="feedback warning">${channels.error}</p>` : null}</div>
133
+ ${channels && channels.error ? html`<p class="feedback warning">${hostText(channels.error, lang())}</p>` : null}</div>
133
134
  </div>`;
134
135
  }