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/2502-readonly-reload.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Settings header, the details drawer, the Inbox and the Memory panel now draw a **Reload** beside the host's read-only sentence, so a window that was told to reload can do it where it stands instead of closing the panel and reaching for the sidebar ([#2502](https://github.com/ThinkFlowLab/FlyBot/issues/2502)).
53 changes: 39 additions & 14 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -5238,6 +5238,13 @@ button.bt-q-opt:active{background:var(--bt-active)}
.bt-option-sub{font-size:11px;line-height:15px;color:var(--bt-ink-2);white-space:normal;overflow-wrap:anywhere}
.bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none}
.bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)}
/* #2502: the host sentence and its Reload entry share one row; the sidebar's own rule sizes
the button, this only places it beside the sentence instead of under it. */
.bt-readonly-line{display:flex;align-items:center;gap:8px}
.bt-readonly-line>.bt-hint{flex:1 1 auto;min-width:0}
.bt-readonly-line>.bt-read-only-reload{flex:none;margin:0}
.bt-readonly-line-head{padding:0 20px 8px}
.bt-readonly-line-head>.bt-hint{padding:0;text-align:left}
.bt-nc-spacer{flex:1}
.bt-nc-compose{padding:0 16px 14px;display:flex;justify-content:center}
.bt-nc-card{display:flex;align-items:flex-end;gap:6px;width:100%;max-width:var(--dsh-chat-content-width,748px);min-height:44px;padding:4px 6px;border-radius:22px;border:1px solid var(--bt-line-2);background:var(--bt-main);box-shadow:0 2px 8px -2px rgba(0,0,0,.06);box-sizing:border-box}
Expand Down Expand Up @@ -6885,7 +6892,7 @@ function useInlineEntry(policy) {
}
function HeldReplyActions({ held, actions, roster }) {
const locked = lockedWindow(roster);
const readOnlyLine = () => (0, import_react10.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly));
const readOnlyLine2 = () => (0, import_react10.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly));
const custom = useInlineEntry(held?.by);
const [busy, setBusy] = (0, import_react10.useState)(false);
const [error, setError] = (0, import_react10.useState)("");
Expand Down Expand Up @@ -6915,7 +6922,7 @@ function HeldReplyActions({ held, actions, roster }) {
(0, import_react10.createElement)("span", null, held.status === "approved" ? t("Sent to {to}", { to: held.toName }) : held.status === "auto" ? t("Auto-sent to {to}", { to: held.toName }) : failed ? t("Not sent: delivery failed") : t("Not sent")),
// #968: the terminal answer comes from one predicate, not from `status === 'failed'` here and a
// different field elsewhere. A failure that can never succeed draws no button on any surface.
failed && canRetry(held) ? locked ? readOnlyLine() : (0, import_react10.createElement)("button", { type: "button", className: "bt-held-retry", disabled: busy, onClick: () => decide("retry") }, t("Retry")) : null,
failed && canRetry(held) ? locked ? readOnlyLine2() : (0, import_react10.createElement)("button", { type: "button", className: "bt-held-retry", disabled: busy, onClick: () => decide("retry") }, t("Retry")) : null,
error ? (0, import_react10.createElement)("span", { className: "bt-flybot-approval-error", role: "alert" }, error) : null
);
}
Expand All @@ -6928,7 +6935,7 @@ function HeldReplyActions({ held, actions, roster }) {
(0, import_react10.createElement)("div", { className: "bt-held-head" }, t("Waiting for your approval"), age),
(0, import_react10.createElement)("div", { className: "bt-flybot-approval-origin" }, t("{from} → {to}", { from: held.from ?? t("A Bot"), to: held.toName })),
reason ? (0, import_react10.createElement)("div", { className: "bt-flybot-approval-reason", title: reason.title }, reason.text) : null,
locked ? readOnlyLine() : (0, import_react10.createElement)(
locked ? readOnlyLine2() : (0, import_react10.createElement)(
"div",
{ className: "bt-flybot-approval-actions" },
(0, import_react10.createElement)("button", { type: "button", disabled: busy, onClick: () => decide("reject") }, t("Reject")),
Expand Down Expand Up @@ -8725,6 +8732,24 @@ function progressOf(task) {
return Math.max(0, Math.min(100, Math.round(value)));
}

// src/client/read-only.js
var readOnlySaid = (roster) => typeof roster?.readOnly === "string" && roster.readOnly.trim() !== "";
function readOnlyLine(h50, roster, actions, { key, className, style, reload = true } = {}) {
if (!readOnlySaid(roster)) return null;
return h50(
"div",
{ key, className: ["bt-readonly-line", className].filter(Boolean).join(" "), style },
h50("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)),
reload ? h50("button", {
type: "button",
className: "bt-read-only-reload",
onClick: () => {
void Promise.resolve(actions?.reloadTeam?.()).catch((error) => actions?.reportError?.(error, "reload"));
}
}, t("Reload")) : null
);
}

// src/client/inbox.js
var import_react15 = require("react");
var KEEP_MS = 7 * 864e5;
Expand Down Expand Up @@ -9359,7 +9384,7 @@ function InboxDialog({ roster, useWork, useSessions, actions, leaving = false })
(0, import_react15.createElement)(
"div",
{ className: "bt-mem-body bt-inbox-body" },
...locked ? [(0, import_react15.createElement)("p", { key: "readonly", className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly))] : [],
...locked ? [readOnlyLine(import_react15.createElement, roster, actions, { key: "readonly" })] : [],
(0, import_react15.createElement)("div", { className: "bt-inbox-feedback", role: "status", "aria-live": "polite" }, feedback),
GROUPS.map((group) => {
const settled = group.id === "done" ? model.doneAll ?? groups.done : null;
Expand Down Expand Up @@ -10719,7 +10744,7 @@ function TeamSidebar(props) {
return entries2.sort((a, b) => Number(Boolean(b.item.pinned)) - Number(Boolean(a.item.pinned)) || Number(foreign(a)) - Number(foreign(b)) || b.at - a.at);
}, [roster, updated]);
const main = roster.byId[roster.mainBotId];
const readOnlySaid = typeof roster.readOnly === "string" && roster.readOnly.trim() !== "";
const readOnlySaid2 = typeof roster.readOnly === "string" && roster.readOnly.trim() !== "";
const readOnlyReload = (0, import_react19.createElement)("button", {
type: "button",
className: "bt-read-only-reload",
Expand Down Expand Up @@ -10758,10 +10783,10 @@ function TeamSidebar(props) {
(0, import_react19.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }),
(0, import_react19.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react19.createElement)(PlusIcon))
),
wide && readOnlySaid ? (0, import_react19.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
wide && readOnlySaid ? readOnlyReload : null,
!wide && readOnlySaid ? (0, import_react19.createElement)("div", { className: "bt-hint bt-read-only", role: "status", title: hostText(roster.readOnly), "aria-label": hostText(roster.readOnly) }, hostText(roster.readOnly)) : null,
!wide && readOnlySaid ? readOnlyReload : null,
wide && readOnlySaid2 ? (0, import_react19.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
wide && readOnlySaid2 ? readOnlyReload : null,
!wide && readOnlySaid2 ? (0, import_react19.createElement)("div", { className: "bt-hint bt-read-only", role: "status", title: hostText(roster.readOnly), "aria-label": hostText(roster.readOnly) }, hostText(roster.readOnly)) : null,
!wide && readOnlySaid2 ? readOnlyReload : null,
(0, import_react19.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }),
(0, import_react19.createElement)(KanbanRow, { wide, useUi, actions }),
(0, import_react19.createElement)(ProjectsNav, { wide }),
Expand Down Expand Up @@ -14604,7 +14629,7 @@ function MemoryDialog({ bot, actions, roster, leaving = false }) {
(0, import_react28.createElement)(
"div",
{ className: "bt-mem-body" },
locked ? (0, import_react28.createElement)("p", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
readOnlyLine(import_react28.createElement, roster, actions),
error ? (0, import_react28.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null,
view === null ? error ? null : (0, import_react28.createElement)(Skeleton) : page === "entries" ? (0, import_react28.createElement)(Entries, { bot, view, remove: locked ? null : remove }) : (0, import_react28.createElement)(Summary, { bot, view, regenerate })
),
Expand Down Expand Up @@ -19566,7 +19591,7 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false
// A window that holds the team read-only still opens every page here, so the header says
// why nothing can be changed, in the host's own sentence and above the refusal below it:
// the standing state first, the answer to one pressed control closest to that control.
typeof roster?.readOnly === "string" && roster.readOnly.trim() ? (0, import_react42.createElement)("div", { className: "bt-hint bt-read-only", role: "status", style: { padding: "0 20px 8px", textAlign: "left" } }, hostText(roster.readOnly)) : null,
readOnlyLine(import_react42.createElement, roster, actions, { key: "readonly", className: "bt-readonly-line-head" }),
shown === "usage" ? null : errorLine,
shown === "usage" ? null : (0, import_react42.createElement)(
"div",
Expand Down Expand Up @@ -20199,7 +20224,7 @@ function GroupOverview({ room: room2, roster, actions, usage, useWork, openSetti
// P-544: the group overview's writes are gone in a locked window, so it says why once, above
// the reads it still draws. The approval card used to carry its own copy (#2381); one page,
// one sentence.
locked ? (0, import_react44.createElement)("div", { key: "read-only", className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null,
readOnlyLine(import_react44.createElement, roster, actions, { key: "read-only" }),
(0, import_react44.createElement)(FlyBotRoomOverview, { key: "plugin-overview", room: room2, roster, actions }),
invited && room2.sharedFrom && !face ? (0, import_react44.createElement)(
"div",
Expand Down Expand Up @@ -20340,7 +20365,7 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
{ key: "set-body", className: "bt-drawer-body bt-set-body", "data-move": move },
// #2387: the lock is the window's, not the Bot's — a read-only window draws the host's sentence
// in place of a group's form too, exactly as it already did for a Bot's.
lockedWindow(roster) ? (0, import_react44.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : bot ? (0, import_react44.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react44.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close })
lockedWindow(roster) ? readOnlyLine(import_react44.createElement, roster, actions) : bot ? (0, import_react44.createElement)(BotSettingsForm, { bot, roster, actions, run }) : (0, import_react44.createElement)(GroupSettingsForm, { room: room2, roster, actions, run, onDeleted: close })
)
];
}
Expand Down Expand Up @@ -20434,7 +20459,7 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a
// 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) ? readOnlyLine(import_react44.createElement, roster, actions, { key: "read-only" }) : null,
lockedWindow(roster) ? (0, import_react44.createElement)(ReadOnlyOverview, { key: "read", bot, roster }) : null,
lockedWindow(roster) ? null : (0, import_react44.createElement)(
"div",
Expand Down
9 changes: 5 additions & 4 deletions core/src/client/details-drawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import { MemoryRow } from './memory-panel.js'
import { RoomSchedules, ScheduleSummary, lockedWindow } from './schedules.js'
import { Segmented, useGlide, useMorph } from './motion.js'
import { MemberWork, useMemberWork } from './member-status.js'
import { hostText, t } from './i18n.js'
import { t } from './i18n.js'
import { remoteBotTitle, roomTitle, copiedToClipboard } from '../shared/remote-label.js'
import { FlyBotRoomOverview, FlyBotDetails } from './flybot-slots.js'
import { call } from './api.js'
import { readOnlyLine } from './read-only.js'

const TAB_LABELS = { details: 'Details', browser: 'Browser' }

Expand Down Expand Up @@ -221,7 +222,7 @@ function GroupOverview({ room, roster, actions, usage, useWork, openSettings })
// P-544: the group overview's writes are gone in a locked window, so it says why once, above
// the reads it still draws. The approval card used to carry its own copy (#2381); one page,
// one sentence.
locked ? h('div', { key: 'read-only', className: 'bt-hint bt-read-only', role: 'status' }, hostText(roster.readOnly)) : null,
readOnlyLine(h, roster, actions, { key: 'read-only' }),
h(FlyBotRoomOverview, { key: 'plugin-overview', room, roster, actions }),
invited && room.sharedFrom && !face
? h('div', { key: 'from', className: 'bt-shared-from' },
Expand Down Expand Up @@ -330,7 +331,7 @@ export function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, ro
// #2387: the lock is the window's, not the Bot's — a read-only window draws the host's sentence
// in place of a group's form too, exactly as it already did for a Bot's.
lockedWindow(roster)
? h('div', { className: 'bt-hint bt-read-only', role: 'status' }, hostText(roster.readOnly))
? readOnlyLine(h, roster, actions)
: bot ? h(BotSettingsForm, { bot, roster, actions, run }) : h(GroupSettingsForm, { room, roster, actions, run, onDeleted: close })),
]
}
Expand Down Expand Up @@ -411,7 +412,7 @@ export function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, ro
// 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) ? readOnlyLine(h, roster, actions, { key: 'read-only' }) : 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')),
Expand Down
3 changes: 2 additions & 1 deletion core/src/client/inbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { hostText, t } from './i18n.js'
import { failureSentence } from '../shared/failure-copy.js'
import { columnOf } from '../shared/task-board.js'
import { lockedWindow } from './schedules.js'
import { readOnlyLine } from './read-only.js'

// Done (and a failed task) ages out after 7 days.
export const KEEP_MS = 7 * 86_400_000
Expand Down Expand Up @@ -661,7 +662,7 @@ export function InboxDialog({ roster, useWork, useSessions, actions, leaving = f
h('div', { className: 'bt-mem-body bt-inbox-body' },
// #2284: one sentence at the top of the dialog, in the host's own words, so a window that
// draws no write says why instead of leaving the reads silent about it.
...(locked ? [h('p', { key: 'readonly', className: 'bt-hint bt-read-only', role: 'status' }, hostText(roster.readOnly))] : []),
...(locked ? [readOnlyLine(h, roster, actions, { key: 'readonly' })] : []),
h('div', { className: 'bt-inbox-feedback', role: 'status', 'aria-live': 'polite' }, feedback),
GROUPS.map(group => {
// #2050: the model keeps every finished row, so the group can draw the page's remainder
Expand Down
3 changes: 2 additions & 1 deletion core/src/client/memory-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { dateLocale, hostText, t } from './i18n.js'
import { memoryBlockedNotice, memoryEmptyNotice } from './memory-empty.js'
import { summaryBlocks } from './text.js'
import { useLinger } from './motion.js'
import { readOnlyLine } from './read-only.js'

const POLL_MS = 1500
const MENU_EXIT_MS = 120
Expand Down Expand Up @@ -321,7 +322,7 @@ export function MemoryDialog({ bot, actions, roster, leaving = false }) {
h('button', { type: 'button', role: 'menuitem', disabled: count === 0 || view?.summarizing, onClick: regenerate },
h('span', { className: 'bt-menu-label' }, t('Regenerate summary')))) : null),
h('div', { className: 'bt-mem-body' },
locked ? h('p', { className: 'bt-hint bt-read-only', role: 'status' }, hostText(roster.readOnly)) : null,
readOnlyLine(h, roster, actions),
error ? h('div', { className: 'bt-error', role: 'alert', style: { padding: 0 } }, hostText(error)) : null,
view === null ? (error ? null : h(Skeleton))
: page === 'entries' ? h(Entries, { bot, view, remove: locked ? null : remove })
Expand Down
29 changes: 29 additions & 0 deletions core/src/client/read-only.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// #2502: the host's sentence tells the reader to close the other window and reload. The sidebar
// (#2247) and the refusal toast (#2265) grew that entry; the Settings header, the drawer, the
// Inbox and the Memory panel kept the sentence with nothing to answer it, and inside the Inbox
// modal the sidebar's own button cannot be reached at all. This is the shared piece those
// surfaces draw: the host's own words under `role="status"`, and one Reload outside that node (a
// live region must stay text), running the same `actions.reloadTeam` read the sidebar runs — never
// a page reload, which would drop the composer.
import { hostText, t } from './i18n.js'

// A sentence of nothing but whitespace says nothing, so it draws nothing. Read the way the
// sidebar reads it (#2247), so the two surfaces cannot disagree about which window is locked.
export const readOnlySaid = roster => typeof roster?.readOnly === 'string' && roster.readOnly.trim() !== ''

// `reload: false` keeps the sentence and drops the control, for a view that already carries one:
// one Reload per screen, on the topmost sentence. The caller passes `h` rather than this module
// importing react: one more react import would renumber every module's binding in the generated
// `client.js`, turning a five-line change into a two-thousand-line diff of the bundle.
export function readOnlyLine(h, roster, actions, { key, className, style, reload = true } = {}) {
if (!readOnlySaid(roster)) return null
return h('div', { key, className: ['bt-readonly-line', className].filter(Boolean).join(' '), style },
h('div', { className: 'bt-hint bt-read-only', role: 'status' }, hostText(roster.readOnly)),
reload
? h('button', {
type: 'button',
className: 'bt-read-only-reload',
onClick: () => { void Promise.resolve(actions?.reloadTeam?.()).catch(error => actions?.reportError?.(error, 'reload')) },
}, t('Reload'))
: null)
}
Loading
Loading