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
49 changes: 47 additions & 2 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -19793,6 +19793,49 @@ function PermissionRow({ bot, roster, actions, run }) {
)
);
}
function ReadOnlyOverview({ bot, roster }) {
const shown = modelOf(bot, roster);
const models = roster.models ?? [];
const catalog = roster.permissions;
const preset = catalog?.options?.find((option) => option.value === (bot.permission ?? catalog?.defaultPreset));
const folder = bot.workDir ?? "";
return (0, import_react44.createElement)(
import_react44.Fragment,
null,
(0, import_react44.createElement)(
"div",
{ key: "model", className: "bt-drawer-model" },
(0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Model")),
(0, import_react44.createElement)(
"div",
{ className: "bt-model-copy bt-readonly-value", title: shown?.ref },
(0, import_react44.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Not set")),
shown ? (0, import_react44.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? shown.provider : t("{provider} · not available", { provider: shown.provider })) : null
)
),
preset ? (0, import_react44.createElement)(
"div",
{ key: "permission", className: "bt-drawer-model" },
(0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Permissions")),
(0, import_react44.createElement)(
"div",
{ className: "bt-model-copy bt-readonly-value" },
(0, import_react44.createElement)("span", { className: "bt-model-name" }, PERMISSION_LABELS[preset.value] ? t(PERMISSION_LABELS[preset.value]) : preset.name)
)
) : null,
(0, import_react44.createElement)(
"div",
{ key: "folder", className: "bt-drawer-model" },
(0, import_react44.createElement)("div", { className: "bt-section-title" }, t("Working folder")),
(0, import_react44.createElement)(
"div",
{ className: "bt-model-copy bt-readonly-value", title: folder || void 0 },
(0, import_react44.createElement)("span", { className: "bt-model-name" }, folder ? folderName(folder) : t("Team workspace")),
folder ? (0, import_react44.createElement)("span", { className: "bt-model-provider" }, folder) : null
)
)
);
}
function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) {
const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS);
const pages = useBrowserPages((map) => map);
Expand Down Expand Up @@ -20186,9 +20229,11 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
] : null,
bot && !bot.remote && tab === "details" ? [
// P-542/P-487: the overview's own model, permission and folder entries are writes too, and a
// locked window refuses every one of them. Say so once at the top and draw the read half only;
// the name above is already text, and the settings gear still opens the page that explains it.
// locked window refuses every one of them. Say so once at the top and draw the read half only,
// which is the values themselves (P-547) — the name above is already text, and the settings
// gear still opens the page that explains the window.
lockedWindow(roster) ? (0, import_react44.createElement)("div", { key: "read-only", className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
lockedWindow(roster) ? (0, import_react44.createElement)(ReadOnlyOverview, { key: "read", bot, roster }) : null,
lockedWindow(roster) ? null : (0, import_react44.createElement)(
"div",
{ key: "model", className: "bt-drawer-model" },
Expand Down
37 changes: 34 additions & 3 deletions core/src/client/details-drawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useEscape } from './overlay-hooks.js'
import { BotSettingsForm } from './bot-settings.js'
import { FaceLead, FacePersonRow, GroupSettingsForm, ROOM_MODES, facePeople, roomSubtitle, shownMode } from './group-settings.js'
import { ModelMenu, modelOf } from './model-menu.js'
import { FolderMenu } from './folder-menu.js'
import { FolderMenu, folderName } from './folder-menu.js'
import { MemoryRow } from './memory-panel.js'
import { RoomSchedules, ScheduleSummary, lockedWindow } from './schedules.js'
import { Segmented, useGlide, useMorph } from './motion.js'
Expand Down Expand Up @@ -53,6 +53,35 @@ function PermissionRow({ bot, roster, actions, run }) {
}, PERMISSION_LABELS[option.value] ? t(PERMISSION_LABELS[option.value]) : option.name))))
}

// P-547: a locked window refuses the overview's writes, not its reading. The model, permission and
// folder cards kept their writer's form or vanished whole, so their saved values went with the
// controls; here each keeps its title and its value as plain text — the shape the name above
// already has (#2391: the value stays, the control goes) — and no menu, preset or button is drawn.
function ReadOnlyOverview({ bot, roster }) {
const shown = modelOf(bot, roster)
const models = roster.models ?? []
const catalog = roster.permissions
const preset = catalog?.options?.find(option => option.value === (bot.permission ?? catalog?.defaultPreset))
const folder = bot.workDir ?? ''
return h(Fragment, null,
h('div', { key: 'model', className: 'bt-drawer-model' },
h('div', { className: 'bt-section-title' }, t('Model')),
h('div', { className: 'bt-model-copy bt-readonly-value', title: shown?.ref },
h('span', { className: 'bt-model-name' }, shown?.name ?? t('Not set')),
shown ? h('span', { className: 'bt-model-provider' }, shown.served || models.length === 0
? shown.provider
: t('{provider} · not available', { provider: shown.provider })) : null)),
preset ? h('div', { key: 'permission', className: 'bt-drawer-model' },
h('div', { className: 'bt-section-title' }, t('Permissions')),
h('div', { className: 'bt-model-copy bt-readonly-value' },
h('span', { className: 'bt-model-name' }, PERMISSION_LABELS[preset.value] ? t(PERMISSION_LABELS[preset.value]) : preset.name))) : null,
h('div', { key: 'folder', className: 'bt-drawer-model' },
h('div', { className: 'bt-section-title' }, t('Working folder')),
h('div', { className: 'bt-model-copy bt-readonly-value', title: folder || undefined },
h('span', { className: 'bt-model-name' }, folder ? folderName(folder) : t('Team workspace')),
folder ? h('span', { className: 'bt-model-provider' }, folder) : null)))
}

function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) {
const state = useBrowserTabs(map => map[bot.id] ?? EMPTY_TABS)
const pages = useBrowserPages(map => map)
Expand Down Expand Up @@ -379,9 +408,11 @@ export function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, ro
] : null,
bot && !bot.remote && tab === 'details' ? [
// P-542/P-487: the overview's own model, permission and folder entries are writes too, and a
// locked window refuses every one of them. Say so once at the top and draw the read half only;
// the name above is already text, and the settings gear still opens the page that explains it.
// locked window refuses every one of them. Say so once at the top and draw the read half only,
// which is the values themselves (P-547) — the name above is already text, and the settings
// gear still opens the page that explains the window.
lockedWindow(roster) ? h('div', { key: 'read-only', className: 'bt-hint bt-read-only', role: 'status' }, hostText(roster.readOnly)) : null,
lockedWindow(roster) ? h(ReadOnlyOverview, { key: 'read', bot, roster }) : null,
lockedWindow(roster) ? null : h('div', { key: 'model', className: 'bt-drawer-model' },
h('div', { className: 'bt-section-title' }, t('Model')),
h(ModelMenu, { bot, roster, actions, run, wide: true }),
Expand Down
49 changes: 49 additions & 0 deletions core/test/details-overview-readonly.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,52 @@ test('P-544: a read-only group overview says why once and draws the name as plai
assert.deepEqual(modelTriggers(entries), [], 'a group never had a model menu')
assert.deepEqual(permissionRows(entries), [], 'a group never had permission presets')
})

// P-547: the same locked window dropped the overview's Model, Permissions and Working folder
// cards whole, so their values went with the controls even though the name above is already
// "value kept, control removed" (`details-drawer.js`'.bt-drawer-name' as plain text). Product's
// probe on a locked window read `model name drawn = false`, `permission label = false`,
// `workDir = false`; in a writable window all three were true. A locked window refuses the
// writes, not the reading, so the three values stay on the page as text and none of the three
// controls is drawn.
const withModel = { ...bot, model: 'deepseek/deepseek-v4.1-flash', workDir: '/home/xuke/flybot' }
const modelsCatalog = [{ ref: 'deepseek/deepseek-v4.1-flash', name: 'DeepSeek V4.1 Flash', providerName: 'DeepSeek' }]
const rosterModelWith = (readOnly) => ({
...base, bots: [withModel], byId: { a: withModel }, models: modelsCatalog,
...(readOnly === undefined ? {} : { readOnly }),
})
const readOnlyValues = (entries) => entries.filter(({ node }) => hasClass(node, 'bt-readonly-value'))
// The value line itself, without the note under it (the provider, the full path).
const readOnlyNames = (entries) => entries.filter(({ node }) => hasClass(node, 'bt-model-name')).map(({ node }) => ownText(node).trim())
const sectionTitles = (entries) => nodes(entries).filter(node => hasClass(node, 'bt-section-title')).map(node => ownText(node).trim())

test('P-547: a locked overview keeps the model, permission and folder values and draws no control', () => {
slots = []
const entries = tree(rosterModelWith(SENTENCE))
assert.equal(statusLine(entries).length, 1, "the host's sentence is still drawn once")
assert.equal(readOnlyValues(entries).length, 3, 'the three values are drawn, not dropped with their controls')
const names = readOnlyNames(entries)
assert.equal(names[0], 'DeepSeek V4.1 Flash', 'the model name is on the page')
assert.equal(names[1], 'Workspace', 'the permission preset is on the page, by its label')
assert.equal(names[2], 'flybot', 'the working folder is on the page by its last segment')
const titles = sectionTitles(entries)
for (const title of ['Model', 'Permissions', 'Working folder']) assert.ok(titles.includes(title), `${title} keeps its section`)
const folder = readOnlyValues(entries)[2].node
assert.equal(folder.props.title, '/home/xuke/flybot', 'the full folder path is kept as the value title')
assert.deepEqual(modelTriggers(entries), [], 'no model menu the host would refuse')
assert.deepEqual(permissionRows(entries), [], 'no permission preset the host would refuse')
assert.deepEqual(folderTriggers(entries), [], 'no working-folder menu the host would refuse')
assert.deepEqual(nodes(entries).filter(node => node.props?.['aria-haspopup'] === 'menu'), [],
'no anchored menu of any kind is drawn in a locked overview')
})

test('P-547: a writable overview keeps its controls and draws no read-only value', () => {
slots = []
const entries = tree(rosterModelWith(undefined))
assert.deepEqual(readOnlyValues(entries), [], 'the read-only values are a locked window only')
assert.equal(modelTriggers(entries).length, 1, 'the model menu is still the writable control')
assert.equal(permissionRows(entries).length, 1, 'the permission presets are still the writable control')
assert.equal(folderTriggers(entries).length, 1, 'the working-folder menu is still the writable control')
assert.equal(ownText(modelTriggers(entries)[0].node).includes('DeepSeek V4.1 Flash'), true,
'the writable trigger still shows the same model name')
})
Loading
Loading