Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2474-settings-pages-readonly.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A read-only window no longer draws the write controls on the seven Settings pages that still had them: **General** (theme, accent and the notification switches), **Bots** (both Main Bot choices and the animation picker), a Bot's own settings, a group chat's settings, **Connectors**, **Capabilities** and **Secrets**. Each page now states its facts as plain text — the theme and accent in use, who holds the Main Bot role, the animation setting, a Bot's role, model, folder and instructions, a group's members and notice, each connector's state, each capability and each stored key with who may use it — because the host refuses every one of those writes with `bot/read-only`. **New Bot** stays as an entry, since the form behind it already explains the lock, and a writable window is unchanged. ([#2474](https://github.com/ThinkFlowLab/FlyBot/issues/2474))
173 changes: 170 additions & 3 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,8 @@ var ZH = {
"Unread marks, badges, and the brand mark.": "未读标记、徽标和品牌标志。",
"Theme accent": "主题强调色",
"Custom accent": "自定义强调色",
On: "开",
None: "无",
"Bots · {count}": "Bot · {count}",
"First in the sidebar. Pick another Bot to hand it the role; {name} then stops being a Main Bot.": "排在侧栏最上面。选另一个 Bot 就会把主 Bot 交给它,{name} 随之不再是主 Bot。",
"Choose a Bot…": "选择一个 Bot…",
Expand Down Expand Up @@ -6682,6 +6684,33 @@ function SecretsPage({ roster, actions, run, Section }) {
const [open, setOpen] = (0, import_react9.useState)(null);
const [adding, setAdding] = (0, import_react9.useState)(false);
const secrets = roster.secrets ?? [];
if (lockedWindow(roster)) {
return [
(0, import_react9.createElement)(
"p",
{ key: "about", className: "bt-secret-about" },
t("Keys your Bots asked for. Bots never see the values: they use them as "),
(0, import_react9.createElement)("code", null, "$DSH_SECRET_NAME"),
t(" in their shell, and every value is replaced by "),
(0, import_react9.createElement)("code", null, "[secret:NAME]"),
t(" before anything goes to a model.")
),
(0, import_react9.createElement)(
Section,
{ key: "list", title: t("Secrets · {count}", { count: secrets.length }) },
secrets.length === 0 ? (0, import_react9.createElement)("div", { className: "bt-set-row" }, (0, import_react9.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : secrets.map((secret) => (0, import_react9.createElement)(
"div",
{ key: secret.name, className: "bt-set-row" },
(0, import_react9.createElement)(
"span",
{ className: "bt-set-copy" },
(0, import_react9.createElement)("span", { className: "bt-secret-title" }, (0, import_react9.createElement)(KeyIcon), (0, import_react9.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react9.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null),
(0, import_react9.createElement)("span", { className: "bt-set-hint" }, scopeLabel(secret.scope, roster))
)
))
)
];
}
return [
(0, import_react9.createElement)(
"p",
Expand Down Expand Up @@ -14113,6 +14142,27 @@ function CapabilitiesPage({ roster, actions, run }) {
setConfirming("");
void run(actions.deleteCapability(item.name));
};
if (lockedWindow(roster)) {
return (0, import_react27.createElement)(
"section",
{ className: "bt-set-section" },
(0, import_react27.createElement)("h3", null, t("Shared capabilities")),
(0, import_react27.createElement)(
"div",
{ className: "bt-set-card bt-connectors-card" },
items.length === 0 ? (0, import_react27.createElement)("div", { className: "bt-set-row" }, (0, import_react27.createElement)("span", { className: "bt-set-hint" }, t("No capabilities saved"))) : items.map((item) => (0, import_react27.createElement)(
"div",
{ key: item.name, className: "bt-set-row" },
(0, import_react27.createElement)(
"span",
{ className: "bt-set-copy" },
(0, import_react27.createElement)("span", { className: "bt-bs-label" }, item.name),
(0, import_react27.createElement)("span", { className: "bt-set-hint" }, item.description || (item.kind === "pack" ? `${t("Pack")}: ${(item.items ?? []).join(", ") || t("No items")}` : item.kind === "mcp" ? item.transport === "stdio" ? t("Local command") : t("HTTP server") : t("Skill")))
)
))
)
);
}
return (0, import_react27.createElement)(
"section",
{ className: "bt-set-section" },
Expand Down Expand Up @@ -14862,18 +14912,55 @@ function BotSettingsForm({ bot, roster, actions, run }) {
return () => clearTimeout(timer);
}, [confirming]);
if (bot.remote) return null;
(0, import_react30.useEffect)(() => {
void actions.refreshModels?.();
}, [bot.id]);
const isMainBot = isMainOf(roster, bot.id);
const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1;
const isTile = roster.mainBotId === bot.id;
if (lockedWindow(roster)) {
return (0, import_react30.createElement)(
"div",
{ className: "bt-bs" },
(0, import_react30.createElement)(
"div",
{ className: "bt-bs-hero" },
(0, import_react30.createElement)(BotMark, { bot, size: 64, live: false }),
(0, import_react30.createElement)(
"div",
{ className: "bt-bs-who" },
(0, import_react30.createElement)("span", { className: "bt-bs-name" }, bot.name),
(0, import_react30.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot"))
)
),
(0, import_react30.createElement)(
Card,
{ title: t("Profile") },
(0, import_react30.createElement)(Row, { label: t("Label"), hint: bot.role || t("Bot") })
),
(0, import_react30.createElement)(
Card,
{ title: t("Model") },
(0, import_react30.createElement)(Row, { label: t("Model"), hint: modelOf(bot, roster)?.name ?? modelHint(bot, roster) })
),
(0, import_react30.createElement)(
Card,
{ title: t("Working folder") },
(0, import_react30.createElement)(Row, { label: t("Folder"), hint: bot.workDir || t("Not set") })
),
(0, import_react30.createElement)(
Card,
{ title: t("Instructions") },
(0, import_react30.createElement)(Row, { label: t("What this Bot does"), hint: bot.instructions || t("None"), stack: true })
)
);
}
const save = (patch) => run(actions.updateBot(bot.id, patch));
const duplicate = () => run(actions.duplicateBot(bot.id).then((copy2) => actions.openSession(copy2.id)));
const remove = () => {
if (confirming) void run(actions.deleteBot(bot.id));
else setConfirming(true);
};
(0, import_react30.useEffect)(() => {
void actions.refreshModels?.();
}, [bot.id]);
return (0, import_react30.createElement)(
"div",
{ className: "bt-bs" },
Expand Down Expand Up @@ -15190,6 +15277,38 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) {
const admin = roster.byId[room2.admin];
const mode = shownMode(room2, roster);
const modeHint = ROOM_MODES.find(([id]) => id === mode)?.[2] ?? "";
if (lockedWindow(roster)) {
return (0, import_react31.createElement)(
"div",
{ className: "bt-bs" },
(0, import_react31.createElement)(
"div",
{ className: "bt-bs-hero" },
(0, import_react31.createElement)(RoomAvatar, { room: room2, roster, size: 64 }),
(0, import_react31.createElement)(
"div",
{ className: "bt-bs-who" },
(0, import_react31.createElement)("span", { className: "bt-bs-name" }, room2.name),
(0, import_react31.createElement)("span", { className: "bt-bs-sub" }, roomSubtitle(room2, roster))
)
),
(0, import_react31.createElement)(
Card,
{ title: t("Members · {count}", { count: face ? people2.length : members.length + humans.length + remotes.length }) },
face ? people2.map((person) => (0, import_react31.createElement)(Row, { key: person.key, label: facePersonLabel(person), hint: t("Human") })) : [
// The stored name, cleaned by the same reader the friend's-Bot rows use (#2474).
humans.map((person) => (0, import_react31.createElement)(Row, { key: person.id, label: remoteBotName(person, person.id), hint: t("Human") })),
members.map((member) => (0, import_react31.createElement)(Row, {
key: member.id,
label: member.name,
hint: [room2.admin === member.id ? t("Admin") : "", room2.taskLead === member.id ? t("Task lead") : ""].filter(Boolean).join(" · ") || t("Bot")
})),
remotes.map((person) => (0, import_react31.createElement)(Row, { key: person.handle || person.id, label: person.label || remoteBotTitle(person), hint: t("Friend's Bot") }))
]
),
room2.notice ? (0, import_react31.createElement)(Card, { title: t("Notice") }, (0, import_react31.createElement)(Row, { label: t("Group notice"), hint: room2.notice, stack: true })) : null
);
}
const update = (patch) => run(actions.updateRoom(room2.id, patch));
const openBot = (id) => {
actions.closeSettings();
Expand Down Expand Up @@ -18743,6 +18862,22 @@ function ConnectorRow({ connector, roster, actions }) {
}
function ConnectorsPage({ roster, actions }) {
const list = roster?.connectors ?? [];
if (lockedWindow(roster)) {
return (0, import_react39.createElement)(
"section",
{ className: "bt-set-section" },
(0, import_react39.createElement)("div", { className: "bt-set-card bt-connectors-card" }, list.length === 0 ? (0, import_react39.createElement)("div", { className: "bt-set-row" }, (0, import_react39.createElement)("span", { className: "bt-set-hint" }, t("No connectors"))) : list.map((connector) => (0, import_react39.createElement)(
"div",
{ key: connector.id, className: "bt-set-row" },
(0, import_react39.createElement)(
"span",
{ className: "bt-set-copy" },
(0, import_react39.createElement)("span", { className: "bt-bs-label" }, connector.name),
(0, import_react39.createElement)("span", { className: "bt-set-hint" }, statusText2(connector))
)
)))
);
}
return (0, import_react39.createElement)(
"section",
{ className: "bt-set-section" },
Expand Down Expand Up @@ -19131,6 +19266,23 @@ function GeneralPage({ roster, actions, run }) {
clearTimeout(timer.current);
timer.current = setTimeout(() => run(actions.setPrefs({ accent: value })), 350);
};
if (lockedWindow(roster)) {
const accent = ACCENT_INKS.find((ink) => INKS[ink][0] === prefs.accent);
return [
(0, import_react42.createElement)(
SetSection,
{ key: "look", title: t("Appearance") },
(0, import_react42.createElement)(SetRow, { label: t("Theme"), hint: t(themes[current].label) }),
(0, import_react42.createElement)(SetRow, { label: t("Accent"), hint: accent ? t(capitalize2(accent)) : prefs.accent ?? t("Default") })
),
(0, import_react42.createElement)(
SetSection,
{ key: "notify", title: t("Notifications") },
(0, import_react42.createElement)(SetRow, { label: t("In other chats"), hint: prefs.notifications !== false ? t("On") : t("Off") }),
NOTIFY_KINDS.map(([kind, label]) => (0, import_react42.createElement)(SetRow, { key: kind, label: t(label), hint: prefs.notifications !== false && prefs.notify?.[kind] !== false ? t("On") : t("Off") }))
)
];
}
return [
(0, import_react42.createElement)(
SetSection,
Expand Down Expand Up @@ -19195,6 +19347,14 @@ function MainBotsSection({ roster, actions, run }) {
const [first, ...others] = mains;
const own = roster.bots.filter((bot) => !bot.remote);
const candidates = own.filter((bot) => !isMainOf(roster, bot.id));
if (lockedWindow(roster)) {
return (0, import_react42.createElement)(
SetSection,
{ title: t("Main Bot") },
(0, import_react42.createElement)(SetRow, { label: t("Main Bot"), hint: first ? first.name : void 0 }),
others.map((bot) => (0, import_react42.createElement)(SetRow, { key: bot.id, label: bot.name, hint: t("Also a Main Bot.") }))
);
}
return (0, import_react42.createElement)(
SetSection,
{ title: t("Main Bot") },
Expand Down Expand Up @@ -19234,6 +19394,13 @@ function MainBotsSection({ roster, actions, run }) {
var MOTIONS = [["quiet", "Quiet"], ["normal", "Normal"], ["lively", "Lively"]];
function MotionSection({ roster, actions, run }) {
const motion = roster.prefs?.motion === "quiet" || roster.prefs?.motion === "lively" ? roster.prefs.motion : "normal";
if (lockedWindow(roster)) {
return (0, import_react42.createElement)(
SetSection,
{ title: t("Animation") },
(0, import_react42.createElement)(SetRow, { label: t("Bot animation"), hint: t(MOTIONS.find(([id]) => id === motion)?.[1] ?? "Normal") })
);
}
return (0, import_react42.createElement)(
SetSection,
{ title: t("Animation") },
Expand Down
26 changes: 23 additions & 3 deletions core/src/client/bot-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { lookOf, BotMark } from './mark.js'
import { ModelMenu, modelOf } from './model-menu.js'
import { ModelTestRow, MODEL_TEST_CSS } from './model-test.js'
import { FolderMenu } from './folder-menu.js'
import { BotSchedules } from './schedules.js'
import { BotSchedules, lockedWindow } from './schedules.js'
import { BotCapabilitiesCard } from './capability-settings.js'
import { BotConnectorsCard } from './connector-bots.js'
import { memoryCountText, useMemoryCount } from './memory-panel.js'
Expand Down Expand Up @@ -205,14 +205,34 @@ export function BotSettingsForm({ bot, roster, actions, run }) {
return () => clearTimeout(timer)
}, [confirming])
if (bot.remote) return null
// DSH settings can change the model list at any time; the hint should match it. The hook sits
// above the read-only branch because the lock can lift under a page that is already mounted:
// both views must run the same hooks in the same order, or React unmounts the whole dialog (#2474).
useEffect(() => { void actions.refreshModels?.() }, [bot.id])
const isMainBot = isMainOf(roster, bot.id)
const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1
const isTile = roster.mainBotId === bot.id
// The host refuses every write of this form in a read-only window, so the page keeps the
// Bot's own facts as text and draws no control (#2474).
if (lockedWindow(roster)) {
return h('div', { className: 'bt-bs' },
h('div', { className: 'bt-bs-hero' },
h(BotMark, { bot, size: 64, live: false }),
h('div', { className: 'bt-bs-who' },
h('span', { className: 'bt-bs-name' }, bot.name),
h('span', { className: 'bt-bs-sub' }, [bot.role, isMainBot ? t('Main Bot') : ''].filter(Boolean).join(' · ') || t('Bot')))),
h(Card, { title: t('Profile') },
h(Row, { label: t('Label'), hint: bot.role || t('Bot') })),
h(Card, { title: t('Model') },
h(Row, { label: t('Model'), hint: modelOf(bot, roster)?.name ?? modelHint(bot, roster) })),
h(Card, { title: t('Working folder') },
h(Row, { label: t('Folder'), hint: bot.workDir || t('Not set') })),
h(Card, { title: t('Instructions') },
h(Row, { label: t('What this Bot does'), hint: bot.instructions || t('None'), stack: true })))
}
const save = patch => run(actions.updateBot(bot.id, patch))
const duplicate = () => run(actions.duplicateBot(bot.id).then(copy => actions.openSession(copy.id)))
const remove = () => { if (confirming) void run(actions.deleteBot(bot.id)); else setConfirming(true) }
// DSH settings can change the model list at any time; the hint should match it.
useEffect(() => { void actions.refreshModels?.() }, [bot.id])
return h('div', { className: 'bt-bs' },
h('div', { className: 'bt-bs-hero' },
h('div', { className: 'bt-avatar-anchor' },
Expand Down
15 changes: 15 additions & 0 deletions core/src/client/capability-settings.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createElement as h, useEffect, useState } from 'react'
import { lockedWindow } from './schedules.js'
import { t } from './i18n.js'

const blank = () => ({ name: '', kind: 'mcp', description: '', transport: 'stdio', command: '', argsText: '', envText: '{}', url: '', headersText: '{}', content: '', items: [] })
Expand Down Expand Up @@ -133,6 +134,20 @@ export function CapabilitiesPage({ roster, actions, run }) {
setConfirming('')
void run(actions.deleteCapability(item.name))
}
// The host refuses saving, deleting and approving a capability in a read-only window, so
// the page lists what it holds as text (#2474).
if (lockedWindow(roster)) {
return h('section', { className: 'bt-set-section' },
h('h3', null, t('Shared capabilities')),
h('div', { className: 'bt-set-card bt-connectors-card' },
items.length === 0 ? h('div', { className: 'bt-set-row' }, h('span', { className: 'bt-set-hint' }, t('No capabilities saved')))
: items.map(item => h('div', { key: item.name, className: 'bt-set-row' },
h('span', { className: 'bt-set-copy' },
h('span', { className: 'bt-bs-label' }, item.name),
h('span', { className: 'bt-set-hint' }, item.description || (item.kind === 'pack'
? `${t('Pack')}: ${(item.items ?? []).join(', ') || t('No items')}`
: item.kind === 'mcp' ? (item.transport === 'stdio' ? t('Local command') : t('HTTP server')) : t('Skill'))))))))
}
return h('section', { className: 'bt-set-section' },
h('h3', null, t('Shared capabilities')),
h('div', { className: 'bt-actions' },
Expand Down
12 changes: 12 additions & 0 deletions core/src/client/connectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useMorph } from './motion.js'
import { MaskedInput } from './secret-card.js'
import { ConnectorIcon } from './icons.js'
import { SwitchRow } from './bot-settings.js'
import { lockedWindow } from './schedules.js'
import { ConnectorBots, usageText } from './connector-bots.js'
import { t } from './i18n.js'
import { copiedToClipboard } from '../shared/remote-label.js'
Expand Down Expand Up @@ -277,6 +278,17 @@ function ConnectorRow({ connector, roster, actions }) {
// Settings → Connectors: one row per connector.
export function ConnectorsPage({ roster, actions }) {
const list = roster?.connectors ?? []
// Every connect, disconnect and send is refused in a read-only window, so the page says
// what each connector is doing instead of drawing its row (#2474).
if (lockedWindow(roster)) {
return h('section', { className: 'bt-set-section' },
h('div', { className: 'bt-set-card bt-connectors-card' }, list.length === 0
? h('div', { className: 'bt-set-row' }, h('span', { className: 'bt-set-hint' }, t('No connectors')))
: list.map(connector => h('div', { key: connector.id, className: 'bt-set-row' },
h('span', { className: 'bt-set-copy' },
h('span', { className: 'bt-bs-label' }, connector.name),
h('span', { className: 'bt-set-hint' }, statusText(connector)))))))
}
return h('section', { className: 'bt-set-section' },
h('div', { className: 'bt-set-card bt-connectors-card' }, list.length === 0
? h('div', { className: 'bt-set-row' }, h('span', { className: 'bt-set-hint' }, t('No connectors')))
Expand Down
Loading
Loading