@songsid/agend 2.3.2-beta.3 → 2.3.2-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-cli.js +21 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +5 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/kiro.d.ts +21 -1
- package/dist/backend/kiro.js +89 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/channel/adapters/discord.js +7 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +4 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +5 -0
- package/dist/classic-channel-manager.js +14 -1
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/config-validator.js +22 -5
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +6 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon.js +16 -4
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +20 -6
- package/dist/dashboard-permissions.js +58 -9
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/fleet-manager.d.ts +16 -1
- package/dist/fleet-manager.js +183 -21
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +24 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +24 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +6 -2
- package/dist/instance-config-impact.js +10 -5
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +8 -0
- package/dist/instance-lifecycle.js +5 -3
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +2 -6
- package/dist/locale.js.map +1 -1
- package/dist/notice-delivery.d.ts +15 -0
- package/dist/notice-delivery.js +37 -0
- package/dist/notice-delivery.js.map +1 -0
- package/dist/outbound-schemas.d.ts +20 -0
- package/dist/outbound-schemas.js +17 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart.js +2 -0
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-triggers.d.ts +2 -0
- package/dist/reply-triggers.js +3 -0
- package/dist/reply-triggers.js.map +1 -0
- package/dist/settings-api.js +27 -4
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +17 -14
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +7 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-instance-patch.js +4 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/setup-host.js +2 -1
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +2 -0
- package/dist/setup-wizard.js.map +1 -1
- package/dist/tool-permissions.js +10 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.js +2 -1
- package/dist/topic-commands.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +1 -1
- package/dist/turn-reply-guard.js +1 -1
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +16 -6
- package/dist/ui/fleet-office-editor.js +10 -6
- package/dist/ui/fleet-office-layout.js +34 -28
- package/dist/ui/fleet-office-model.js +6 -3
- package/dist/ui/fleet-office-overview.js +2 -2
- package/dist/ui/fleet-office-pack.d.ts +3 -1
- package/dist/ui/fleet-office-pack.js +58 -7
- package/dist/ui/fleet-office-skin.js +13 -4
- package/dist/ui/fleet-office-store.js +27 -1
- package/dist/ui/fleet-office-view.js +5 -4
- package/dist/ui/fleet-office-zones.js +155 -6
- package/dist/ui/fleet-office.js +169 -41
- package/dist/ui/panel-settings.js +32 -10
- package/dist/ui/settings-dialogs.js +8 -1
- package/dist/ui/settings-model.js +14 -6
- package/dist/ui/settings-registry.js +4 -0
- package/dist/ui/settings-strings.js +14 -0
- package/dist/ui/shared/app-i18n.js +2 -0
- package/dist/ui/shared/app-shell.js +14 -6
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +17 -2
- package/dist/ui/shared/side-panel.js +95 -0
- package/dist/web-api.d.ts +3 -0
- package/dist/web-api.js +11 -0
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-office-acts.d.ts +173 -0
- package/dist/web-office-acts.js +285 -0
- package/dist/web-office-acts.js.map +1 -0
- package/dist/web-office-skins.d.ts +1 -1
- package/dist/web-office-skins.js +1 -1
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-settings-patch.d.ts +2 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/package.json +1 -1
- package/templates/windows-shortcuts.ps1 +11 -2
|
@@ -952,8 +952,13 @@ function Classic({ ctx, search, openDialog }) {
|
|
|
952
952
|
})}</div>`;
|
|
953
953
|
}
|
|
954
954
|
|
|
955
|
+
const configuredNoticeMode = (value, route) => value === false ? false : value === "dm" || route === "dm" ? "dm" : true;
|
|
956
|
+
const noticeMode = (v) => v === "dm" ? "dm" : v === "false" || v === false ? false : true;
|
|
957
|
+
const noticeChoices = () => [{ value: "true", label: tn("noticeTopic") }, { value: "dm", label: tn("noticeDm") }, { value: "false", label: tn("noticeOff") }];
|
|
958
|
+
const noticeLabel = (v) => tn(v === "dm" ? "noticeDm" : v === false ? "noticeOff" : "noticeTopic");
|
|
959
|
+
|
|
955
960
|
/** General's form, from the saved defaults (`d`) and ClassicBot's (`c`). */
|
|
956
|
-
function generalForm(d, c, fleet = {}) {
|
|
961
|
+
function generalForm(d, c, fleet = {}, schema = {}) {
|
|
957
962
|
const hang = d.hang_detector || {}, startup = d.startup || {};
|
|
958
963
|
return {
|
|
959
964
|
backend: d.backend || "claude-code", model: d.model || "", locale: d.locale || "", auto: String(d.auto_pause_after ?? 0),
|
|
@@ -966,6 +971,9 @@ function generalForm(d, c, fleet = {}) {
|
|
|
966
971
|
// #2347 §10 review: unset startup concurrency is automatic (deriveSpawnConcurrency: 2–10 by free memory and cores),
|
|
967
972
|
// not 10 — an empty field, and a save that leaves it unset (the defaults merge replaces `startup` whole).
|
|
968
973
|
concurrency: startup.concurrency == null ? "" : String(startup.concurrency), stagger: String(startup.stagger_delay_ms ?? 500),
|
|
974
|
+
tips: String(configuredNoticeMode(d.tips, fleet.web?.notice_delivery?.tips)),
|
|
975
|
+
replyTriggers: Boolean(d.reply_triggers ?? schema.reply_triggers_default),
|
|
976
|
+
classicReplyTriggers: Boolean(c.reply_triggers ?? d.reply_triggers ?? schema.reply_triggers_default), classicReplyTriggersInherit: !hasOwn(c, "reply_triggers"),
|
|
969
977
|
classicBackend: c.backend || d.backend || "claude-code",
|
|
970
978
|
classicToolProgress: c.tool_progress ?? d.tool_progress ?? "off", classicToolProgressInherit: !hasOwn(c, "tool_progress"),
|
|
971
979
|
classicReply: c.reply_completion_guard ?? d.reply_completion_guard ?? true, classicReplyInherit: !hasOwn(c, "reply_completion_guard"),
|
|
@@ -985,7 +993,7 @@ function General({ ctx, part = "all" }) {
|
|
|
985
993
|
const hang = d.hang_detector || {}, startup = d.startup || {};
|
|
986
994
|
const primary = channelsOf(ctx.fleet)[0];
|
|
987
995
|
const original = useMemo(() => ({ c: structuredClone(c) }), []);
|
|
988
|
-
const base = useMemo(() => generalForm(d, c, ctx.fleet), []);
|
|
996
|
+
const base = useMemo(() => generalForm(d, c, ctx.fleet, schema), []);
|
|
989
997
|
const [f, setF] = useDraft(ctx, "general", () => base);
|
|
990
998
|
const set = (k) => (v) => setF((x) => ({ ...x, [k]: v }));
|
|
991
999
|
/** The class of a field's row: `field`, and `s-dirty` while one of its keys differs from what is saved. */
|
|
@@ -1011,27 +1019,31 @@ function General({ ctx, part = "all" }) {
|
|
|
1011
1019
|
const defaultsPatch = changedFields({
|
|
1012
1020
|
backend: f.backend, model: f.model || undefined, locale: f.locale || null, auto_pause_after: num(f.auto),
|
|
1013
1021
|
agent_mode: f.agent_mode, tool_set: f.tool_set || null, tool_progress: f.tool_progress, cross_instance_visibility: f.visibility,
|
|
1014
|
-
reply_completion_guard: f.reply, log_level: f.log_level,
|
|
1015
|
-
// The dashboard features every agent may use unless its own setting says otherwise. All
|
|
1022
|
+
tips: noticeMode(f.tips), reply_triggers: f.replyTriggers, reply_completion_guard: f.reply, log_level: f.log_level,
|
|
1023
|
+
// The dashboard features every agent may use unless its own setting says otherwise. All of them go, because the write
|
|
1016
1024
|
// to the fleet's defaults merges top-level keys only (`dashboard` is replaced whole): the untouched ones as saved.
|
|
1017
|
-
dashboard: Object.fromEntries(DASHBOARD_FEATURES.map((k) => [k, f[`dash_${k}`]])),
|
|
1025
|
+
dashboard: Object.fromEntries(DASHBOARD_FEATURES.filter((k) => k !== "office").map((k) => [k, f[`dash_${k}`]])),
|
|
1026
|
+
office_access: f.dash_office,
|
|
1018
1027
|
hang_detector: { enabled: f.hangOn, timeout_minutes: num(f.hang) },
|
|
1019
1028
|
startup: { ...(f.concurrency.trim() === "" ? {} : { concurrency: num(f.concurrency) }), stagger_delay_ms: num(f.stagger) },
|
|
1020
1029
|
}, {
|
|
1021
1030
|
backend: d.backend || "claude-code", model: d.model || undefined, locale: d.locale || null, auto_pause_after: d.auto_pause_after ?? 0,
|
|
1022
1031
|
agent_mode: d.agent_mode || "mcp", tool_set: TOOL_SETS.includes(d.tool_set) ? d.tool_set : null, tool_progress: d.tool_progress ?? "off",
|
|
1023
|
-
cross_instance_visibility: visibilityDefault(d), reply_completion_guard: d.reply_completion_guard ?? true, log_level: d.log_level || "info",
|
|
1024
|
-
dashboard: Object.fromEntries(DASHBOARD_FEATURES.map((k) => [k, dashboardDefault(d, k)])),
|
|
1032
|
+
tips: configuredNoticeMode(d.tips, ctx.fleet.web?.notice_delivery?.tips), cross_instance_visibility: visibilityDefault(d), reply_triggers: Boolean(d.reply_triggers ?? schema.reply_triggers_default), reply_completion_guard: d.reply_completion_guard ?? true, log_level: d.log_level || "info",
|
|
1033
|
+
dashboard: Object.fromEntries(DASHBOARD_FEATURES.filter((k) => k !== "office").map((k) => [k, dashboardDefault(d, k)])),
|
|
1034
|
+
office_access: dashboardDefault(d, "office"),
|
|
1025
1035
|
hang_detector: { enabled: hang.enabled !== false, timeout_minutes: hang.timeout_minutes ?? HANG_TIMEOUT_DEFAULT_MINUTES },
|
|
1026
1036
|
startup: { ...(startup.concurrency == null ? {} : { concurrency: startup.concurrency }), stagger_delay_ms: startup.stagger_delay_ms ?? 500 },
|
|
1027
1037
|
});
|
|
1028
1038
|
const oc = original.c;
|
|
1029
1039
|
const classicPatch = changedFields({
|
|
1040
|
+
reply_triggers: f.classicReplyTriggersInherit ? null : f.classicReplyTriggers,
|
|
1030
1041
|
backend: f.classicBackend,
|
|
1031
1042
|
tool_progress: f.classicToolProgressInherit ? null : f.classicToolProgress,
|
|
1032
1043
|
reply_completion_guard: f.classicReplyInherit ? null : f.classicReply,
|
|
1033
1044
|
admin_users: f.admins, allowed_guilds: f.guilds,
|
|
1034
1045
|
}, {
|
|
1046
|
+
reply_triggers: hasOwn(oc, "reply_triggers") ? oc.reply_triggers : null,
|
|
1035
1047
|
backend: oc.backend || d.backend || "claude-code",
|
|
1036
1048
|
tool_progress: hasOwn(oc, "tool_progress") ? oc.tool_progress : null,
|
|
1037
1049
|
reply_completion_guard: hasOwn(oc, "reply_completion_guard") ? oc.reply_completion_guard : null,
|
|
@@ -1054,7 +1066,7 @@ function General({ ctx, part = "all" }) {
|
|
|
1054
1066
|
// ClassicBot's inherited ones say what they inherit from Agent defaults.
|
|
1055
1067
|
const unset = {
|
|
1056
1068
|
locale: !d.locale, backend: !d.backend, model: !d.model, auto: !hasOwn(d, "auto_pause_after"), tool_progress: !hasOwn(d, "tool_progress"),
|
|
1057
|
-
visibility: !VISIBILITY_MODES.includes(d.cross_instance_visibility), reply: !hasOwn(d, "reply_completion_guard"), tool_set: !d.tool_set,
|
|
1069
|
+
tips: !hasOwn(d, "tips"), replyTriggers: !hasOwn(d, "reply_triggers"), visibility: !VISIBILITY_MODES.includes(d.cross_instance_visibility), reply: !hasOwn(d, "reply_completion_guard"), tool_set: !d.tool_set,
|
|
1058
1070
|
log_level: !d.log_level, agent_mode: !d.agent_mode, hangOn: !hasOwn(hang, "enabled"), hang: !hasOwn(hang, "timeout_minutes"),
|
|
1059
1071
|
concurrency: !hasOwn(startup, "concurrency"), stagger: !hasOwn(startup, "stagger_delay_ms"),
|
|
1060
1072
|
};
|
|
@@ -1077,6 +1089,8 @@ function General({ ctx, part = "all" }) {
|
|
|
1077
1089
|
<div class=${fc("field", "tool_progress")}>${L(tn("toolProgress"), "defaults.tool_progress", "g-tp", "tool_progress")}<${Select} id="g-tp" value=${f.tool_progress} onChange=${set("tool_progress")} options=${TOOL_PROGRESS} /></div>
|
|
1078
1090
|
<div class=${fc("field", "visibility")}>${L(tn("crossInstanceVisibility"), "defaults.cross_instance_visibility", "g-vis", "visibility")}<${Select} id="g-vis" value=${f.visibility} onChange=${set("visibility")} options=${VISIBILITY_MODES} />
|
|
1079
1091
|
<p class="note">${tn("crossInstanceVisibilityHint")}</p></div>
|
|
1092
|
+
<div class=${fc("field", "tips")}>${L(tn("dailyTips"), "defaults.tips", "g-tips", "tips")}<${Select} id="g-tips" value=${f.tips} onChange=${set("tips")} options=${noticeChoices()} /><${InEffect} text=${noticeLabel(noticeMode(f.tips))} /><p class="note">${tn("dailyTipsHint")}</p></div>
|
|
1093
|
+
<div class=${fc("field", "replyTriggers")}><label class="check"><input id="g-reply-triggers" type="checkbox" checked=${f.replyTriggers} onChange=${(e) => set("replyTriggers")(e.target.checked)} /><span>${tn("replyTriggers")}</span>${dm("replyTriggers")}<${Impact} schema=${schema} field="defaults.reply_triggers" /></label><p class="note">${tn("replyTriggersHint")}</p></div>
|
|
1080
1094
|
<div class=${fc("field", "reply")}><label class="check"><input id="g-reply" type="checkbox" checked=${f.reply} onChange=${(e) => set("reply")(e.target.checked)} /><span>${tn("replyCompletionGuard")}</span>${dm("reply")}<${Impact} schema=${schema} field="defaults.reply_completion_guard" /></label>
|
|
1081
1095
|
<p class="note">${fleetReply}</p></div>
|
|
1082
1096
|
</div>
|
|
@@ -1093,6 +1107,9 @@ function General({ ctx, part = "all" }) {
|
|
|
1093
1107
|
<div class=${fc("field", "classicToolProgress", "classicToolProgressInherit")}>${L(tn("toolProgress"), "classic_defaults.tool_progress", "g-ctp")}
|
|
1094
1108
|
<${Override} inherit=${f.classicToolProgressInherit} onInherit=${set("classicToolProgressInherit")} inherited=${fromDefaults("tool_progress", d.tool_progress ?? "off", "#g-tp")}>
|
|
1095
1109
|
<${Select} id="g-ctp" disabled=${f.classicToolProgressInherit} value=${f.classicToolProgressInherit ? (d.tool_progress ?? "off") : f.classicToolProgress} onChange=${set("classicToolProgress")} options=${TOOL_PROGRESS} /></${Override}></div>
|
|
1110
|
+
<div class=${fc("field", "classicReplyTriggers", "classicReplyTriggersInherit")}>${L(tn("replyTriggers"), "classic_defaults.reply_triggers")}
|
|
1111
|
+
<${Override} inherit=${f.classicReplyTriggersInherit} onInherit=${set("classicReplyTriggersInherit")} inherited=${fromDefaults("reply_triggers", Boolean(d.reply_triggers ?? schema.reply_triggers_default), "#g-reply-triggers")}>
|
|
1112
|
+
<label class="check"><input id="g-creply-triggers" type="checkbox" disabled=${f.classicReplyTriggersInherit} checked=${f.classicReplyTriggersInherit ? Boolean(d.reply_triggers ?? schema.reply_triggers_default) : f.classicReplyTriggers} onChange=${(e) => set("classicReplyTriggers")(e.target.checked)} /><span>${tn("replyTriggers")}</span></label></${Override}><p class="note">${tn("replyTriggersHint")}</p></div>
|
|
1096
1113
|
<div class=${fc("field", "classicReply", "classicReplyInherit")}>${L(tn("replyCompletionGuard"), "classic_defaults.reply_completion_guard")}
|
|
1097
1114
|
<${Override} inherit=${f.classicReplyInherit} onInherit=${set("classicReplyInherit")} inherited=${fromDefaults("reply_completion_guard", d.reply_completion_guard ?? true, "#g-reply")}>
|
|
1098
1115
|
<label class="check"><input id="g-creply" type="checkbox" disabled=${f.classicReplyInherit} checked=${f.classicReplyInherit ? (d.reply_completion_guard ?? true) : f.classicReply} onChange=${(e) => set("classicReply")(e.target.checked)} /><span>${tn("replyCompletionGuard")}</span></label></${Override}>
|
|
@@ -1140,7 +1157,7 @@ function General({ ctx, part = "all" }) {
|
|
|
1140
1157
|
* the temporary public link none of it can be changed — the form says so and stays disabled (the server refuses too).
|
|
1141
1158
|
*/
|
|
1142
1159
|
export const WEB_READ_ONLY = [
|
|
1143
|
-
"web.allowed_hosts", "web.view_access", "web.
|
|
1160
|
+
"web.allowed_hosts", "web.view_access", "web.preview", "web.preview_port", "web.preview_origin", "web.preview_on_public_link",
|
|
1144
1161
|
"web.usage_panel", "web.search", "web.provider_secrets", "web.upload_max_file_mb", "web.upload_max_total_mb", "web_terminal.bind", "web_terminal.tunnel",
|
|
1145
1162
|
];
|
|
1146
1163
|
/**
|
|
@@ -1173,6 +1190,7 @@ function WebSection({ ctx, staged, part = "all" }) {
|
|
|
1173
1190
|
// serves their reads there).
|
|
1174
1191
|
const overPublicLink = globalThis.document?.body?.dataset?.publicLink === "1";
|
|
1175
1192
|
const original = useMemo(() => ({
|
|
1193
|
+
notify: configuredNoticeMode(web.notify_login, web.notice_delivery?.login),
|
|
1176
1194
|
echo: web.echo_to_channel ?? true,
|
|
1177
1195
|
log: web.log_tab === true, // #1218: off by default
|
|
1178
1196
|
usagePub: web.usage_report_on_public_link === true, // #2122: off by default
|
|
@@ -1182,7 +1200,7 @@ function WebSection({ ctx, staged, part = "all" }) {
|
|
|
1182
1200
|
}), []);
|
|
1183
1201
|
const text = (v) => (v === null || v === undefined ? "" : String(v));
|
|
1184
1202
|
/** #2347: a field's row class: `s-dirty` while it differs from what is saved (the draft may come from before). */
|
|
1185
|
-
const savedForm = useMemo(() => ({ echo: original.echo, log: original.log, usagePub: original.usagePub, pub: original.pub.allow_public,
|
|
1203
|
+
const savedForm = useMemo(() => ({ notify: String(original.notify), echo: original.echo, log: original.log, usagePub: original.usagePub, pub: original.pub.allow_public,
|
|
1186
1204
|
ttl: String(original.pub.ttl_minutes), protocol: original.pub.protocol, hostname: original.pub.hostname, originPort: String(original.pub.origin_port),
|
|
1187
1205
|
abs: text(original.session.absolute_hours), idle: text(original.session.idle_hours), termOn: original.terminal.enabled !== false,
|
|
1188
1206
|
termTtl: text(original.terminal.ttl_minutes), autoOpen: original.pub.auto_open }), []);
|
|
@@ -1194,6 +1212,7 @@ function WebSection({ ctx, staged, part = "all" }) {
|
|
|
1194
1212
|
const pub = pending.public_link || {}, session = pending.session?.local || {}, terminal = pending.web_terminal || {};
|
|
1195
1213
|
const pick = (obj, key, fallback) => Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : fallback;
|
|
1196
1214
|
return {
|
|
1215
|
+
notify: String(pick(pending, "notify_login", original.notify)),
|
|
1197
1216
|
echo: pick(pending, "echo_to_channel", original.echo), log: pick(pending, "log_tab", original.log), usagePub: pick(pending, "usage_report_on_public_link", original.usagePub),
|
|
1198
1217
|
pub: pick(pub, "allow_public", original.pub.allow_public), ttl: String(pick(pub, "ttl_minutes", original.pub.ttl_minutes)), protocol: pick(pub, "protocol", original.pub.protocol),
|
|
1199
1218
|
hostname: pick(pub, "hostname", original.pub.hostname), originPort: String(pick(pub, "origin_port", original.pub.origin_port)), autoOpen: pick(pub, "auto_open", original.pub.auto_open),
|
|
@@ -1242,6 +1261,7 @@ function WebSection({ ctx, staged, part = "all" }) {
|
|
|
1242
1261
|
const terminalPatch = changedFields({ enabled: f.termOn === (original.terminal.enabled !== false) ? original.terminal.enabled : f.termOn ? null : false,
|
|
1243
1262
|
ttl_minutes: value(f.termTtl) }, original.terminal);
|
|
1244
1263
|
const body = {
|
|
1264
|
+
...(noticeMode(f.notify) !== original.notify ? { notify_login: noticeMode(f.notify) } : {}),
|
|
1245
1265
|
...(f.echo !== original.echo ? { echo_to_channel: f.echo } : {}),
|
|
1246
1266
|
...(f.log !== original.log ? { log_tab: f.log } : {}),
|
|
1247
1267
|
...(f.usagePub !== original.usagePub ? { usage_report_on_public_link: f.usagePub } : {}),
|
|
@@ -1250,6 +1270,7 @@ function WebSection({ ctx, staged, part = "all" }) {
|
|
|
1250
1270
|
...(Object.keys(terminalPatch).length ? { web_terminal: terminalPatch } : {}),
|
|
1251
1271
|
};
|
|
1252
1272
|
const fields = [
|
|
1273
|
+
...(body.notify_login !== undefined ? ["web.notify_login"] : []),
|
|
1253
1274
|
...(body.echo_to_channel !== undefined ? ["web.echo_to_channel"] : []),
|
|
1254
1275
|
...(body.log_tab !== undefined ? ["web.log_tab"] : []),
|
|
1255
1276
|
...(body.usage_report_on_public_link !== undefined ? ["web.usage_report_on_public_link"] : []),
|
|
@@ -1297,6 +1318,7 @@ function WebSection({ ctx, staged, part = "all" }) {
|
|
|
1297
1318
|
${overPublicLink && part !== "readonly" ? html`<p class="note" role="status" id="webPublicRefused">${tn("webPublicRefused")}</p>` : null}
|
|
1298
1319
|
${has("access") ? lock(html`<section class="card"><h3>${tn("webSessionTitle")}</h3>
|
|
1299
1320
|
<p class="note">${tn("webSessionHint")}</p>
|
|
1321
|
+
<div class=${fc("field", "notify")}><label for="webNotifyLogin">${tn("webNotifyLogin")}<${Impact} schema=${schema} field="web.notify_login" /></label><${Select} id="webNotifyLogin" value=${f.notify} onChange=${set("notify")} options=${noticeChoices()} /><${InEffect} text=${noticeLabel(noticeMode(f.notify))} />${web.notify_login === undefined ? html`<${DefaultMark} />` : null}<p class="note">${tn("webNotifyLoginHint")}</p></div>
|
|
1300
1322
|
<div class="grid2">
|
|
1301
1323
|
<div class=${fc("field", "abs")}><label for="webSessionAbsolute">${tn("sessionAbsolute")}</label><input id="webSessionAbsolute" type="number" min="1" max="720" step="any"
|
|
1302
1324
|
class=${errors.abs ? "invalid" : ""} value=${f.abs} onInput=${(e) => set("abs")(e.target.value)} /><${Feedback} error=${errors.abs} />
|
|
@@ -642,6 +642,7 @@ export function ClassicDialog({ room, ctx, onClose }) {
|
|
|
642
642
|
const inherited = {
|
|
643
643
|
backend: cd.backend || fd.backend || "claude-code", model: cd.model || fd.model || "",
|
|
644
644
|
auto_pause_after: cd.auto_pause_after ?? fd.auto_pause_after ?? 0, tool_progress: cd.tool_progress ?? fd.tool_progress ?? "off",
|
|
645
|
+
reply_triggers: Boolean(cd.reply_triggers ?? fd.reply_triggers ?? schema.reply_triggers_default),
|
|
645
646
|
reply_completion_guard: cd.reply_completion_guard ?? fd.reply_completion_guard ?? true,
|
|
646
647
|
};
|
|
647
648
|
const c = room;
|
|
@@ -649,8 +650,9 @@ export function ClassicDialog({ room, ctx, onClose }) {
|
|
|
649
650
|
backend: c.backend || inherited.backend, model: c.model || "",
|
|
650
651
|
auto_pause_after: String(c.auto_pause_after ?? inherited.auto_pause_after), tool_progress: c.tool_progress ?? inherited.tool_progress,
|
|
651
652
|
reply_completion_guard: c.reply_completion_guard ?? inherited.reply_completion_guard,
|
|
653
|
+
reply_triggers: c.reply_triggers ?? inherited.reply_triggers,
|
|
652
654
|
collab: !!c.collab, web_echo: classicWebEchoEffective(c, fleet), context_lines: String(c.context_lines ?? cd.context_lines ?? 5),
|
|
653
|
-
inherit: { auto_pause_after: !hasOwn(c, "auto_pause_after"), tool_progress: !hasOwn(c, "tool_progress"), reply_completion_guard: !hasOwn(c, "reply_completion_guard") },
|
|
655
|
+
inherit: { reply_triggers: !hasOwn(c, "reply_triggers"), auto_pause_after: !hasOwn(c, "auto_pause_after"), tool_progress: !hasOwn(c, "tool_progress"), reply_completion_guard: !hasOwn(c, "reply_completion_guard") },
|
|
654
656
|
}));
|
|
655
657
|
const set = (k) => (v) => setF((x) => ({ ...x, [k]: v }));
|
|
656
658
|
const setInherit = (k) => (on) => setF((x) => ({ ...x, inherit: { ...x.inherit, [k]: on } }));
|
|
@@ -664,12 +666,14 @@ export function ClassicDialog({ room, ctx, onClose }) {
|
|
|
664
666
|
auto_pause_after: f.inherit.auto_pause_after ? null : Number(f.auto_pause_after),
|
|
665
667
|
tool_progress: f.inherit.tool_progress ? null : f.tool_progress,
|
|
666
668
|
reply_completion_guard: f.inherit.reply_completion_guard ? null : f.reply_completion_guard,
|
|
669
|
+
reply_triggers: f.inherit.reply_triggers ? null : f.reply_triggers,
|
|
667
670
|
collab: f.collab, context_lines: Number(f.context_lines), web_echo: f.web_echo,
|
|
668
671
|
}, {
|
|
669
672
|
backend: c.backend || inherited.backend, model: (c.model && c.model.trim()) || null,
|
|
670
673
|
auto_pause_after: hasOwn(c, "auto_pause_after") ? c.auto_pause_after : null,
|
|
671
674
|
tool_progress: hasOwn(c, "tool_progress") ? c.tool_progress : null,
|
|
672
675
|
reply_completion_guard: hasOwn(c, "reply_completion_guard") ? c.reply_completion_guard : null,
|
|
676
|
+
reply_triggers: hasOwn(c, "reply_triggers") ? c.reply_triggers : null,
|
|
673
677
|
collab: !!c.collab, context_lines: c.context_lines ?? cd.context_lines ?? 5, web_echo: classicWebEchoEffective(c, fleet),
|
|
674
678
|
});
|
|
675
679
|
const stage = async () => {
|
|
@@ -707,6 +711,9 @@ export function ClassicDialog({ room, ctx, onClose }) {
|
|
|
707
711
|
<label class="check"><input type="checkbox" disabled=${f.inherit.reply_completion_guard} checked=${f.inherit.reply_completion_guard ? inherited.reply_completion_guard : f.reply_completion_guard}
|
|
708
712
|
onChange=${(e) => set("reply_completion_guard")(e.target.checked)} /><span>${tn("replyCompletionGuard")}</span></label></${Override}>
|
|
709
713
|
<p class="note">${replyHint(f.backend, classicMode, fd.kiro_ui)}</p></div>
|
|
714
|
+
<div class="field">${L(tn("replyTriggers"), "classic.reply_triggers")}
|
|
715
|
+
<${Override} inherit=${f.inherit.reply_triggers} onInherit=${setInherit("reply_triggers")} inherited=${roomInh("reply_triggers", "", "#g-creply-triggers", "#g-reply-triggers")}>
|
|
716
|
+
<label class="check"><input type="checkbox" disabled=${f.inherit.reply_triggers} checked=${f.inherit.reply_triggers ? inherited.reply_triggers : f.reply_triggers} onChange=${(e) => set("reply_triggers")(e.target.checked)} /><span>${tn("replyTriggers")}</span></label></${Override}><p class="note">${tn("replyTriggersHint")}</p></div>
|
|
710
717
|
<label class="check"><input type="checkbox" checked=${f.collab} onChange=${(e) => set("collab")(e.target.checked)} /><span>${tn("collabMode")}</span><${Impact} schema=${schema} field="classic.collab" /></label>
|
|
711
718
|
<label class="check"><input type="checkbox" checked=${f.web_echo} onChange=${(e) => set("web_echo")(e.target.checked)} /><span>${tn("webEcho")}</span><${Impact} schema=${schema} field="classic.web_echo" /></label>
|
|
712
719
|
</div>
|
|
@@ -40,19 +40,25 @@ export function changedFields(next, baseline) {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* The dashboard features an agent may use (dashboard-permissions.ts): each "inherit" (the fleet default: `defaults.dashboard`,
|
|
43
|
-
* else allowed), "on" or "off". The patch names only the features that changed: null
|
|
43
|
+
* else allowed — off for OFF_UNLESS_ON, `office`), "on" or "off". The patch names only the features that changed: null
|
|
44
|
+
* follows the default again.
|
|
44
45
|
*/
|
|
45
|
-
export const DASHBOARD_FEATURES = ["appearance", "task_board", "web_settings"];
|
|
46
|
+
export const DASHBOARD_FEATURES = ["appearance", "task_board", "web_settings", "office"];
|
|
47
|
+
/** #2578: the features a person switches on — unset is off (dashboard-permissions.ts OFF_UNLESS_ON). */
|
|
48
|
+
export const OFF_UNLESS_ON = ["office"];
|
|
46
49
|
export const DASHBOARD_CHOICES = ["inherit", "on", "off"];
|
|
47
50
|
const dashboardOf = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : {});
|
|
48
|
-
/** What a feature is for an agent with no setting of its own: the default's, else allowed. */
|
|
49
|
-
export const dashboardDefault = (defaults, feature) =>
|
|
51
|
+
/** What a feature is for an agent with no setting of its own: the default's, else allowed (off for OFF_UNLESS_ON). */
|
|
52
|
+
export const dashboardDefault = (defaults, feature) => {
|
|
53
|
+
const v = feature === "office" && defaults && Object.hasOwn(defaults, "office_access") ? defaults.office_access : dashboardOf(defaults && defaults.dashboard)[feature];
|
|
54
|
+
return OFF_UNLESS_ON.includes(feature) ? v === true : v !== false;
|
|
55
|
+
};
|
|
50
56
|
export function dashboardForm(inst) {
|
|
51
|
-
const own = dashboardOf(inst.dashboard);
|
|
57
|
+
const own = { ...dashboardOf(inst.dashboard), ...(Object.hasOwn(inst, "office_access") ? { office: inst.office_access } : {}) };
|
|
52
58
|
return Object.fromEntries(DASHBOARD_FEATURES.map((f) => [f, own[f] === true ? "on" : own[f] === false ? "off" : "inherit"]));
|
|
53
59
|
}
|
|
54
60
|
export function dashboardPatchValue(choices) {
|
|
55
|
-
return Object.fromEntries(DASHBOARD_FEATURES.map((f) => [f, choices[f] === "on" ? true : choices[f] === "off" ? false : null]));
|
|
61
|
+
return Object.fromEntries(DASHBOARD_FEATURES.filter((f) => f !== "office").map((f) => [f, choices[f] === "on" ? true : choices[f] === "off" ? false : null]));
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
/** #1302: cross-instance notices. Unset, or a value the fleet does not know, means full. */
|
|
@@ -175,6 +181,7 @@ export function agentPatch(form, inst, defaults, visibilityBaseline, statusEmoji
|
|
|
175
181
|
display_name: form.inherit.display_name ? null : form.display_name.trim(),
|
|
176
182
|
status_emojis: form.status_emojis,
|
|
177
183
|
dashboard: dashboardPatchValue(form.dashboard),
|
|
184
|
+
office_access: form.dashboard.office === "on" ? true : form.dashboard.office === "off" ? false : null,
|
|
178
185
|
};
|
|
179
186
|
return changedFields(next, {
|
|
180
187
|
backend: inst.backend || defaults.backend || "claude-code",
|
|
@@ -198,6 +205,7 @@ export function agentPatch(form, inst, defaults, visibilityBaseline, statusEmoji
|
|
|
198
205
|
display_name: hasOwn(inst, "display_name") ? inst.display_name : null,
|
|
199
206
|
status_emojis: statusEmojisBaseline,
|
|
200
207
|
dashboard: dashboardPatchValue(dashboardForm(inst)),
|
|
208
|
+
office_access: dashboardForm(inst).office === "on" ? true : dashboardForm(inst).office === "off" ? false : null,
|
|
201
209
|
});
|
|
202
210
|
}
|
|
203
211
|
|
|
@@ -33,6 +33,8 @@ const ROWS = [
|
|
|
33
33
|
// Fleet · Agents
|
|
34
34
|
["agents.new", "agents", "#newAgentBtn", "settings.newAgent", null, ["new agent", "create", "add agent", "instance"], ["新增 agent", "建立", "新 agent"]],
|
|
35
35
|
["agents.provider-keys", "agents", ".s-provider-keys", "settings.providerKeys", "web.provider_secrets", ["api key", "provider", "openai", "anthropic", "token", "secret"], ["API 金鑰", "金鑰", "供應商", "token"]],
|
|
36
|
+
["defaults.tips", "agent-defaults", "#g-tips", "settings.dailyTips", "defaults.tips", ["daily tips", "dm", "notices"], ["每日提示", "私訊"]],
|
|
37
|
+
["web.notify-login", "access", "#webNotifyLogin", "settings.webNotifyLogin", "web.notify_login", ["sign in", "login", "dm", "notices"], ["登入通知", "私訊"]],
|
|
36
38
|
// Fleet · Agent defaults
|
|
37
39
|
["defaults.locale", "agent-defaults", "#g-locale", "settings.language", "defaults.locale", ["language", "bot language", "reply language", "locale"], ["語言", "回覆語言", "bot 語言"]],
|
|
38
40
|
["defaults.backend", "agent-defaults", "#g-be", "settings.defaultBackend", "defaults.backend", ["backend", "cli", "claude", "codex", "kiro"], ["後端", "CLI"]],
|
|
@@ -40,6 +42,8 @@ const ROWS = [
|
|
|
40
42
|
["defaults.auto-pause", "agent-defaults", "#g-auto", "settings.autoPause", "defaults.auto_pause_after", ["auto pause", "idle", "sleep", "minutes"], ["自動暫停", "閒置", "休眠"]],
|
|
41
43
|
["defaults.tool-progress", "agent-defaults", "#g-tp", "settings.toolProgress", "defaults.tool_progress", ["tool progress", "verbose", "steps"], ["工具進度", "詳細"]],
|
|
42
44
|
["defaults.visibility", "agent-defaults", "#g-vis", "settings.crossInstanceVisibility", "defaults.cross_instance_visibility", ["visibility", "bot to bot", "cross instance", "messages"], ["可見性", "bot 之間", "跨 instance"]],
|
|
45
|
+
["defaults.reply-triggers", "agent-defaults", "#g-reply-triggers", "settings.replyTriggers", "defaults.reply_triggers", ["classic native reply", "reply trigger"], ["原生回覆觸發"]],
|
|
46
|
+
["chat.classic-reply-triggers", "chat", "#g-creply-triggers", "settings.replyTriggers", "classic_defaults.reply_triggers", ["classic native reply"], ["ClassicBot 原生回覆觸發"]],
|
|
43
47
|
["defaults.reply-guard", "agent-defaults", "#g-reply", "settings.replyCompletionGuard", "defaults.reply_completion_guard", ["delivered reply", "reply guard", "require reply"], ["送達回覆", "回覆保護"]],
|
|
44
48
|
["defaults.tool-set", "agent-defaults", "#g-ts", "settings.toolSet", "defaults.tool_set", ["tool set", "tools", "mcp"], ["工具組", "工具"]],
|
|
45
49
|
["defaults.dashboard", "agent-defaults", "#g-dash", "settings.dashboardAccess", "defaults.dashboard", ["dashboard access", "dashboard features", "permissions", "appearance tools", "task board", "web settings"], ["Dashboard 權限", "權限", "外觀", "任務看板", "網頁設定"]],
|
|
@@ -4,6 +4,10 @@ import { register } from "/assets/app-i18n.js";
|
|
|
4
4
|
|
|
5
5
|
register("settings", {
|
|
6
6
|
en: {
|
|
7
|
+
noticeTopic: "Topic", noticeDm: "Direct message (topic fallback)", noticeOff: "Off",
|
|
8
|
+
webNotifyLogin: "Web sign-in notices", dailyTips: "Daily tips",
|
|
9
|
+
webNotifyLoginHint: "DM the signing-in operator or public-link owner; local guest notices go to the owning connection’s first configured admin. Missing account or failed DM falls back to the topic. Off suppresses operator notices only; guest and public-link security notices remain in the topic. Existing default: Topic; new fleets: DM.",
|
|
10
|
+
dailyTipsHint: "DM the General connection’s first configured admin, with topic fallback. Default: Topic. /tips still posts where invoked.",
|
|
7
11
|
title: "Settings",
|
|
8
12
|
myAgents: "My Agents",
|
|
9
13
|
bots: "Bots",
|
|
@@ -108,6 +112,7 @@ register("settings", {
|
|
|
108
112
|
dashboard_appearance: "Its look (theme, pictures, widgets, effects, mod proposals)",
|
|
109
113
|
dashboard_task_board: "The task board",
|
|
110
114
|
dashboard_web_settings: "Your web appearance settings",
|
|
115
|
+
dashboard_office: "Office: walk, speak and act in the Office view",
|
|
111
116
|
dashboardInherit: "Fleet default ({0})",
|
|
112
117
|
dashboardOn: "allowed",
|
|
113
118
|
dashboardOff: "off",
|
|
@@ -570,6 +575,8 @@ register("settings", {
|
|
|
570
575
|
removeItem: "Remove {0}",
|
|
571
576
|
zeroDisabled: "0 = off",
|
|
572
577
|
optional: "optional",
|
|
578
|
+
replyTriggers: "Native replies trigger ClassicBot",
|
|
579
|
+
replyTriggersHint: "Off by default: only an exact @bot mention triggers a collab room. On also accepts one-hop replies to that bot. Forwarded content never counts; fleet-topic traffic is unchanged.",
|
|
573
580
|
collabMode: "Collaboration mode",
|
|
574
581
|
contextLines: "Context lines",
|
|
575
582
|
refresh: "Refresh",
|
|
@@ -672,6 +679,10 @@ register("settings", {
|
|
|
672
679
|
se_externalEmojiHint: "使用其他伺服器的 emoji,bot 在要加反應的頻道需有「使用外部表情符號」權限。",
|
|
673
680
|
se_unavailable: "無法使用",
|
|
674
681
|
se_hint: "留空即使用預設值。預覽由 AgEnD 依 bot 實際 react 的方式解析。進度前綴不是反應,而是進度訊息開頭的 emoji。",
|
|
682
|
+
noticeTopic: "頻道", noticeDm: "私訊(失敗時改送頻道)", noticeOff: "關閉",
|
|
683
|
+
webNotifyLogin: "網頁登入通知", dailyTips: "每日提示",
|
|
684
|
+
webNotifyLoginHint: "私訊登入的管理者或公開連結擁有者;本機訪客登入通知送給所屬連線的第一位設定管理員。帳號不明或私訊失敗時改送原頻道。關閉只停用管理者登入通知;訪客與公開連結安全通知仍送頻道。既有預設:頻道;新 fleet:私訊。",
|
|
685
|
+
dailyTipsHint: "私訊 General 所屬連線的第一位設定管理員,失敗時改送頻道。預設:頻道。/tips 仍送至呼叫的聊天室。",
|
|
675
686
|
title: "設定",
|
|
676
687
|
myAgents: "我的 Agent",
|
|
677
688
|
bots: "機器人",
|
|
@@ -750,6 +761,7 @@ register("settings", {
|
|
|
750
761
|
dashboard_appearance: "它的外觀(主題、圖片、小工具、特效、mod 提案)",
|
|
751
762
|
dashboard_task_board: "任務看板",
|
|
752
763
|
dashboard_web_settings: "你的網頁外觀設定",
|
|
764
|
+
dashboard_office: "辦公室:在辦公室畫面走動、說話、做動作",
|
|
753
765
|
dashboardInherit: "跟 fleet 預設({0})",
|
|
754
766
|
dashboardOn: "允許",
|
|
755
767
|
dashboardOff: "關閉",
|
|
@@ -1210,6 +1222,8 @@ register("settings", {
|
|
|
1210
1222
|
removeItem: "移除 {0}",
|
|
1211
1223
|
zeroDisabled: "0 = 關閉",
|
|
1212
1224
|
optional: "選填",
|
|
1225
|
+
replyTriggers: "原生回覆觸發 ClassicBot",
|
|
1226
|
+
replyTriggersHint: "預設關閉:協作房間只有精確 @bot 提及才觸發。開啟後也接受對該機器人的單層原生回覆;轉發內容不算。Fleet 話題的訊息接收方式不變。",
|
|
1213
1227
|
collabMode: "協作模式",
|
|
1214
1228
|
contextLines: "上下文行數",
|
|
1215
1229
|
refresh: "重新整理",
|
|
@@ -103,6 +103,7 @@ register("app", {
|
|
|
103
103
|
// #2293: global search — chats, cross-instance messages and tasks (not the sidebar's filter of agents).
|
|
104
104
|
search: "Search", searchTitle: "Search chats, messages between agents and tasks ({0})",
|
|
105
105
|
confirmTitle: "Please confirm", confirmOk: "Continue", cancel: "Cancel",
|
|
106
|
+
sidePanelLeave: "Leave {0} without saving? Your changes there will be lost.", sidePanelDiscard: "Discard changes",
|
|
106
107
|
// The server's own wording (src/locale.ts needs.reason.*), so the web says what Discord says.
|
|
107
108
|
needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_crashed: "Crashed", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
|
|
108
109
|
},
|
|
@@ -143,6 +144,7 @@ register("app", {
|
|
|
143
144
|
needsNav: "等你處理", needsTab: "待處理", needsCount: "{0} 件待處理",
|
|
144
145
|
search: "搜尋", searchTitle: "搜尋聊天、agent 之間的訊息與任務({0})",
|
|
145
146
|
confirmTitle: "請確認", confirmOk: "繼續", cancel: "取消",
|
|
147
|
+
sidePanelLeave: "不儲存就離開「{0}」?你在那裡的修改會遺失。", sidePanelDiscard: "捨棄修改",
|
|
146
148
|
needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_crashed: "已崩潰", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
|
|
147
149
|
},
|
|
148
150
|
});
|
|
@@ -16,7 +16,8 @@ import { onPrefsChanged, savePref } from "./app-prefs.js";
|
|
|
16
16
|
import { applyPageTheme, pageNow, resolvedMode, themesStore } from "./app-themes.js";
|
|
17
17
|
import { ErrorState, Skeleton } from "./ui-states.js";
|
|
18
18
|
import { InstanceNav, RosterNav, namesStore, setNamesPrimary } from "./instance-nav.js";
|
|
19
|
-
import {
|
|
19
|
+
import { usageRailStore } from "./header-tools.js";
|
|
20
|
+
import { RightRail, sidePanelStore } from "./side-panel.js";
|
|
20
21
|
import { OwnerCssKeepBar } from "./owner-css.js";
|
|
21
22
|
import { AgentWidget, fleetBinds, sidebarWidget, sidebarWidgetStore } from "./agent-widget.js";
|
|
22
23
|
|
|
@@ -89,14 +90,17 @@ export function openSearch(preset = {}) {
|
|
|
89
90
|
}
|
|
90
91
|
/**
|
|
91
92
|
* Ctrl+K (⌘K) anywhere opens search; Ctrl+F (⌘F) in an agent's chat opens it narrowed to that agent — elsewhere the
|
|
92
|
-
* browser's own find stays. Not while another dialog is open (search, open already, has its input focused)
|
|
93
|
+
* browser's own find stays. Not while another dialog is open (search, open already, has its input focused); a docked
|
|
94
|
+
* side panel (#2585) is not one.
|
|
93
95
|
*/
|
|
94
96
|
export function handleSearchKey(e) {
|
|
95
97
|
if (!searchOffered(true)) return;
|
|
96
98
|
if (e.isComposing || e.altKey || e.shiftKey || !(isMacLike() ? e.metaKey : e.ctrlKey)) return;
|
|
97
99
|
const key = (e.key || "").toLowerCase();
|
|
98
100
|
if (key !== "k" && key !== "f") return;
|
|
99
|
-
|
|
101
|
+
// #2585: a docked side panel is not modal — the page beside it is in use, so search is too (a result that leaves
|
|
102
|
+
// the page asks first when the panel holds changes: side-panel.js).
|
|
103
|
+
if (typeof document !== "undefined" && document.querySelector("dialog[open]:not(.side-panel)")) return;
|
|
100
104
|
const route = navStore.get().route;
|
|
101
105
|
if (key === "f" && !(route && route.panel === "chat" && route.instance)) return;
|
|
102
106
|
e.preventDefault();
|
|
@@ -127,7 +131,7 @@ export const sidebarWidthStore = createStore({ width: null });
|
|
|
127
131
|
function mainBudget() {
|
|
128
132
|
const vw = viewportWidth();
|
|
129
133
|
if (vw < 1200 || typeof document === "undefined") return vw;
|
|
130
|
-
const rail = document.querySelector(".u-rail");
|
|
134
|
+
const rail = document.querySelector(".shell > .u-rail, .shell > .side-panel"); // #2585: a docked panel in its place
|
|
131
135
|
if (!rail) return vw;
|
|
132
136
|
const w = Math.round(rail.getBoundingClientRect ? rail.getBoundingClientRect().width : 0) || RAIL_W;
|
|
133
137
|
return vw - w;
|
|
@@ -181,11 +185,15 @@ function SidebarResize() {
|
|
|
181
185
|
// once the page has drawn its change.
|
|
182
186
|
const again = () => afterPaint(applyKeptSidebarWidth);
|
|
183
187
|
const offRail = usageRailStore.subscribe(again);
|
|
188
|
+
// #2585: a docked panel in the rail's place (it is published on every render of its editor: only its coming and
|
|
189
|
+
// going changes the room).
|
|
190
|
+
let docked = !!sidePanelStore.get().panel;
|
|
191
|
+
const offPanel = sidePanelStore.subscribe(() => { const now = !!sidePanelStore.get().panel; if (now !== docked) { docked = now; again(); } });
|
|
184
192
|
const offNav = navStore.subscribe(again);
|
|
185
193
|
const w = win();
|
|
186
194
|
if (w && typeof w.addEventListener === "function") w.addEventListener("resize", applyKeptSidebarWidth);
|
|
187
195
|
return () => {
|
|
188
|
-
offRail(); offNav();
|
|
196
|
+
offRail(); offPanel(); offNav();
|
|
189
197
|
if (w && typeof w.removeEventListener === "function") w.removeEventListener("resize", applyKeptSidebarWidth);
|
|
190
198
|
if (activeDrag) activeDrag.end(false); // unmounted mid-drag: nothing kept, nothing left behind
|
|
191
199
|
};
|
|
@@ -787,7 +795,7 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
|
|
|
787
795
|
${viewOnly || guest || !shell.banner ? null : html`<${shell.banner} />`}
|
|
788
796
|
<${Outlet} route=${nav.route} seq=${nav.seq} panels=${panels} />
|
|
789
797
|
</main>
|
|
790
|
-
${guest ? null : html`<${
|
|
798
|
+
${guest ? null : html`<${RightRail} route=${nav.route} />`}
|
|
791
799
|
<${BottomTabs} route=${nav.route} viewOnly=${viewOnly} />
|
|
792
800
|
${shell.dialog ? html`<${shell.dialog.Component} ...${shell.dialog.props} onClose=${closeDialog} />` : null}
|
|
793
801
|
${viewOnly || guest || (nav.route && nav.route.panel === "settings") ? null : html`<${PendingChanges} />`}
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
// tasks — the task board changed: the card numbers a write touched (SSE only: the board reads again when it is shown).
|
|
37
37
|
// offices — the stored offices changed ({ rev }; SSE only: the Office view reads /ui/offices again).
|
|
38
38
|
// office_skins — a skin pack was uploaded or deleted ({ rev }; SSE only: the Office view reads /ui/office-skins again).
|
|
39
|
-
|
|
39
|
+
// office_acts — the agents' live acts in the Office view changed ({ acts }, #2578; a poll page reads /ui/office-acts).
|
|
40
|
+
const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "reactions", "steps_tick", "clock", "traffic", "delegation", "backend_login", "tasks", "offices", "office_skins", "office_acts"];
|
|
40
41
|
const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
|
|
41
42
|
const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
|
|
42
43
|
const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -1348,6 +1348,18 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
|
|
|
1348
1348
|
@media (prefers-reduced-motion: no-preference) { .u-crow { transition: background-color 0.15s var(--ease); } }
|
|
1349
1349
|
.u-rail-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--s-3) var(--s-4); display: flex; flex-direction: column; gap: var(--s-3); }
|
|
1350
1350
|
@media (max-width: 1199px) { .u-rail { display: none; } }
|
|
1351
|
+
/* #2585: the docked side panel (side-panel.js), in the usage rail's place beside the page — a non-modal dialog, so the
|
|
1352
|
+
shared dialog's head, body and actions, laid out as a full-height column. Below the rail's width it is never drawn
|
|
1353
|
+
(the editor is the centred Dialog there). */
|
|
1354
|
+
.dlg.side-panel { position: static; inset: auto; margin: 0; width: 400px; max-width: 400px; height: auto; max-height: none; flex-shrink: 0;
|
|
1355
|
+
display: flex; flex-direction: column; min-height: 0; border-radius: 0; box-shadow: none; border-left: 1px solid var(--border); background: var(--bg); color: var(--text); }
|
|
1356
|
+
.side-panel .dlg-box { flex: 1; min-height: 0; max-height: none; }
|
|
1357
|
+
.side-panel .dlg-head { padding: var(--s-3) var(--s-3) var(--s-3) var(--s-5); border-bottom: 1px solid var(--border); }
|
|
1358
|
+
.side-panel .dlg-body { flex: 1; min-height: 0; padding-top: var(--s-4); }
|
|
1359
|
+
.side-panel .dlg-actions { padding: var(--s-3) var(--s-5); border-top: 1px solid var(--border); }
|
|
1360
|
+
@media (max-width: 1199px) { .dlg.side-panel { display: none; } }
|
|
1361
|
+
/* A toast (Offices saved.) beside a docked panel, not over its Cancel and Save. */
|
|
1362
|
+
@media (min-width: 1200px) { .shell:has(> .side-panel) .toasts { right: calc(400px + var(--s-6)); } }
|
|
1351
1363
|
@media (max-width: 899px) {
|
|
1352
1364
|
.hide-narrow { display: none; }
|
|
1353
1365
|
.v-id { padding-left: var(--s-3); padding-right: var(--s-3); }
|
|
@@ -1731,7 +1743,7 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
1731
1743
|
.oc-office > .oc-bar { margin: 0; padding: var(--s-3) var(--s-5) var(--s-2); }
|
|
1732
1744
|
.oc-office > .oc-legend { margin: 0 var(--s-5) var(--s-2); }
|
|
1733
1745
|
.oo { flex: 1 0 auto; display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; width: 100%; }
|
|
1734
|
-
.oo > :not(.oo-stage) { margin-left: var(--s-5); margin-right: var(--s-5); }
|
|
1746
|
+
.oo > :not(.oo-stage):not(dialog) { margin-left: var(--s-5); margin-right: var(--s-5); } /* not the editors' dialogs: they centre themselves (#2585) */
|
|
1735
1747
|
.oo-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
|
|
1736
1748
|
.oo-pick { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--text-2); }
|
|
1737
1749
|
.oo-pick .tb-select { max-width: 200px; }
|
|
@@ -1839,13 +1851,16 @@ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
|
|
|
1839
1851
|
.ol-bar { position: absolute; left: 0; right: 0; bottom: 0; z-index: 8; display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); padding: var(--s-2);
|
|
1840
1852
|
background: var(--surface); border-top: 1px solid var(--border); }
|
|
1841
1853
|
.ol-actions { display: inline-flex; gap: var(--s-2); margin-left: auto; }
|
|
1854
|
+
/* #2585: Edit layout's controls docked in the side panel (a wide desk): one column, the waypoint's own row wrapping. */
|
|
1855
|
+
.ol-side { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-3); }
|
|
1856
|
+
.ol-side .ol-corridors { display: flex; flex-wrap: wrap; gap: var(--s-2); }
|
|
1842
1857
|
.ol-corridors { display: inline-flex; flex-wrap: wrap; gap: 4px; }
|
|
1843
1858
|
.ol-msg { flex-basis: 100%; margin: 0; }
|
|
1844
1859
|
@media (max-width: 640px) {
|
|
1845
1860
|
/* A phone: the bars at the column's narrower padding; the stage at least most of a screen; no overview (as the canvas). */
|
|
1846
1861
|
.oc-office > .oc-bar { padding: var(--s-2) var(--s-3); }
|
|
1847
1862
|
.oc-office > .oc-legend { margin: 0 var(--s-3) var(--s-2); }
|
|
1848
|
-
.oo > :not(.oo-stage) { margin-left: var(--s-3); margin-right: var(--s-3); }
|
|
1863
|
+
.oo > :not(.oo-stage):not(dialog) { margin-left: var(--s-3); margin-right: var(--s-3); }
|
|
1849
1864
|
.oo-stage { min-height: max(300px, calc(100dvh - 300px)); }
|
|
1850
1865
|
.oo-pick .tb-select { max-width: 150px; }
|
|
1851
1866
|
.oo-mini { display: none; }
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// #2585 (the user): settings-like editors docked on the right. On a screen wide enough for the usage rail (RAIL_MEDIA,
|
|
2
|
+
// #1673) an editor opens in the rail's place, beside the page — not modal: the page stays in view and in use (the
|
|
3
|
+
// Office re-fits beside it). While one is open the rail steps aside; it comes back as it was when the panel closes (its
|
|
4
|
+
// shown/hidden choice is never touched). Narrower, the same editor is the shared Dialog (centred; a phone's sheet).
|
|
5
|
+
//
|
|
6
|
+
// <${SidePanel} title=… onClose=… actions=… busy=…>…body…</${SidePanel}>: rendered = open, the owner unmounts it to
|
|
7
|
+
// close (like Dialog). Docked, the owner renders nothing itself: it publishes its title, body and actions to the shell's
|
|
8
|
+
// host (sidePanelStore) on every render, so the body keeps the owner's state and closures; the owner going (a close, a
|
|
9
|
+
// navigation) takes the panel with it. Esc in the panel and its × close it (not while `busy`); focus moves into it when
|
|
10
|
+
// it opens and goes back to what opened it when it closes. Docked, the body renders under the shell, not under its
|
|
11
|
+
// owner: a context provided above the owner does not reach it (pass what it needs as props, or read a store).
|
|
12
|
+
// `dirty`: the editor holds unsaved changes. Docked, the page beside it is in use (its links, search): leaving the page
|
|
13
|
+
// then closes the panel as Cancel does — and with changes, asks first (the app's leave guard; one at a time).
|
|
14
|
+
// `dock` false keeps it from docking (the Office in fullscreen, where the shell is out of view); `fallback`, when given,
|
|
15
|
+
// is what it is below the rail's width (or not docked) instead of the Dialog — Edit layout's bar over the canvas.
|
|
16
|
+
import { html, useEffect, useLayoutEffect, useRef } from "./app-html.js";
|
|
17
|
+
import { createStore, useStore } from "./app-store.js";
|
|
18
|
+
import { t } from "./app-i18n.js";
|
|
19
|
+
import { Icon } from "./ui-icons.js";
|
|
20
|
+
import { Dialog } from "./ui-dialog.js";
|
|
21
|
+
import { confirmDialog } from "./ui-confirm.js";
|
|
22
|
+
import { navigate, setLeaveGuard } from "./app-nav.js";
|
|
23
|
+
import { UsageRail, useRailWidth } from "./header-tools.js";
|
|
24
|
+
|
|
25
|
+
/** { panel: { id, title, label, body, actions, busy, onClose } | null } — the one docked panel. */
|
|
26
|
+
export const sidePanelStore = createStore({ panel: null });
|
|
27
|
+
let seq = 0;
|
|
28
|
+
const clear = (id) => { if (sidePanelStore.get().panel?.id === id) sidePanelStore.set({ panel: null }); };
|
|
29
|
+
|
|
30
|
+
/** An editor: docked on a wide screen, the shared Dialog (`wide`) elsewhere. */
|
|
31
|
+
export function SidePanel({ title, label, onClose, actions, busy = false, dirty = false, dock = true, fallback, children }) {
|
|
32
|
+
const docked = useRailWidth() && dock;
|
|
33
|
+
const id = useRef(0);
|
|
34
|
+
if (!id.current) id.current = ++seq;
|
|
35
|
+
const opener = useRef(undefined);
|
|
36
|
+
if (opener.current === undefined) opener.current = typeof document !== "undefined" ? document.activeElement : null;
|
|
37
|
+
useLayoutEffect(() => {
|
|
38
|
+
if (docked) sidePanelStore.set({ panel: { id: id.current, title, label, body: children, actions, busy, onClose } });
|
|
39
|
+
else clear(id.current);
|
|
40
|
+
});
|
|
41
|
+
useLayoutEffect(() => () => {
|
|
42
|
+
const host = typeof document !== "undefined" ? document.querySelector(".side-panel") : null;
|
|
43
|
+
const inside = !!host && (host.contains(document.activeElement) || document.activeElement === document.body);
|
|
44
|
+
clear(id.current);
|
|
45
|
+
// Focus back to what opened it — when it was in the panel (or nowhere): a person who moved on keeps their place.
|
|
46
|
+
const back = opener.current;
|
|
47
|
+
if ((inside || !host) && back && typeof back.focus === "function" && document.contains?.(back)) back.focus();
|
|
48
|
+
}, []);
|
|
49
|
+
// Leaving the page with changes in the docked panel: asked in the app's dialog; yes = Cancel, then on to there.
|
|
50
|
+
const live = useRef(null);
|
|
51
|
+
live.current = { title, onClose };
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!docked || !dirty) return undefined;
|
|
54
|
+
let asking = false;
|
|
55
|
+
return setLeaveGuard((_to, path) => {
|
|
56
|
+
if (!asking) {
|
|
57
|
+
asking = true;
|
|
58
|
+
confirmDialog({ message: t("app.sidePanelLeave", live.current.title), confirmLabel: t("app.sidePanelDiscard"), danger: true }).then((yes) => {
|
|
59
|
+
asking = false;
|
|
60
|
+
if (!yes) return;
|
|
61
|
+
live.current.onClose();
|
|
62
|
+
navigate(path, { force: true });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
});
|
|
67
|
+
}, [docked, dirty]);
|
|
68
|
+
if (docked) return null;
|
|
69
|
+
if (fallback !== undefined) return fallback;
|
|
70
|
+
return html`<${Dialog} title=${title} label=${label} onClose=${onClose} actions=${actions} busy=${busy} wide=${true}>${children}</${Dialog}>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The docked panel itself: a non-modal dialog in the rail's place. */
|
|
74
|
+
function SidePanelHost({ panel }) {
|
|
75
|
+
const box = useRef(null);
|
|
76
|
+
// Into the panel as it opens (its box: no ring; Tab moves on), once per panel.
|
|
77
|
+
useLayoutEffect(() => { if (box.current && typeof box.current.focus === "function") box.current.focus({ preventScroll: true }); }, [panel.id]);
|
|
78
|
+
const close = () => { if (!panel.busy) panel.onClose(); };
|
|
79
|
+
const onKeyDown = (e) => { if (e.key === "Escape" && !e.defaultPrevented) { e.preventDefault(); close(); } };
|
|
80
|
+
return html`<dialog open data-trust="dialog" class="dlg side-panel" aria-label=${panel.label || panel.title} onKeyDown=${onKeyDown}>
|
|
81
|
+
<div class="dlg-box" tabindex="-1" ref=${box}>
|
|
82
|
+
<div class="dlg-head"><h2 class="dlg-title">${panel.title}</h2>
|
|
83
|
+
<button type="button" class="icon-btn dlg-x" aria-label=${t("app.close")} title=${t("app.close")} onClick=${close}><${Icon} name="close" /></button></div>
|
|
84
|
+
<div class="dlg-body">${panel.body}</div>
|
|
85
|
+
${panel.actions ? html`<div class="dlg-actions">${panel.actions}</div>` : null}
|
|
86
|
+
</div>
|
|
87
|
+
</dialog>`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The shell's right-hand column: a docked panel when one is open (wide screens), else the usage rail. */
|
|
91
|
+
export function RightRail({ route }) {
|
|
92
|
+
const { panel } = useStore(sidePanelStore);
|
|
93
|
+
const wide = useRailWidth();
|
|
94
|
+
return panel && wide ? html`<${SidePanelHost} panel=${panel} />` : html`<${UsageRail} route=${route} />`;
|
|
95
|
+
}
|
package/dist/web-api.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { WebEffectService } from "./web-effect-service.js";
|
|
|
10
10
|
import type { ThemeStore } from "./web-theme-store.js";
|
|
11
11
|
import type { OfficeStore, OfficeWriteLimiter } from "./web-office-store.js";
|
|
12
12
|
import { type OfficeSkinStore } from "./web-office-skins.js";
|
|
13
|
+
import type { OfficeActs } from "./web-office-acts.js";
|
|
13
14
|
import type { WebAppearance } from "./web-appearance.js";
|
|
14
15
|
import type { SendOpts } from "./channel/types.js";
|
|
15
16
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
@@ -196,6 +197,8 @@ export interface WebApiContext {
|
|
|
196
197
|
readonly officeStore?: OfficeStore;
|
|
197
198
|
/** How often a person may save the offices (absent: no limit, as in a test without it). */
|
|
198
199
|
readonly officeWrites?: OfficeWriteLimiter;
|
|
200
|
+
/** #2578: the agents' live acts in the Office view (office_set); absent: /ui/office-acts is not offered. */
|
|
201
|
+
readonly officeActs?: Pick<OfficeActs, "live">;
|
|
199
202
|
/** #2513: the Office view's uploaded skin packs (web-office-skins/); absent: /ui/office-skins is not offered. */
|
|
200
203
|
readonly officeSkins?: OfficeSkinStore;
|
|
201
204
|
/** How often a person may upload or delete a skin pack (absent: no limit). */
|
package/dist/web-api.js
CHANGED
|
@@ -2212,6 +2212,17 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2212
2212
|
});
|
|
2213
2213
|
return true;
|
|
2214
2214
|
}
|
|
2215
|
+
// #2578: GET /ui/office-acts { acts }: the agents' live acts (office_set), read as the offices are (the public link
|
|
2216
|
+
// too, which has no stream); every change also comes as `office_acts` on the stream. Nothing here writes one.
|
|
2217
|
+
if (method === "GET" && path === "/ui/office-acts") {
|
|
2218
|
+
const acts = ctx.officeActs;
|
|
2219
|
+
if (!acts) {
|
|
2220
|
+
json(res, 404, { error: "Office acts are not kept on this fleet" });
|
|
2221
|
+
return true;
|
|
2222
|
+
}
|
|
2223
|
+
json(res, 200, { acts: acts.live(), now: Date.now() }); // now: the host's clock, for the page's offset
|
|
2224
|
+
return true;
|
|
2225
|
+
}
|
|
2215
2226
|
if (path === "/ui/office-skins" || path.startsWith("/ui/office-skins/"))
|
|
2216
2227
|
return handleOfficeSkins(req, res, url, ctx, path, method);
|
|
2217
2228
|
// ── #1630: avatars and Discord server emoji, same-origin (the page never loads a platform's CDN; img-src stays 'self') ──
|