@songsid/agend 2.3.0 → 2.3.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/authority/authority-state.d.ts +4 -0
- package/dist/authority/authority-state.js +16 -10
- package/dist/authority/authority-state.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +7 -1
- package/dist/authority/session-authorizations.js +6 -4
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/authority/web-authority.d.ts +19 -6
- package/dist/authority/web-authority.js +22 -8
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/muse.d.ts +20 -2
- package/dist/backend/muse.js +154 -16
- package/dist/backend/muse.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +33 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +1 -1
- package/dist/channel/adapters/telegram.js +3 -35
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +5 -5
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +11 -0
- package/dist/cli.js +6 -3
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +42 -5
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +47 -3
- package/dist/daemon.js +380 -60
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +23 -1
- package/dist/delivery-outbox.js +114 -17
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +9 -0
- package/dist/doctor.js.map +1 -1
- package/dist/event-log.d.ts +5 -0
- package/dist/event-log.js +13 -0
- package/dist/event-log.js.map +1 -1
- package/dist/fleet-context.d.ts +1 -1
- package/dist/fleet-lock.d.ts +6 -0
- package/dist/fleet-lock.js +19 -0
- package/dist/fleet-lock.js.map +1 -1
- package/dist/fleet-manager.d.ts +174 -2
- package/dist/fleet-manager.js +678 -68
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.d.ts +1 -0
- package/dist/fleet-process-handlers.js +9 -1
- package/dist/fleet-process-handlers.js.map +1 -1
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +11 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +4 -4
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +7 -7
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +109 -57
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +110 -58
- package/dist/guest-seen-people.d.ts +38 -0
- package/dist/guest-seen-people.js +78 -0
- package/dist/guest-seen-people.js.map +1 -0
- package/dist/instance-config-impact.js +4 -1
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +2 -0
- package/dist/instance-lifecycle.js +27 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +62 -12
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -0
- package/dist/log-service.js.map +1 -1
- package/dist/mac-auto-restore.d.ts +117 -0
- package/dist/mac-auto-restore.js +294 -0
- package/dist/mac-auto-restore.js.map +1 -0
- package/dist/needs-you.js +1 -0
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +2 -2
- package/dist/outbound-handlers.js +5 -5
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +2 -0
- package/dist/outbound-schemas.js +2 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +9 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/search/search-index.d.ts +10 -2
- package/dist/search/search-index.js +17 -4
- package/dist/search/search-index.js.map +1 -1
- package/dist/service-installer.d.ts +23 -1
- package/dist/service-installer.js +40 -11
- package/dist/service-installer.js.map +1 -1
- package/dist/session-oom-stop.d.ts +45 -0
- package/dist/session-oom-stop.js +259 -0
- package/dist/session-oom-stop.js.map +1 -0
- package/dist/session-oom.d.ts +13 -0
- package/dist/session-oom.js +119 -7
- package/dist/session-oom.js.map +1 -1
- package/dist/settings-api.d.ts +27 -1
- package/dist/settings-api.js +108 -2
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +37 -3
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +5 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/spawn-gate.d.ts +2 -0
- package/dist/spawn-gate.js +17 -4
- package/dist/spawn-gate.js.map +1 -1
- package/dist/start-attempt.d.ts +15 -1
- package/dist/start-attempt.js +35 -5
- package/dist/start-attempt.js.map +1 -1
- package/dist/start-failure-chat.d.ts +41 -0
- package/dist/start-failure-chat.js +159 -0
- package/dist/start-failure-chat.js.map +1 -0
- package/dist/start-failure-notice.d.ts +4 -0
- package/dist/start-failure-notice.js +8 -0
- package/dist/start-failure-notice.js.map +1 -1
- package/dist/start-failure.d.ts +49 -0
- package/dist/start-failure.js +141 -1
- package/dist/start-failure.js.map +1 -1
- package/dist/telegram-api-root.d.ts +1 -0
- package/dist/telegram-api-root.js +40 -0
- package/dist/telegram-api-root.js.map +1 -0
- package/dist/turn-reply-guard.d.ts +25 -0
- package/dist/turn-reply-guard.js +111 -12
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +21 -3
- package/dist/ui/backend-signin.js +5 -2
- package/dist/ui/chat-ambient.js +1 -1
- package/dist/ui/chat-effects.js +4 -4
- package/dist/ui/chat-render.js +5 -2
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/chat-thread.js +2 -2
- package/dist/ui/first-run.js +2 -2
- package/dist/ui/fleet-canvas-view.js +114 -0
- package/dist/ui/fleet-canvas.js +15 -77
- package/dist/ui/fleet-office-editor.js +80 -5
- package/dist/ui/fleet-office-layout.js +197 -0
- package/dist/ui/fleet-office-model.js +6 -3
- package/dist/ui/fleet-office-pack.d.ts +39 -0
- package/dist/ui/fleet-office-pack.js +240 -29
- package/dist/ui/fleet-office-skin.js +130 -21
- package/dist/ui/fleet-office-store.js +75 -4
- package/dist/ui/fleet-office-view.js +154 -0
- package/dist/ui/fleet-office-zones.js +95 -33
- package/dist/ui/fleet-office.js +530 -94
- package/dist/ui/fleet-usage.js +7 -6
- package/dist/ui/panel-chat.js +20 -7
- package/dist/ui/panel-details.js +2 -2
- package/dist/ui/panel-fleet.js +11 -77
- package/dist/ui/panel-log.js +5 -5
- package/dist/ui/panel-settings.js +586 -164
- package/dist/ui/search-dialog.js +26 -7
- package/dist/ui/settings-appearance.js +82 -29
- package/dist/ui/settings-apply.js +42 -7
- package/dist/ui/settings-dialogs.js +68 -22
- package/dist/ui/settings-draft.js +51 -0
- package/dist/ui/settings-guests.js +244 -78
- package/dist/ui/settings-learn.js +2 -2
- package/dist/ui/settings-model.js +2 -0
- package/dist/ui/settings-registry.js +142 -0
- package/dist/ui/settings-strings.js +108 -44
- package/dist/ui/settings-web-authority.js +34 -23
- package/dist/ui/shared/app-i18n.js +14 -0
- package/dist/ui/shared/app-nav.js +10 -5
- package/dist/ui/shared/app-prefs.js +80 -2
- package/dist/ui/shared/app-route.js +39 -5
- package/dist/ui/shared/app-shell.js +80 -8
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +251 -20
- package/dist/ui/shared/app.js +3 -0
- package/dist/ui/shared/effect-stock-trust.js +1 -1
- package/dist/ui/shared/identity-color.js +4 -4
- package/dist/ui/shared/signin.js +2 -2
- package/dist/ui/shared/ui-dialog.js +6 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/usage-dialog.js +21 -11
- package/dist/ui/shared/view-strings.js +2 -0
- package/dist/ui/shared/web-authorize.js +13 -12
- package/dist/web-api.d.ts +9 -3
- package/dist/web-api.js +161 -6
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +7 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-experimental.d.ts +1 -1
- package/dist/web-experimental.js +1 -1
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-mod-agent.js +2 -2
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-office-skins.d.ts +108 -0
- package/dist/web-office-skins.js +298 -0
- package/dist/web-office-skins.js.map +1 -0
- package/dist/web-office-store.d.ts +8 -2
- package/dist/web-office-store.js +37 -7
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +6 -0
- package/dist/web-settings-patch.js +61 -4
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +4 -2
- package/dist/web-shell-routes.js +17 -7
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/working-recovery.d.ts +104 -0
- package/dist/working-recovery.js +263 -0
- package/dist/working-recovery.js.map +1 -0
- package/dist/working-state-journal.d.ts +70 -0
- package/dist/working-state-journal.js +106 -0
- package/dist/working-state-journal.js.map +1 -0
- package/package.json +1 -1
package/dist/ui/chat-effects.js
CHANGED
|
@@ -13,7 +13,7 @@ import { getAppEffects, wakeEffects, effectsLoaded, onEffectsAwake } from "./eff
|
|
|
13
13
|
|
|
14
14
|
register("chatFx", {
|
|
15
15
|
en: { stock: "Off: the page layout could not be verified ({0}). Reload and report this code if it persists.", transport: "Off: the isolated effect frame could not start. Reload to try again.", enableHere: "Turn effects on here", playMotion: "Play effects anyway", leaveSafe: "Leave safe mode", viewerShow: "Show themes, effects and widgets here", clock: "Off: the page timing clock is unavailable. Reload to try again.", gatesUnknown: "Off: the playback conditions could not be verified.", resource: "Off: this effect could not be loaded or verified. Reload to try again.", frontQueue: "Waiting for a foreground slot.", consent: "This page has older playback information. Reload to use the isolated effect player.", previewMissing: "Off: the isolated effect frame is unavailable. Reload to try again.", unsupported: "Off: this browser lacks required Canvas Worker or layout support.", calibration: "Starting the effect.", baseline: "This page has older playback information. Reload to try again.", safetyUnknown: "Stopped: this device's safety history could not be verified. Reload to check it.", rawReason: "Playback blocked: {0}", missingRow: "Playback state is unavailable (runtime_unavailable).", region: "Off: this effect does not declare the chat region.", blocked: "Placed effects are blocked for this viewer:", deviceSetting: "Open device preferences", webSetting: "Open Web settings", reloadHint: "Reload page",
|
|
16
|
-
title: "Effects", loading: "Checking effects…", empty: "No effects in this chat.", failed: "Effects stopped because their resources could not be verified. Reload the page to try again.", reload: "Reload page", pause: "Pause", resume: "Resume", remove: "Remove from chat", removeFailed: "Could not remove this effect. Reload the page to check its state.", css: "Agent foreground effects are paused while your custom CSS is on or cannot be verified.", setting: "Open
|
|
16
|
+
title: "Effects", loading: "Checking effects…", empty: "No effects in this chat.", failed: "Effects stopped because their resources could not be verified. Reload the page to try again.", reload: "Reload page", pause: "Pause", resume: "Resume", remove: "Remove from chat", removeFailed: "Could not remove this effect. Reload the page to check its state.", css: "Agent foreground effects are paused while your custom CSS is on or cannot be verified.", setting: "Open Settings → Look", running: "Running", starting: "Starting", off: "Stopped", resumeFailed: "This effect cannot resume: the page or its safety history is not ready. Reload the page or choose another effect.", queued: "Waiting for a slot", unknown: "Playback is off or unavailable here.", budget: "Paused to protect page responsiveness. Resume keeps the three-pauses safety limit.", pauseHelp: "Resume keeps this device’s automatic-pause history and three-pause safety limit.", paused: "Paused", low: "Reduced quality",
|
|
17
17
|
typing: "Waiting while you type — it plays when you leave the message box.", covered: "Waiting while a menu or panel covers the chat.",
|
|
18
18
|
dialogOpen: "Paused while this dialog is open.", wasRunning: "Was running; paused while this dialog is open.", modal: "Waiting while a dialog is open.",
|
|
19
19
|
off3: "Turned off on this device after three automatic pauses. Resume cannot restart it here.", interrupted: "Stopped: the page closed while it was playing. Resume to play it again.",
|
|
@@ -21,7 +21,7 @@ register("chatFx", {
|
|
|
21
21
|
hiddenTab: "Waiting until this tab is visible.", offscreen: "Waiting until the chat is on screen.",
|
|
22
22
|
safeMode: "Off in safe mode.", publicLink: "Off on a public link.", disabled: "Effects are switched off." },
|
|
23
23
|
"zh-TW": { stock: "已關閉:無法驗證頁面版面({0})。請重新載入;若持續發生,請回報此代碼。", transport: "已關閉:無法啟動隔離效果框架。請重新載入再試。", enableHere: "在這裡開啟效果", playMotion: "仍然播放效果", leaveSafe: "離開安全模式", viewerShow: "在這裡顯示主題、效果與小工具", clock: "已關閉:頁面計時無法使用。請重新載入再試。", gatesUnknown: "已關閉:無法確認播放條件。", resource: "已關閉:無法載入或驗證此效果。請重新載入再試。", frontQueue: "等待前景空位。", consent: "這個頁面的播放資訊已過期。請重新載入以使用隔離效果播放器。", previewMissing: "已關閉:隔離效果框架無法使用。請重新載入再試。", unsupported: "已關閉:這個瀏覽器缺少 Canvas Worker 或版面驗證支援。", calibration: "正在啟動效果。", baseline: "這個頁面的播放資訊已過期,請重新載入再試。", safetyUnknown: "已停止:無法確認這台裝置的安全紀錄。請重新載入確認。", rawReason: "播放受阻:{0}", missingRow: "無法取得播放狀態(runtime_unavailable)。", region: "已關閉:此效果未宣告聊天區域。", blocked: "放置的效果目前無法在這個畫面播放:", deviceSetting: "開啟裝置偏好設定", webSetting: "開啟網頁設定", reloadHint: "重新載入頁面",
|
|
24
|
-
title: "效果", loading: "正在確認效果…", empty: "這個聊天沒有放置效果。", failed: "無法確認效果資源,已停止播放。重新載入頁面後再試。", reload: "重新載入頁面", pause: "暫停", resume: "恢復", remove: "從聊天移除", removeFailed: "無法移除效果。請重新載入頁面確認狀態。", css: "自訂 CSS 開啟或無法確認時,代理的前景效果會暫停。", setting: "
|
|
24
|
+
title: "效果", loading: "正在確認效果…", empty: "這個聊天沒有放置效果。", failed: "無法確認效果資源,已停止播放。重新載入頁面後再試。", reload: "重新載入頁面", pause: "暫停", resume: "恢復", remove: "從聊天移除", removeFailed: "無法移除效果。請重新載入頁面確認狀態。", css: "自訂 CSS 開啟或無法確認時,代理的前景效果會暫停。", setting: "開啟設定 → 外觀", running: "播放中", starting: "啟動中", off: "已停止", resumeFailed: "頁面或安全紀錄尚未就緒,無法恢復此效果。請重新載入頁面,或選擇其他效果。", queued: "等待空位", unknown: "這裡已關閉播放,或無法播放效果。", budget: "為維持頁面順暢,已暫停效果。恢復仍保留三次暫停的安全上限。", pauseHelp: "恢復仍保留這台裝置的自動暫停紀錄與三次暫停安全上限。", paused: "已暫停", low: "降低畫質",
|
|
25
25
|
typing: "輸入時暫停,離開訊息輸入框就會播放。", covered: "選單或面板蓋住聊天時暫停。",
|
|
26
26
|
dialogOpen: "開著這個對話框時暫停。", wasRunning: "原本在播放;開著這個對話框時暫停。", modal: "有對話框開著時暫停。",
|
|
27
27
|
off3: "已自動暫停三次,這台裝置已關閉此效果,無法在這裡恢復。", interrupted: "播放時頁面被關閉而停止。按「恢復」再播放一次。",
|
|
@@ -64,7 +64,7 @@ export function effectRowText(row, { ownOpen = false, wasRunning = false, doc =
|
|
|
64
64
|
/** Fixed local actions: no server/Worker-provided destination or raw reason in a URL. */
|
|
65
65
|
export function effectReasonAction(reason) {
|
|
66
66
|
if (reason === "preview_not_consented" || reason === "preview_unavailable") return { reload: true, label: "chatFx.reloadHint" };
|
|
67
|
-
if (["disabled", "owner_css"].includes(reason)) return { href: "/settings/
|
|
67
|
+
if (["disabled", "owner_css"].includes(reason)) return { href: "/settings/look", label: "chatFx.setting" };
|
|
68
68
|
if (["transport_unavailable", "baseline_unsupported", "baseline_unavailable", "baseline_timeout", "runtime_unavailable", "resource_unavailable", "resource_timeout", "clock_unavailable", "safety_unknown"].includes(reason)) return { reload: true, label: "chatFx.reloadHint" };
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
@@ -155,7 +155,7 @@ export function ChatEffects({ name, lease, element, store, open, onClose, onPlay
|
|
|
155
155
|
return html`${!open && localViewer() && effects.length ? html`<div class="chat-fx-notice"><label><input type="checkbox" checked=${themesShown()} onChange=${e => setThemesShown(e.currentTarget.checked)} /> ${t("chatFx.viewerShow")}</label></div>` : null}${!open && state?.error && !effects.length ? html`<div class="chat-fx-notice" role="status">${effectRowText({ reason: state.error })} <${ReasonAction} reason=${state.error} /></div>` : null}${!open && state?.reason ? html`<div class="chat-fx-notice" role="status">${effectRowText({ reason: state.reason })} <${ReasonAction} reason=${state.reason} /></div>` : null}${!open && blocked.length ? html`<div class="chat-fx-notice" role="status"><strong>${t("chatFx.blocked")}</strong>
|
|
156
156
|
${blocked.map(({ p, row }) => html`<span key=${p.id}>${p.manifest.title}: ${effectRowText(row)} ${["paused", "host_budget"].includes(row?.reason) ? html`<small>${t(row.reason === "host_budget" ? "chatFx.budget" : "chatFx.pauseHelp")}</small>` : null} <${ReasonAction} reason=${row?.reason} /></span>`)}</div>` : null}${open ? html`<${Dialog} title=${t("chatFx.title")} onClose=${onClose} busy=${busy}>
|
|
157
157
|
${state?.error ? html`<p>${t("chatFx.failed")}</p><button type="button" class="btn" onClick=${() => location.reload()}>${t("chatFx.reload")}</button>` : null}
|
|
158
|
-
${css ? html`<p>${t("chatFx.css")}</p><button type="button" class="btn" onClick=${() => { onClose(); navigate("/settings/
|
|
158
|
+
${css ? html`<p>${t("chatFx.css")}</p><button type="button" class="btn" onClick=${() => { onClose(); navigate("/settings/look"); }}>${t("chatFx.setting")}</button>` : null}
|
|
159
159
|
${!effects.length && !state?.error ? html`<p>${t(state?.unavailable ? "chatFx.unknown" : state?.loading || !state ? "chatFx.loading" : "chatFx.empty")}</p>` : null}
|
|
160
160
|
${effects.map(p => { const row = rows.find(r => r.id === p.id); return html`<section key=${p.id}><h3>${p.manifest.title}</h3><p>${effectRowText(row, { ownOpen: open, wasRunning: before.current?.get(p.id) === true })}${row?.quality === "low" ? ` · ${t("chatFx.low")}` : ""}</p>
|
|
161
161
|
<${ReasonAction} reason=${row?.reason} onClose=${onClose} />
|
package/dist/ui/chat-render.js
CHANGED
|
@@ -627,7 +627,9 @@
|
|
|
627
627
|
if (!Array.isArray(attachments)) return "";
|
|
628
628
|
var cards = !!(opts && opts.htmlCards);
|
|
629
629
|
// #1589: { inlinePreviews: true } (not on the public link) plays media and offers text inline, after the file list.
|
|
630
|
-
|
|
630
|
+
// #2547: { textPreviews: true } offers the text cards (Markdown, CSV/TSV, JSON, code, text) on the public link too:
|
|
631
|
+
// the same capped, same-origin read and the same rendering (nothing in the file ever runs); media stays local.
|
|
632
|
+
var inline = !!(opts && opts.inlinePreviews), texts = inline || !!(opts && opts.textPreviews);
|
|
631
633
|
var out = [], after = [];
|
|
632
634
|
attachments.forEach(function (a) {
|
|
633
635
|
// #1565: a file from before the fleet restarted that could not be served again — its name, marked unavailable;
|
|
@@ -639,7 +641,8 @@
|
|
|
639
641
|
if (!a || typeof a.id !== "string" || !/^[0-9a-f]{32}$/.test(a.id)) return;
|
|
640
642
|
if (cards && isHtmlAttachment(a)) after.push('<div class="html-card" data-att="' + a.id + '"></div>');
|
|
641
643
|
var url = "/ui/file/" + a.id;
|
|
642
|
-
var kind =
|
|
644
|
+
var kind = texts ? attachmentPreviewType(a) : null;
|
|
645
|
+
if ((kind === "video" || kind === "audio") && !inline) kind = null;
|
|
643
646
|
if (kind === "video") after.push('<div data-trust="media" class="att-media"><video class="att-video" controls preload="metadata" src="' + url + '"></video></div>');
|
|
644
647
|
else if (kind === "audio") after.push('<div data-trust="media" class="att-media"><audio class="att-audio" controls preload="metadata" src="' + url + '"></audio></div>');
|
|
645
648
|
else if (kind) after.push('<div class="text-card" data-att="' + a.id + '" data-type="' + kind + '"></div>');
|
package/dist/ui/chat-strings.js
CHANGED
|
@@ -49,7 +49,7 @@ register("chat", {
|
|
|
49
49
|
cmd_wake: "Wake a paused instance", cmd_save: "Save the conversation to a file",
|
|
50
50
|
tourTitle: "Welcome to web chat", tourStep: "{0} of {1}", tourBack: "Back", tourNext: "Next", tourDone: "Got it", tourSkip: "Skip",
|
|
51
51
|
tour1: "Pick an instance in the list to chat with it. On a phone, ☰ opens the list.",
|
|
52
|
-
tour2: "This is the agent's conversation. If it also has a Telegram or Discord chat, web echo (Settings →
|
|
52
|
+
tour2: "This is the agent's conversation. If it also has a Telegram or Discord chat, web echo (Settings → Chat & connections) decides whether what you send here shows there too.",
|
|
53
53
|
tour3: "Attach files and images with 📎, or paste or drop them on the chat.",
|
|
54
54
|
tour4: "While the agent works, Stop reply appears in the message box: it interrupts the current reply. The instance itself keeps running.",
|
|
55
55
|
tour5: "A “needs you” badge means the agent is waiting on its terminal, for example a permission question. Answer it from the buttons in its chat.",
|
|
@@ -190,7 +190,7 @@ register("chat", {
|
|
|
190
190
|
cmd_wake: "喚醒已暫停的 instance", cmd_save: "把對話存成檔案",
|
|
191
191
|
tourTitle: "歡迎使用網頁聊天", tourStep: "{0} / {1}", tourBack: "上一步", tourNext: "下一步", tourDone: "知道了", tourSkip: "略過",
|
|
192
192
|
tour1: "在清單裡選一個 instance,就能跟它對話。手機上請按 ☰ 打開清單。",
|
|
193
|
-
tour2: "這是這個 Agent 的對話。如果它也有 Telegram 或 Discord 聊天,在這裡送出的訊息是否也出現在那裡,由「設定 →
|
|
193
|
+
tour2: "這是這個 Agent 的對話。如果它也有 Telegram 或 Discord 聊天,在這裡送出的訊息是否也出現在那裡,由「設定 → 聊天與連線」的網頁轉發決定。",
|
|
194
194
|
tour3: "按 📎 附上檔案和圖片,也可以直接貼上或拖到聊天裡。",
|
|
195
195
|
tour4: "Agent 工作時,輸入框會出現「中止回覆」:中斷目前的回覆,instance 本身會繼續運行。",
|
|
196
196
|
tour5: "看到「等你回應」標記,表示 Agent 在終端機上等你,例如權限詢問。到它的聊天裡按按鈕回應即可。",
|
package/dist/ui/chat-thread.js
CHANGED
|
@@ -14,7 +14,7 @@ import { chatTime, chatTimeFull, onDayChange } from "./chat-time.js";
|
|
|
14
14
|
import { textLinkSegments } from "/assets/app-route.js";
|
|
15
15
|
|
|
16
16
|
const R = () => globalThis.AgendChatRender;
|
|
17
|
-
/** #1589: the public link
|
|
17
|
+
/** #1589: the public link plays no media in the page (its page says so: <body data-public-link="1">); #2547: its text cards show. */
|
|
18
18
|
const PUBLIC_LINK = () => globalThis.document?.body?.dataset?.publicLink === "1";
|
|
19
19
|
/** #1589: how much of a text attachment is shown before "Show all". */
|
|
20
20
|
export const TEXT_PREVIEW_LINES = 200;
|
|
@@ -146,7 +146,7 @@ export function createThread(list, scroller, opts) {
|
|
|
146
146
|
// #2272: reply to it — a hover strip at its top right (phones: swipe left or long-press; R on a focused message).
|
|
147
147
|
const key = msgKey(x);
|
|
148
148
|
const act = opts.canReply?.() === false ? "" : `<div data-trust="thread-control" class="msg-actions"><button type="button" class="chip-btn icon-only" data-act="reply" data-arg="${escAttr(key)}" title="${escAttr(tr("chat.reply"))}" aria-label="${escAttr(tr("chat.reply"))}">${REPLY}</button></div>`;
|
|
149
|
-
return `<div class="msg ${u ? "user" : "agent"}" data-key="${escAttr(key)}" tabindex="-1">${act}<div class="meta"><span class="sender">${escAttr(x.sender)}</span>${time}${ticks}</div>${quoteHtml(x.replyTo)}<div class="body">${clicked ? clickHtml(x.click) : `<div class="md">${renderedMarkdown(x.text, x.role === "agent", opts.emojis, opts.mentions)}</div>`}${R().attachmentsHtml(x.attachments, { htmlCards: x.role === "agent", goneTitle: tr("chat.attGone"), goneLabel: tr("chat.attUnavailable"), inlinePreviews: !PUBLIC_LINK() })}${buttons}</div>${R().reactionsHtml(x.reactions, tr("chat.reactedBy", x.instance || ""))}${tools}</div>`;
|
|
149
|
+
return `<div class="msg ${u ? "user" : "agent"}" data-key="${escAttr(key)}" tabindex="-1">${act}<div class="meta"><span class="sender">${escAttr(x.sender)}</span>${time}${ticks}</div>${quoteHtml(x.replyTo)}<div class="body">${clicked ? clickHtml(x.click) : `<div class="md">${renderedMarkdown(x.text, x.role === "agent", opts.emojis, opts.mentions)}</div>`}${R().attachmentsHtml(x.attachments, { htmlCards: x.role === "agent", goneTitle: tr("chat.attGone"), goneLabel: tr("chat.attUnavailable"), inlinePreviews: !PUBLIC_LINK(), textPreviews: true })}${buttons}</div>${R().reactionsHtml(x.reactions, tr("chat.reactedBy", x.instance || ""))}${tools}</div>`;
|
|
150
150
|
}
|
|
151
151
|
/**
|
|
152
152
|
* A message this page is sending (chat-store.js): its own bubble at once, dimmed with "Sending…" until the server
|
package/dist/ui/first-run.js
CHANGED
|
@@ -5,7 +5,7 @@ import { html } from "/assets/app-html.js";
|
|
|
5
5
|
import { register, t } from "/assets/app-i18n.js";
|
|
6
6
|
import { appStore, createStore, useStore } from "/assets/app-store.js";
|
|
7
7
|
import { navigate } from "/assets/app-nav.js";
|
|
8
|
-
import {
|
|
8
|
+
import { sectionPath } from "/assets/app-route.js";
|
|
9
9
|
import { Icon } from "/assets/ui-icons.js";
|
|
10
10
|
|
|
11
11
|
register("firstRun", {
|
|
@@ -52,7 +52,7 @@ export function useFirstRun() {
|
|
|
52
52
|
export function FirstRunCard({ onConnect }) {
|
|
53
53
|
const shown = useFirstRun();
|
|
54
54
|
if (!shown) return null;
|
|
55
|
-
const connect = onConnect || (() => { local.set({ wizard: true }); navigate(
|
|
55
|
+
const connect = onConnect || (() => { local.set({ wizard: true }); navigate(sectionPath("bots")); });
|
|
56
56
|
return html`<section class="card first-run" aria-labelledby="first-run-title">
|
|
57
57
|
<h3 id="first-run-title"><${Icon} name="plug" size=${14} /> ${t("firstRun.title")}</h3>
|
|
58
58
|
<p>${t("firstRun.body")}</p>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The Fleet canvases' shared view (#1918's org chart canvas, fleet-canvas.js; #2540's Office, fleet-office.js): the pure
|
|
2
|
+
// pan and zoom math and the pointer gestures, so both feel the same. A view is { k, x, y }: a world point (wx, wy) is
|
|
3
|
+
// drawn at (x + wx·k, y + wy·k) in the viewport's CSS px.
|
|
4
|
+
//
|
|
5
|
+
// - One pointer pans once it has moved a few px (so a tap still opens what it is on); two pinch: the world point under
|
|
6
|
+
// their old midpoint goes to the new one, scaled by how their span changed (a two-finger move pans too). The pointer
|
|
7
|
+
// is captured only once it drags: a capture at once would send the tap's click to the viewport, not what was tapped.
|
|
8
|
+
// The click that ends a drag or a pinch opens nothing.
|
|
9
|
+
// - The wheel zooms about the pointer; + / − / 0 zoom in, out and fit (with no Alt, Ctrl or Meta held).
|
|
10
|
+
|
|
11
|
+
export const K_MIN = 0.2, K_MAX = 2.5;
|
|
12
|
+
/** `k` kept in [lo, hi] (the org canvas's range by default). */
|
|
13
|
+
export const clampK = (k, lo = K_MIN, hi = K_MAX) => Math.min(hi, Math.max(lo, k));
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The view that shows region `r` in a `size` viewport: as large as fits (at most `maxK`), centred across, from the top.
|
|
17
|
+
* `minK`: never smaller than this (a phone's opening); a region that is then wider than the viewport starts at its left.
|
|
18
|
+
* `widthOnly`: fit the width, let the height run on (the opening: the rest is below).
|
|
19
|
+
*/
|
|
20
|
+
export function fitView(r, size, { margin = 24, minK = K_MIN, maxK = 1.25, widthOnly = false } = {}) {
|
|
21
|
+
const across = (size.w - 2 * margin) / r.w, down = widthOnly ? Infinity : (size.h - 2 * margin) / r.h;
|
|
22
|
+
const k = clampK(Math.max(minK, Math.min(across, down, maxK)));
|
|
23
|
+
const x = r.w * k > size.w - 2 * margin ? margin - r.x * k : size.w / 2 - (r.x + r.w / 2) * k;
|
|
24
|
+
return { k, x, y: margin - r.y * k };
|
|
25
|
+
}
|
|
26
|
+
/** The view at scale `k` with the viewport point (cx, cy) on the same world point as in `v`. */
|
|
27
|
+
export function zoomTo(v, k, cx, cy) {
|
|
28
|
+
return { k, x: cx - (cx - v.x) * (k / v.k), y: cy - (cy - v.y) * (k / v.k) };
|
|
29
|
+
}
|
|
30
|
+
/** Zoom by `f` about the viewport point (cx, cy): the canvas point under it stays under it. */
|
|
31
|
+
export function zoomAt(v, f, cx, cy) { return zoomTo(v, clampK(v.k * f), cx, cy); }
|
|
32
|
+
/** A pinch step at scale `k`: the world point under the old midpoint goes to the new midpoint. */
|
|
33
|
+
export function pinchTo(v, from, to, k) {
|
|
34
|
+
const wx = (from.mid.x - v.x) / v.k, wy = (from.mid.y - v.y) / v.k;
|
|
35
|
+
return { k, x: to.mid.x - wx * k, y: to.mid.y - wy * k };
|
|
36
|
+
}
|
|
37
|
+
/** A pinch step: the canvas point under the old midpoint goes to the new midpoint, the zoom scaled by the spans' ratio. */
|
|
38
|
+
export function pinchView(v, from, to) { return pinchTo(v, from, to, clampK(v.k * (to.dist / from.dist))); }
|
|
39
|
+
/** Viewport CSS px → world units, and back. */
|
|
40
|
+
export const toWorld = (v, sx, sy) => ({ x: (sx - v.x) / v.k, y: (sy - v.y) / v.k });
|
|
41
|
+
export const toScreen = (v, wx, wy) => ({ x: v.x + wx * v.k, y: v.y + wy * v.k });
|
|
42
|
+
/** The view at the same scale with world point (wx, wy) in the middle of a `size` viewport. */
|
|
43
|
+
export const centreOn = (v, size, wx, wy) => ({ k: v.k, x: size.w / 2 - wx * v.k, y: size.h / 2 - wy * v.k });
|
|
44
|
+
/**
|
|
45
|
+
* The world point a click on an overview at `rect` (its client box) shows, for an overview of world box `box` drawn
|
|
46
|
+
* whole and centred in it (an SVG viewBox's xMidYMid meet); null when it has no size.
|
|
47
|
+
*/
|
|
48
|
+
export function miniPoint(rect, box, clientX, clientY) {
|
|
49
|
+
if (!rect || !rect.width || !rect.height) return null;
|
|
50
|
+
const scale = Math.min(rect.width / box.w, rect.height / box.h);
|
|
51
|
+
return { x: box.x + (clientX - rect.left - (rect.width - box.w * scale) / 2) / scale, y: box.y + (clientY - rect.top - (rect.height - box.h * scale) / 2) / scale };
|
|
52
|
+
}
|
|
53
|
+
/** The wheel's zoom factor (a notch is about 15%; a trackpad's small deltas zoom a little). */
|
|
54
|
+
export const wheelFactor = (e) => Math.exp(-e.deltaY * 0.0015);
|
|
55
|
+
/** What a key does to the view: "in", "out", "fit" (0), or null (any other key, or with Alt, Ctrl or Meta). */
|
|
56
|
+
export function zoomKey(e) {
|
|
57
|
+
if (e.altKey || e.ctrlKey || e.metaKey) return null;
|
|
58
|
+
if (e.key === "+" || e.key === "=") return "in";
|
|
59
|
+
if (e.key === "-" || e.key === "_") return "out";
|
|
60
|
+
if (e.key === "0") return "fit";
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── The pointer gestures: the state lives in the caller's ref (it outlives a render), the handlers are made per render ──
|
|
65
|
+
|
|
66
|
+
export const SLOP = 5;
|
|
67
|
+
/** A fresh gesture state: the pointers down, how far the one has moved, the pinch's last span, whether to swallow the click. */
|
|
68
|
+
export const gestureState = () => ({ pts: new Map(), moved: 0, span: null, swallow: false });
|
|
69
|
+
/**
|
|
70
|
+
* The handlers for state `d`: `el()` the element measured and captured; `pan(dx, dy)` and `pinch(from, to)` move the
|
|
71
|
+
* view. `move` returns whether it was a drag or a pinch (not a hover). `busy()`: a drag or pinch is under way.
|
|
72
|
+
*/
|
|
73
|
+
export function gestures(d, { el, pan, pinch }) {
|
|
74
|
+
const spanOf = () => {
|
|
75
|
+
const [a, b] = [...d.pts.values()], r = el().getBoundingClientRect();
|
|
76
|
+
return { mid: { x: (a.x + b.x) / 2 - r.left, y: (a.y + b.y) / 2 - r.top }, dist: Math.hypot(a.x - b.x, a.y - b.y) };
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
down(e) {
|
|
80
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
81
|
+
d.pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
82
|
+
if (d.pts.size === 1) { d.moved = 0; d.swallow = false; d.span = null; }
|
|
83
|
+
else if (d.pts.size === 2) { d.swallow = true; d.span = spanOf(); }
|
|
84
|
+
},
|
|
85
|
+
move(e) {
|
|
86
|
+
const prev = d.pts.get(e.pointerId);
|
|
87
|
+
if (!prev) return false;
|
|
88
|
+
d.pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
89
|
+
if (d.pts.size === 1) {
|
|
90
|
+
d.moved += Math.abs(e.clientX - prev.x) + Math.abs(e.clientY - prev.y);
|
|
91
|
+
if (d.moved < SLOP) return false;
|
|
92
|
+
if (!d.swallow) { d.swallow = true; try { el().setPointerCapture(e.pointerId); } catch { /* gone already */ } }
|
|
93
|
+
pan(e.clientX - prev.x, e.clientY - prev.y);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (d.pts.size !== 2) return true;
|
|
97
|
+
d.swallow = true;
|
|
98
|
+
const next = spanOf();
|
|
99
|
+
if (d.span && d.span.dist > 0 && next.dist > 0) pinch(d.span, next);
|
|
100
|
+
d.span = next;
|
|
101
|
+
return true;
|
|
102
|
+
},
|
|
103
|
+
up(e) {
|
|
104
|
+
d.pts.delete(e.pointerId);
|
|
105
|
+
// Back to one finger: it pans on at once (the pinch already swallows the click); two again: a new span.
|
|
106
|
+
d.span = d.pts.size === 2 ? spanOf() : null;
|
|
107
|
+
if (d.pts.size === 1) d.moved = SLOP;
|
|
108
|
+
// The click that ends a drag comes after this: let it be swallowed, then forget.
|
|
109
|
+
if (d.swallow && !d.pts.size) setTimeout(() => { if (!d.pts.size) d.swallow = false; }, 0);
|
|
110
|
+
},
|
|
111
|
+
clickCapture(e) { if (d.swallow) { e.preventDefault(); e.stopPropagation(); } },
|
|
112
|
+
busy: () => d.swallow && d.pts.size > 0,
|
|
113
|
+
};
|
|
114
|
+
}
|
package/dist/ui/fleet-canvas.js
CHANGED
|
@@ -32,6 +32,7 @@ import { Office } from "./fleet-office.js";
|
|
|
32
32
|
import { OC, REST_KEY, SUM_LINE, canvasLayout, hits, needle, openingRegion, summaryRows } from "./fleet-canvas-layout.js";
|
|
33
33
|
import { createTrafficStore, FAR_K, HEAD, POLL_MS, sendingBy, trafficEdges } from "./fleet-traffic.js";
|
|
34
34
|
import { routeScene } from "./fleet-route-scene.js";
|
|
35
|
+
import { K_MAX, K_MIN, fitView, gestureState, gestures, miniPoint, pinchView, wheelFactor, zoomAt, zoomKey } from "./fleet-canvas-view.js";
|
|
35
36
|
import { createRouteClient } from "./fleet-route.js";
|
|
36
37
|
|
|
37
38
|
register("orgc", {
|
|
@@ -62,32 +63,9 @@ const tc = (k, ...v) => t(`orgc.${k}`, ...v);
|
|
|
62
63
|
|
|
63
64
|
export { OC, REST_KEY, SUM_LINE, canvasLayout, openingRegion, summaryRows };
|
|
64
65
|
|
|
65
|
-
// ── The view: pan, zoom, fit (
|
|
66
|
+
// ── The view: pan, zoom, fit and the gestures (fleet-canvas-view.js, shared with the Office view) ──
|
|
66
67
|
|
|
67
|
-
export
|
|
68
|
-
const clampK = (k) => Math.min(K_MAX, Math.max(K_MIN, k));
|
|
69
|
-
/**
|
|
70
|
-
* The view that shows region `r` in a `size` viewport: as large as fits (at most `maxK`), centred across, from the top.
|
|
71
|
-
* `minK`: never smaller than this (a phone's opening); a region that is then wider than the viewport starts at its left.
|
|
72
|
-
* `widthOnly`: fit the width, let the height run on (the opening: the rest is below).
|
|
73
|
-
*/
|
|
74
|
-
export function fitView(r, size, { margin = 24, minK = K_MIN, maxK = 1.25, widthOnly = false } = {}) {
|
|
75
|
-
const across = (size.w - 2 * margin) / r.w, down = widthOnly ? Infinity : (size.h - 2 * margin) / r.h;
|
|
76
|
-
const k = clampK(Math.max(minK, Math.min(across, down, maxK)));
|
|
77
|
-
const x = r.w * k > size.w - 2 * margin ? margin - r.x * k : size.w / 2 - (r.x + r.w / 2) * k;
|
|
78
|
-
return { k, x, y: margin - r.y * k };
|
|
79
|
-
}
|
|
80
|
-
/** A pinch step: the canvas point under the old midpoint goes to the new midpoint, the zoom scaled by the spans' ratio. */
|
|
81
|
-
export function pinchView(v, from, to) {
|
|
82
|
-
const k = clampK(v.k * (to.dist / from.dist));
|
|
83
|
-
const wx = (from.mid.x - v.x) / v.k, wy = (from.mid.y - v.y) / v.k;
|
|
84
|
-
return { k, x: to.mid.x - wx * k, y: to.mid.y - wy * k };
|
|
85
|
-
}
|
|
86
|
-
/** Zoom by `f` about the viewport point (cx, cy): the canvas point under it stays under it. */
|
|
87
|
-
export function zoomAt(v, f, cx, cy) {
|
|
88
|
-
const k = clampK(v.k * f);
|
|
89
|
-
return { k, x: cx - (cx - v.x) * (k / v.k), y: cy - (cy - v.y) * (k / v.k) };
|
|
90
|
-
}
|
|
68
|
+
export { K_MIN, K_MAX, fitView, pinchView, zoomAt };
|
|
91
69
|
|
|
92
70
|
// ── Per device ──
|
|
93
71
|
|
|
@@ -316,7 +294,7 @@ function Canvas({ view, folded, onFold, q, jump, traffic }) {
|
|
|
316
294
|
const L = useMemo(() => canvasLayout(view, folded, { narrow, label: (s, n) => `${n} ${STATE_LABEL(s)}` }), [view, folded, narrow]);
|
|
317
295
|
const svgRef = useRef(null), worldRef = useRef(null), miniRef = useRef(null), miniViewRef = useRef(null);
|
|
318
296
|
const vp = useRef(null);
|
|
319
|
-
const drag = useRef(
|
|
297
|
+
const drag = useRef(gestureState());
|
|
320
298
|
const size = () => {
|
|
321
299
|
const r = svgRef.current ? svgRef.current.getBoundingClientRect() : null;
|
|
322
300
|
return r && r.width > 0 && r.height > 0 ? { w: r.width, h: r.height } : { w: 960, h: 600 };
|
|
@@ -364,58 +342,20 @@ function Canvas({ view, folded, onFold, q, jump, traffic }) {
|
|
|
364
342
|
});
|
|
365
343
|
|
|
366
344
|
const open = openChat;
|
|
367
|
-
// The click that ends a drag or a pinch opens nothing and folds nothing.
|
|
368
|
-
const
|
|
345
|
+
// The click that ends a drag or a pinch opens nothing and folds nothing (fleet-canvas-view.js gestures).
|
|
346
|
+
const g = gestures(drag.current, { el: () => svgRef.current, pan: (dx, dy) => set({ ...vp.current, x: vp.current.x + dx, y: vp.current.y + dy }),
|
|
347
|
+
pinch: (from, to) => set(pinchView(vp.current, from, to)) });
|
|
348
|
+
const onClickCapture = g.clickCapture;
|
|
369
349
|
const onWheel = (e) => {
|
|
370
350
|
e.preventDefault();
|
|
371
351
|
const r = svgRef.current.getBoundingClientRect();
|
|
372
|
-
set(zoomAt(vp.current,
|
|
373
|
-
};
|
|
374
|
-
// One pointer pans (after a few pixels, so a tap still opens a card); two pinch: the canvas point under their old
|
|
375
|
-
// midpoint goes to the new one, scaled by how their span changed (so a two-finger move pans too). The pointer is
|
|
376
|
-
// captured only once it drags: a capture at once would send the tap's click to the canvas, not the card.
|
|
377
|
-
const spanOf = () => {
|
|
378
|
-
const [a, b] = [...drag.current.pts.values()], r = svgRef.current.getBoundingClientRect();
|
|
379
|
-
return { mid: { x: (a.x + b.x) / 2 - r.left, y: (a.y + b.y) / 2 - r.top }, dist: Math.hypot(a.x - b.x, a.y - b.y) };
|
|
380
|
-
};
|
|
381
|
-
const onPointerDown = (e) => {
|
|
382
|
-
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
383
|
-
const d = drag.current;
|
|
384
|
-
d.pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
385
|
-
if (d.pts.size === 1) { d.moved = 0; d.swallow = false; d.span = null; }
|
|
386
|
-
else if (d.pts.size === 2) { d.swallow = true; d.span = spanOf(); }
|
|
387
|
-
};
|
|
388
|
-
const onPointerMove = (e) => {
|
|
389
|
-
const d = drag.current, prev = d.pts.get(e.pointerId);
|
|
390
|
-
if (!prev) return;
|
|
391
|
-
d.pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
392
|
-
if (d.pts.size === 1) {
|
|
393
|
-
d.moved += Math.abs(e.clientX - prev.x) + Math.abs(e.clientY - prev.y);
|
|
394
|
-
if (d.moved < 5) return;
|
|
395
|
-
if (!d.swallow) { d.swallow = true; try { svgRef.current.setPointerCapture(e.pointerId); } catch { /* gone already */ } }
|
|
396
|
-
set({ ...vp.current, x: vp.current.x + e.clientX - prev.x, y: vp.current.y + e.clientY - prev.y });
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
if (d.pts.size !== 2) return;
|
|
400
|
-
d.swallow = true;
|
|
401
|
-
const next = spanOf();
|
|
402
|
-
if (d.span && d.span.dist > 0 && next.dist > 0) set(pinchView(vp.current, d.span, next));
|
|
403
|
-
d.span = next;
|
|
404
|
-
};
|
|
405
|
-
const onPointerUp = (e) => {
|
|
406
|
-
const d = drag.current;
|
|
407
|
-
d.pts.delete(e.pointerId);
|
|
408
|
-
// Back to one finger: it pans on at once (the pinch already swallows the click); two again: a new span.
|
|
409
|
-
d.span = d.pts.size === 2 ? spanOf() : null;
|
|
410
|
-
if (d.pts.size === 1) d.moved = 5;
|
|
411
|
-
// The click that ends a drag comes after this: let it be swallowed, then forget.
|
|
412
|
-
if (d.swallow && !d.pts.size) setTimeout(() => { if (!d.pts.size) d.swallow = false; }, 0);
|
|
352
|
+
set(zoomAt(vp.current, wheelFactor(e), e.clientX - r.left, e.clientY - r.top));
|
|
413
353
|
};
|
|
354
|
+
const onPointerDown = g.down, onPointerMove = g.move, onPointerUp = g.up;
|
|
414
355
|
const onKeyDown = (e) => {
|
|
415
356
|
if (e.altKey || e.ctrlKey || e.metaKey) return;
|
|
416
|
-
|
|
417
|
-
if (e.
|
|
418
|
-
if (e.key === "0") { e.preventDefault(); fit(); return; }
|
|
357
|
+
const z = zoomKey(e);
|
|
358
|
+
if (z) { e.preventDefault(); if (z === "fit") fit(); else zoomBy(z === "in" ? 1.25 : 0.8); return; }
|
|
419
359
|
const dir = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[e.key];
|
|
420
360
|
const card = e.target && e.target.closest ? e.target.closest(".oc-node") : null;
|
|
421
361
|
if (card && (e.key === "Enter" || e.key === " ")) {
|
|
@@ -436,12 +376,10 @@ function Canvas({ view, folded, onFold, q, jump, traffic }) {
|
|
|
436
376
|
if (c) show(c, false);
|
|
437
377
|
};
|
|
438
378
|
const onMini = (e) => {
|
|
439
|
-
const
|
|
440
|
-
if (!
|
|
441
|
-
const scale = Math.min(r.width / box.w, r.height / box.h);
|
|
442
|
-
const wx = box.x + (e.clientX - r.left - (r.width - box.w * scale) / 2) / scale, wy = box.y + (e.clientY - r.top - (r.height - box.h * scale) / 2) / scale;
|
|
379
|
+
const w = miniPoint(miniRef.current.getBoundingClientRect(), miniBox(L), e.clientX, e.clientY);
|
|
380
|
+
if (!w) return;
|
|
443
381
|
const s = size(), v = vp.current;
|
|
444
|
-
set({ k: v.k, x: s.w / 2 -
|
|
382
|
+
set({ k: v.k, x: s.w / 2 - w.x * v.k, y: s.h / 2 - w.y * v.k });
|
|
445
383
|
};
|
|
446
384
|
|
|
447
385
|
const box = miniBox(L);
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// editor). An admin session adds an office, renames it, chooses its style (Desks / Studio), picks who is in it (whole
|
|
3
3
|
// teams, or one instance at a time; none: everyone) and deletes one — then saves the whole list with the rev it read
|
|
4
4
|
// (PUT /ui/offices; a change made meanwhile is a 409: read again, nothing overwritten). A stored office's layout (seats,
|
|
5
|
-
// paths)
|
|
6
|
-
//
|
|
5
|
+
// paths) and its layout overrides (Edit layout, fleet-office-layout.js) are kept as they are. Skin packs (#2513): the
|
|
6
|
+
// packs the host keeps are styles to choose; an admin uploads one here (its manifest.json and its PNGs, picked together
|
|
7
|
+
// or as a folder — sent at once, checked by the host, which keeps it whole) or deletes one. Never offered to a guest,
|
|
8
|
+
// the public link or a read-only session (the host refuses them too).
|
|
7
9
|
import { html, useMemo, useState } from "/assets/app-html.js";
|
|
8
10
|
import { t, register } from "/assets/app-i18n.js";
|
|
9
11
|
import { Dialog } from "/assets/ui-dialog.js";
|
|
@@ -22,6 +24,10 @@ register("orge", {
|
|
|
22
24
|
rateLimited: "Too many saves. Try again in a minute.",
|
|
23
25
|
failed: "The offices were not saved ({0}).", layoutKept: "Its seats and paths are kept.",
|
|
24
26
|
styleUnknown: "{0} (not available here: drawn as Desks)",
|
|
27
|
+
packs: "Skin packs", packsLead: "Styles drawn from an artist's pictures. Pick a pack's manifest.json and all its PNG files together (or its folder).",
|
|
28
|
+
packsNone: "No skin packs yet.", upload: "Upload a skin pack", uploadFolder: "Upload a folder", uploading: "Uploading…", uploaded: "Skin pack “{0}” uploaded.",
|
|
29
|
+
uploadFailed: "The skin pack was not uploaded: {0}", noManifest: "no manifest.json among the files", badManifest: "manifest.json is not JSON",
|
|
30
|
+
removePack: "Delete {0}", removedPack: "Skin pack “{0}” deleted.", removeFailed: "The skin pack was not deleted ({0}).",
|
|
25
31
|
},
|
|
26
32
|
"zh-TW": {
|
|
27
33
|
edit: "編輯辦公室", title: "辦公室", lead: "這個 fleet 的每個人在辦公室檢視中都會看到這些辦公室。沒有選任何成員的辦公室會顯示全部。",
|
|
@@ -34,12 +40,79 @@ register("orge", {
|
|
|
34
40
|
rateLimited: "儲存太頻繁,請一分鐘後再試。",
|
|
35
41
|
failed: "辦公室沒有儲存({0})。", layoutKept: "它的座位與路徑會保留。",
|
|
36
42
|
styleUnknown: "{0}(這裡沒有:以辦公桌顯示)",
|
|
43
|
+
packs: "外觀包", packsLead: "用畫師的圖片畫出的風格。請把外觀包的 manifest.json 和所有 PNG 檔一起選取(或選整個資料夾)。",
|
|
44
|
+
packsNone: "還沒有外觀包。", upload: "上傳外觀包", uploadFolder: "上傳資料夾", uploading: "上傳中…", uploaded: "已上傳外觀包「{0}」。",
|
|
45
|
+
uploadFailed: "外觀包沒有上傳:{0}", noManifest: "檔案中沒有 manifest.json", badManifest: "manifest.json 不是 JSON",
|
|
46
|
+
removePack: "刪除 {0}", removedPack: "已刪除外觀包「{0}」。", removeFailed: "外觀包沒有刪除({0})。",
|
|
37
47
|
},
|
|
38
48
|
});
|
|
39
49
|
const te = (k, ...v) => t(`orge.${k}`, ...v);
|
|
40
50
|
export const MAX_OFFICES = 20;
|
|
41
51
|
const STYLES = ["pixel", "studio"];
|
|
42
52
|
|
|
53
|
+
/** A file's bytes as base64 (in slices: a large picture never goes through one huge string call). */
|
|
54
|
+
export function base64Of(bytes) {
|
|
55
|
+
let out = "";
|
|
56
|
+
for (let i = 0; i < bytes.length; i += 0x8000) out += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
|
|
57
|
+
return btoa(out);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* What an upload sends, from the files a person picked: { body } or { error }. A folder's files keep their path inside
|
|
61
|
+
* it (its first part, the folder's own name, dropped), so a manifest naming "sprites/idle.png" finds it.
|
|
62
|
+
*/
|
|
63
|
+
export async function packUpload(files) {
|
|
64
|
+
const named = [];
|
|
65
|
+
for (const f of files) {
|
|
66
|
+
const rel = typeof f.webkitRelativePath === "string" && f.webkitRelativePath ? f.webkitRelativePath.split("/").slice(1).join("/") : f.name;
|
|
67
|
+
named.push({ name: rel || f.name, file: f });
|
|
68
|
+
}
|
|
69
|
+
const man = named.find((x) => x.name === "manifest.json") || named.find((x) => x.name.endsWith("/manifest.json") || x.file.name === "manifest.json");
|
|
70
|
+
if (!man) return { error: te("noManifest") };
|
|
71
|
+
let manifest;
|
|
72
|
+
try { manifest = JSON.parse(await man.file.text()); } catch { return { error: te("badManifest") }; }
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const x of named) {
|
|
75
|
+
if (x === man || !/\.png$/i.test(x.name)) continue;
|
|
76
|
+
out[x.name] = base64Of(new Uint8Array(await x.file.arrayBuffer()));
|
|
77
|
+
}
|
|
78
|
+
return { body: { manifest, files: out } };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function Packs({ packs, store }) {
|
|
82
|
+
const [busy, setBusy] = useState(false);
|
|
83
|
+
const [msg, setMsg] = useState("");
|
|
84
|
+
const send = async (e) => {
|
|
85
|
+
const input = e.target, files = [...(input.files || [])];
|
|
86
|
+
if (!files.length) return;
|
|
87
|
+
setBusy(true); setMsg(te("uploading"));
|
|
88
|
+
try {
|
|
89
|
+
const up = await packUpload(files);
|
|
90
|
+
if (up.error) { setMsg(te("uploadFailed", up.error)); return; }
|
|
91
|
+
const r = await store.uploadSkin(up.body);
|
|
92
|
+
if (r.ok) { setMsg(""); toast(te("uploaded", (r.pack && r.pack.name) || up.body.manifest.id)); }
|
|
93
|
+
else setMsg(te("uploadFailed", r.detail ? `${r.error} — ${r.detail}` : r.error));
|
|
94
|
+
} finally { setBusy(false); try { input.value = ""; } catch { /* a read-only value */ } }
|
|
95
|
+
};
|
|
96
|
+
const remove = async (pk) => {
|
|
97
|
+
setBusy(true); setMsg("");
|
|
98
|
+
try {
|
|
99
|
+
const r = await store.deleteSkin(pk.id);
|
|
100
|
+
if (r.ok) toast(te("removedPack", pk.name)); else setMsg(te("removeFailed", r.error || r.status));
|
|
101
|
+
} finally { setBusy(false); }
|
|
102
|
+
};
|
|
103
|
+
return html`<fieldset class="oe-packs oe-form" disabled=${busy}>
|
|
104
|
+
<legend class="oe-label">${te("packs")}</legend>
|
|
105
|
+
<p class="note">${te("packsLead")}</p>
|
|
106
|
+
${packs.length ? html`<ul class="oe-pack-list">${packs.map((pk) => html`<li key=${pk.id} class="oe-pack" data-pack=${pk.id}>
|
|
107
|
+
<span>${pk.name}</span>${pk.name !== pk.id ? html` <span class="muted mono">${pk.id}</span>` : null}
|
|
108
|
+
<button type="button" class="btn btn-sm btn-danger oe-pack-remove" onClick=${() => remove(pk)} aria-label=${te("removePack", pk.name)}>${te("remove")}</button></li>`)}</ul>`
|
|
109
|
+
: html`<p class="note" id="oePacksNone">${te("packsNone")}</p>`}
|
|
110
|
+
<label class="btn btn-sm oe-upload">${te("upload")}<input type="file" class="sr-only oe-upload-files" multiple accept=".json,.png,application/json,image/png" onChange=${send} /></label>
|
|
111
|
+
<label class="btn btn-sm oe-upload">${te("uploadFolder")}<input type="file" class="sr-only oe-upload-dir" webkitdirectory="" multiple onChange=${send} /></label>
|
|
112
|
+
${msg ? html`<p class="note oe-pack-msg" role="status">${msg}</p>` : null}
|
|
113
|
+
</fieldset>`;
|
|
114
|
+
}
|
|
115
|
+
|
|
43
116
|
/** A new office's id: unique among `taken`, the store's pattern ([A-Za-z0-9_-], never "auto"). */
|
|
44
117
|
export function newOfficeId(taken, rand = Math.random) {
|
|
45
118
|
for (;;) {
|
|
@@ -49,7 +122,7 @@ export function newOfficeId(taken, rand = Math.random) {
|
|
|
49
122
|
}
|
|
50
123
|
/** What is sent: the office as the host stores it (no empty layout key). */
|
|
51
124
|
export function officeForSave(o) {
|
|
52
|
-
return { id: o.id, name: o.name.trim(), skin: o.skin, members: [...o.members], ...(o.layout ? { layout: o.layout } : {}) };
|
|
125
|
+
return { id: o.id, name: o.name.trim(), skin: o.skin, members: [...o.members], ...(o.layout ? { layout: o.layout } : {}), ...(o.overrides ? { overrides: o.overrides } : {}) };
|
|
53
126
|
}
|
|
54
127
|
|
|
55
128
|
function Members({ office, view, everyone, onChange }) {
|
|
@@ -69,7 +142,7 @@ function Members({ office, view, everyone, onChange }) {
|
|
|
69
142
|
* the snapshot in force (the page keeps a newer one it already shows: #2431 review); `current()` that snapshot now (null:
|
|
70
143
|
* not known), read when the person chooses after a 409 (#2449 review); `onClose`.
|
|
71
144
|
*/
|
|
72
|
-
export function OfficeEditor({ offices, rev, view, store, ask = () => 0, current = () => null, onSaved, onClose }) {
|
|
145
|
+
export function OfficeEditor({ offices, rev, view, store, packs = [], ask = () => 0, current = () => null, onSaved, onClose }) {
|
|
73
146
|
// The draft and the rev it was read at go together (#2424 review): a list the stream re-reads meanwhile never moves
|
|
74
147
|
// the rev under an older draft (a save would then overwrite what it never showed). After a 409 the person chooses
|
|
75
148
|
// (#2431): keep their edits (on the newer rev: Save then replaces what was saved meanwhile, knowingly) or load the
|
|
@@ -129,7 +202,8 @@ export function OfficeEditor({ offices, rev, view, store, ask = () => 0, current
|
|
|
129
202
|
<label class="oe-field"><span class="oe-label">${te("styleLabel")}</span>
|
|
130
203
|
<select class="tb-select oe-style" value=${o.skin} onChange=${(e) => set(o.id, { skin: e.target.value })}>
|
|
131
204
|
${STYLES.map((s) => html`<option key=${s} value=${s}>${t(`orgo.style_${s}`)}</option>`)}
|
|
132
|
-
${
|
|
205
|
+
${packs.map((pk) => html`<option key=${`pack:${pk.id}`} value=${pk.id}>${pk.name}</option>`)}
|
|
206
|
+
${STYLES.includes(o.skin) || packs.some((pk) => pk.id === o.skin) ? null : html`<option key=${o.skin} value=${o.skin}>${te("styleUnknown", o.skin)}</option>`}</select></label>
|
|
133
207
|
<button type="button" class="btn btn-sm oe-members-btn" aria-expanded=${open === o.id ? "true" : "false"} onClick=${() => setOpen(open === o.id ? null : o.id)}>
|
|
134
208
|
${te("members")}: ${o.members.length ? te("membersN", o.members.length) : te("everyone")}</button>
|
|
135
209
|
<button type="button" class="btn btn-sm btn-danger oe-remove" onClick=${() => { setDraft((d) => d.filter((x) => x.id !== o.id)); if (open === o.id) setOpen(null); }}>${te("remove")}</button>
|
|
@@ -141,6 +215,7 @@ export function OfficeEditor({ offices, rev, view, store, ask = () => 0, current
|
|
|
141
215
|
${draft.length >= MAX_OFFICES ? html`<span class="note">${te("tooMany", MAX_OFFICES)}</span>` : null}
|
|
142
216
|
</fieldset>
|
|
143
217
|
${msg ? html`<p class="note oe-msg" role="status">${msg}</p>` : null}
|
|
218
|
+
${store.uploadSkin ? html`<${Packs} packs=${packs} store=${store} />` : null}
|
|
144
219
|
${conflict ? html`<div class="oe-conflict">
|
|
145
220
|
<button type="button" class="btn btn-sm" id="oeKeepMine" onClick=${keepMine} disabled=${busy}>${te("keepMine")}</button>
|
|
146
221
|
<button type="button" class="btn btn-sm" id="oeLoadTheirs" onClick=${loadTheirs} disabled=${busy}>${te("loadTheirs")}</button>
|