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/362-mobile-layout.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
On a phone browser (about 400px wide) you can skim a chat, press **Approve** / **Reject** on a held reply and work the Agent inbox: approval buttons become full-size touch targets, floating approval cards span the screen beside the icon rail, and inbox cards put their buttons under the text so titles stay readable. Wider screens are unchanged ([#362](https://github.com/ThinkFlowLab/FlyBot/issues/362)).
26 changes: 26 additions & 0 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -16410,6 +16410,31 @@ function serveBrowserReads({ browsers, post, clientId = globalThis.crypto.random
};
}

// src/client/mobile.js
var MOBILE_MAX_WIDTH = 640;
var MOBILE_CSS = `
@media (max-width:${MOBILE_MAX_WIDTH}px){
.bt-held-actions{max-width:100%}
.bt-held-card{padding:10px 12px}
:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions{gap:8px;margin-top:8px}
:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button{flex:1 1 0;min-width:0;height:40px;padding:0 16px;border-radius:999px;border:0;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:14px;font-weight:500;cursor:pointer}
:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button.bt-flybot-approve{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-weight:600}
:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button:disabled{opacity:.5;cursor:default}
.bt-held-retry{min-height:32px}
.bt-flybot-approvals{left:64px;right:8px;bottom:calc(8px + env(safe-area-inset-bottom,0px));max-width:none;max-height:min(60vh,calc(100vh - 16px))}
.bt-flybot-pointers{top:8px;right:8px;max-width:calc(100vw - 72px)}
.bt-flybot-pointer{max-width:100%;min-width:0}
.bt-flybot-pointer-text{flex:0 1 auto;min-width:0;max-width:none}
.bt-flybot-pointer-note{flex:0 100 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}
.bt-flybot-pointer button{flex:none;min-height:28px;padding:4px 12px}
.bt-inbox-card{flex-wrap:wrap;row-gap:8px}
.bt-inbox-copy{flex-basis:calc(100% - 38px)}
.bt-inbox-title,.bt-inbox-reason{white-space:normal;overflow-wrap:anywhere;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}
.bt-inbox-actions{flex:1 1 100%;justify-content:flex-end;flex-wrap:wrap;padding-left:38px}
.bt-inbox-actions :is(.bt-soft,.bt-send){height:36px;padding:0 16px;font-size:13px}
}
`;

// src/client/friend-lines.js
function friendLineNews(bots, known, isOpen, selfId) {
const next = { ...known };
Expand Down Expand Up @@ -17579,6 +17604,7 @@ function apply(ctx) {
installCompiledStyles(FLYBOT_CSS, "flybot"),
installCompiledStyles(MEMBER_CSS, "members"),
installCompiledStyles(USER_MESSAGE_CSS, "user-messages"),
installCompiledStyles(MOBILE_CSS, "mobile"),
browsers ? installCompiledStyles(BROWSER_CSS, "browser") : null,
installTheme(),
shadow("sidebar.workspaces", { priority: -10, inject: face }, TeamSidebar),
Expand Down
2 changes: 2 additions & 0 deletions core/src/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { FOLDER_CSS } from './folder-menu.js'
import { USAGE_CSS } from './usage-page.js'
import { SCHEDULE_CSS } from './schedules.js'
import { INBOX_CSS } from './inbox.js'
import { MOBILE_CSS } from './mobile.js'
import { STORAGE, migrateStorage } from './storage.js'
import { friendLineNews } from './friend-lines.js'
import { groupsFor, readMode, surfaceGroups, writeMode } from './surface.js'
Expand Down Expand Up @@ -849,6 +850,7 @@ export function apply(ctx) {
installCompiledStyles(FLYBOT_CSS, 'flybot'),
installCompiledStyles(MEMBER_CSS, 'members'),
installCompiledStyles(USER_MESSAGE_CSS, 'user-messages'),
installCompiledStyles(MOBILE_CSS, 'mobile'),
browsers ? installCompiledStyles(BROWSER_CSS, 'browser') : null,
installTheme(),
shadow('sidebar.workspaces', { priority: -10, inject: face }, TeamSidebar),
Expand Down
31 changes: 31 additions & 0 deletions core/src/client/mobile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Phone-width layout, step 1 (#362): at about 400px a phone browser can skim a chat, press
// Approve / Reject on a held reply and work the Agent inbox. Only these surfaces change;
// the shell keeps its own icon rail (DSH collapses the sidebar below 1024px; the floating
// cards stay clear of its 56px, so the top + and the account stay reachable), and the inbox
// dialog already fills the screen at this width (styles.js). Installed after the shell
// group's other styles, so these rules win at equal specificity. No PWA, push or native
// app here: they come after #352, and a notification will only ever open the card.
export const MOBILE_MAX_WIDTH = 640

export const MOBILE_CSS = `
@media (max-width:${MOBILE_MAX_WIDTH}px){
.bt-held-actions{max-width:100%}
.bt-held-card{padding:10px 12px}
:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions{gap:8px;margin-top:8px}
:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button{flex:1 1 0;min-width:0;height:40px;padding:0 16px;border-radius:999px;border:0;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:14px;font-weight:500;cursor:pointer}
:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button.bt-flybot-approve{background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-weight:600}
:is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button:disabled{opacity:.5;cursor:default}
.bt-held-retry{min-height:32px}
.bt-flybot-approvals{left:64px;right:8px;bottom:calc(8px + env(safe-area-inset-bottom,0px));max-width:none;max-height:min(60vh,calc(100vh - 16px))}
.bt-flybot-pointers{top:8px;right:8px;max-width:calc(100vw - 72px)}
.bt-flybot-pointer{max-width:100%;min-width:0}
.bt-flybot-pointer-text{flex:0 1 auto;min-width:0;max-width:none}
.bt-flybot-pointer-note{flex:0 100 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}
.bt-flybot-pointer button{flex:none;min-height:28px;padding:4px 12px}
.bt-inbox-card{flex-wrap:wrap;row-gap:8px}
.bt-inbox-copy{flex-basis:calc(100% - 38px)}
.bt-inbox-title,.bt-inbox-reason{white-space:normal;overflow-wrap:anywhere;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}
.bt-inbox-actions{flex:1 1 100%;justify-content:flex-end;flex-wrap:wrap;padding-left:38px}
.bt-inbox-actions :is(.bt-soft,.bt-send){height:36px;padding:0 16px;font-size:13px}
}
`
60 changes: 60 additions & 0 deletions core/test/mobile-layout.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { test } from 'node:test'
import { MOBILE_CSS, MOBILE_MAX_WIDTH } from '../src/client/mobile.js'

// Phone-width layout, step 1 (#362): chat, the held-reply Approve / Reject and the Agent
// inbox fit a ~400px phone browser. Only a max-width media query, so wider screens are as before.
const rule = (selector) => {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return MOBILE_CSS.match(new RegExp(`(?:^|\\n)${escaped}\\{([^}]*)\\}`))?.[1] ?? ''
}

test('every phone rule sits inside one max-width media query at 640px', () => {
assert.equal(MOBILE_MAX_WIDTH, 640)
const body = MOBILE_CSS.trim()
assert.ok(body.startsWith('@media (max-width:640px){'), 'opens with the phone media query')
assert.ok(body.endsWith('}'), 'closes it')
assert.equal((body.match(/@media/g) ?? []).length, 1)
// Balanced braces: nothing leaks out of the media block.
const open = body.indexOf('{')
let depth = 0
for (const [i, ch] of [...body].entries()) {
if (ch === '{') depth++
if (ch === '}') depth--
if (i >= open && depth === 0 && i < body.length - 1) assert.fail(`rule outside the media query at ${i}`)
}
assert.equal(depth, 0)
})

test('held-reply and floating approval buttons are touch-sized, Approve filled', () => {
const button = rule(':is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button')
assert.match(button, /height:40px/)
assert.match(button, /flex:1 1 0/)
assert.match(rule(':is(.bt-held-card,.bt-flybot-approval) .bt-flybot-approval-actions>button.bt-flybot-approve'), /background:var\(--bt-accent\)/)
assert.match(rule('.bt-held-actions'), /max-width:100%/)
})

test('floating cards span the screen beside the icon rail, not over the top + or the account', () => {
const stack = rule('.bt-flybot-approvals')
assert.match(stack, /left:64px/)
assert.match(stack, /right:8px/)
assert.match(stack, /max-width:none/)
assert.match(rule('.bt-flybot-pointers'), /max-width:calc\(100vw - 72px\)/)
})

test('inbox cards wrap their actions under the text so the title stays readable', () => {
assert.match(rule('.bt-inbox-card'), /flex-wrap:wrap/)
assert.match(rule('.bt-inbox-actions'), /flex:1 1 100%/)
assert.match(rule('.bt-inbox-actions :is(.bt-soft,.bt-send)'), /height:36px/)
assert.match(rule('.bt-inbox-title,.bt-inbox-reason'), /white-space:normal/)
})

test('the client installs the phone styles after the shell styles they override', () => {
const index = readFileSync(new URL('../src/client/index.js', import.meta.url), 'utf8')
const at = name => index.indexOf(`installCompiledStyles(${name}`)
assert.ok(at('MOBILE_CSS') > 0, 'MOBILE_CSS is installed')
for (const earlier of ['SHELL_CSS', 'INBOX_CSS', 'FLYBOT_CSS']) assert.ok(at(earlier) >= 0 && at(earlier) < at('MOBILE_CSS'), `${earlier} comes first`)
const bundle = readFileSync(new URL('../client.js', import.meta.url), 'utf8')
assert.ok(bundle.includes('.bt-inbox-actions :is(.bt-soft,.bt-send){height:36px'), 'client.js carries the phone styles')
})
8 changes: 8 additions & 0 deletions docs/subsystems/core-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The browser side of the patched `ds-bot` core (`core/`): sources in `core/src/cl
| `core/test/new-chat.test.js` | Picker filters and the **+** short action menu (`data-menu`; New chat, New Bot, New group chat, plugin actions, then Add coding agent; #385 marks, verbs and hints). `ungrantedLocalFix`, `copiedToClipboard` (no clipboard or a refused write is a failure) and the refusal card (invite and leave-out actions, the copy-by-hand link field, no grant written by the client). |
| `core/src/client/schedules.js` | Settings → Scheduled tasks, a Bot's card and the details line. Each row has **Edit** (pencil; the row turns into the same form that creates a task, filled in, with **Save changes**; an edited task keeps its time zone, a cron rule it cannot show is kept) and **Pause** / **Resume**; a paused row reads **Paused** (#354). With Automation tasks off, the hint reads "Switch on Automation tasks on the Plugins page." and the button **Open Plugins** closes the Settings dialog and opens DSH's Plugins panel (`actions.selectPanel('plugins')`); the Harness settings dialog has no such switch (#378). Tests: `core/test/schedules-client.test.js`. |
| `core/src/client/inbox.js` | Agent inbox, step 1 (#349): the **Inbox** row at the top of the sidebar (`InboxRow`, count badge = cards in *Needs you*) and the inbox dialog (`InboxDialog`, `ui.inbox`, `actions.openInbox` / `closeInbox`) with the groups **Needs you**, **In progress** and **Done** (`inboxModel`). Tests: `core/test/inbox.test.js`. |
| `core/src/client/mobile.js` | Phone-width layout, step 1 (#362): `MOBILE_CSS` under `@media (max-width:640px)` for held-reply and floating approval buttons, the chat pointers and Agent inbox cards. Tests: `core/test/mobile-layout.test.js`. |

## Invariants

Expand Down Expand Up @@ -108,6 +109,13 @@ The overlay (`flybot-slots.js` `FlyBotApprovals`) is the fallback, not a second
- Each card has exactly one status, as a colored pill with an icon and a word (never color alone), the Bot, where and when, the title on one line with the full text on hover, and **Open** (closes the inbox, opens the chat). An approval the group chat's card can answer (`answerable`) also has **Reject** / **Allow once**, which call `actions.answerApproval`, the same answer as that card (no second approval path); the card leaves and a status line says what was done; an error stays on the card. No native confirm dialog. A held Bot message, an approval DSH's card answers in the Bot's own chat, and every task have **Open** only: Bot-sent messages are approved in their chat. Every action is a button, so the dialog is keyboard reachable; the close button takes focus and Escape closes.
- Out of step 1: the three-column board, tool-call counts, failed turns without a task record (they need an event source, #353), team views (#51/#149).

### Phone-width layout (#362, step 1)

- Below 640px (`MOBILE_MAX_WIDTH`, `core/src/client/mobile.js`, `MOBILE_CSS`, installed last in the shell group so it wins at equal specificity) a ~400px phone browser can skim a chat, press **Approve** / **Reject** on a held reply and work the Agent inbox. Wider screens are unchanged: every rule sits in that one media query.
- Held-reply and floating approval cards: **Reject** / **Approve** share the card width as 40px buttons, **Approve** filled with the accent; the held card uses the message's full width. The floating approval stack and the chat pointers span the screen beside the shell's 56px icon rail (DSH collapses the sidebar below 1024px), so the top **+** and the account stay reachable.
- Inbox cards put their buttons on a row under the text (36px), and the title and reason wrap to two lines instead of one truncated line. The inbox dialog already fills the screen at this width (`.bt-mem`).
- Out of step 1 (owner decision 2026-10-20): PWA install and push (after #352), native apps. A notification will only ever open the card; approval stays on the in-app buttons.

### Approval wait badge on chat rows

- A Bot row uses the same gold wait dot as a room row when `bot.pendingCount > 0` (Bot Social holds on that friend's chat) or a local hold is open. Shared-room overview pending lines stay one line with ellipsis (`bt-shared-wait`).
Expand Down
1 change: 1 addition & 0 deletions docs/subsystems/social.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ pins the list. The authority for the list is Bot Social's README and `docs/FACE_
- Status chips on room details and Past rooms (`data-flybot=face-status`) read **Open** / **Closed** in sentence case with readable contrast, and the past row keeps its own spacing between avatars, title, time and chip (#170).
- One card-placement rule (#418): a held message's decision sits where that message is. A Bot's reply to a friend has its buttons on the held reply bubble in place (no copy in the dock of that chat or the friend's chat); a message with no bubble of its own (a Bot's `message_friend_bot`, a typed composer line held by policy, a face-room line) has its card in the dock of the chat it belongs to, as below.
- In-chat approval cards (`flybot.session.dock`) are right-aligned bubbles capped at ~460px: **Waiting for approval**, one `from → to` line, a word-wrapped preview with **Show more**, and **Approve** / **Reject** (one right-hand group) with a quiet **Always approve** link at the left of the same row, alone above them when the card is narrow (#166, #260, #271). One place per approval (#278): a Bot's direct message to a friend is drawn in the friend's chat once that chat exists (the sending Bot's own chat only until then), never in a Face-to-face room that has the friend in it; a Face-to-face room shows, counts as waiting, and gets a "Waiting for the owner's approval" notice for just the lines held for its own room id. The client also hides that old notice in a Face-to-face room's history. The held text shows once (#306): a Bot line held in a Face-to-face room keeps its text in the card, and the room draws no bubble for it until it is approved (`GroupMessage`, `core/src/client/cells.js`, matched by `room.heldLines`, #456, and by `room.pendingApprovals` as a fallback); a line the owner typed is never held and stays a plain bubble. A Bot's message to a friend (`message_friend_bot`) has no bubble in either chat, so the card is already its only place. #456: the held room line lives only on its card (greyed preview) until **Approve**: `onFaceLine` answers `{ held: true, approvalId }` for a Bot line Bot Social holds, `social/decide-approval` calls `flybot.rooms.settleHeld`, Approve makes it a room bubble and Reject a grey `<Bot> · Not sent` line, with no `Rejected.` note in the face room. A decided card leaves the dock at once (no **Approved** / **Rejected** stub, any card kind). A face card's `from` is the room's own #384 label for that local Bot (`Researcher (Alice) → the room`, `faceFromLabel`). `applyFaceSnapshot` posts a human or system event for a round only when the core room logged it now (`applyFaceLine` refuses an event id it has), so an event read twice starts no second round.
- On a phone-width screen (below 640px, #362) the hold card's **Approve** / **Reject** are 40px touch targets at the right of their row; wider screens keep the compact 28px buttons.
- The composer **Message {name}** accessible name is set by core `HeaderPill` (placeholder CSS var plus `aria-label` on `[data-composer-input]`, restored on cleanup); chat-header tok jargon stays in pristine DSH / ds-bot.

### Face-room poll identity (#122)
Expand Down
9 changes: 6 additions & 3 deletions patches/ds-bot/0003-flybot-api-client.patch
Original file line number Diff line number Diff line change
Expand Up @@ -1009,7 +1009,7 @@ diff --git a/src/client/icons.js b/src/client/icons.js
diff --git a/src/client/index.js b/src/client/index.js
--- a/src/client/index.js
+++ b/src/client/index.js
@@ -1,44 +1,61 @@
@@ -1,44 +1,62 @@
+import { FLYBOT_CSS, FlyBotApprovals, FlyBotSessionDock, createClientApi as createFlyBotClient } from './flybot-slots.js'
import { call } from './api.js'
import { isHex, inkText, paintCss } from './inks.js'
Expand Down Expand Up @@ -1046,6 +1046,7 @@ diff --git a/src/client/index.js b/src/client/index.js
import { USAGE_CSS } from './usage-page.js'
import { SCHEDULE_CSS } from './schedules.js'
+import { INBOX_CSS } from './inbox.js'
+import { MOBILE_CSS } from './mobile.js'
import { STORAGE, migrateStorage } from './storage.js'
+import { friendLineNews } from './friend-lines.js'
import { groupsFor, readMode, surfaceGroups, writeMode } from './surface.js'
Expand Down Expand Up @@ -1296,7 +1297,7 @@ diff --git a/src/client/index.js b/src/client/index.js
actions.referenceChat = (sessionId, span) => {
const controller = atMenu?.controllerOf(sessionId)
if (controller === undefined || span === undefined) return
@@ -752,11 +752,13 @@ export function apply(ctx) {
@@ -752,27 +752,33 @@ export function apply(ctx) {
installCompiledStyles(SHELL_CSS, 'shell'),
installCompiledStyles(SIDEBAR_CSS, 'sidebar'),
installCompiledStyles(SECRET_CSS, 'secrets'),
Expand All @@ -1310,8 +1311,10 @@ diff --git a/src/client/index.js b/src/client/index.js
+ installCompiledStyles(FLYBOT_CSS, 'flybot'),
installCompiledStyles(MEMBER_CSS, 'members'),
installCompiledStyles(USER_MESSAGE_CSS, 'user-messages'),
+ installCompiledStyles(MOBILE_CSS, 'mobile'),
browsers ? installCompiledStyles(BROWSER_CSS, 'browser') : null,
@@ -765,14 +765,17 @@ export function apply(ctx) {
installTheme(),
shadow('sidebar.workspaces', { priority: -10, inject: face }, TeamSidebar),
shadow('sidebar.brand.mark', { priority: -10 }, BrandMark),
shadow('sidebar.brand.name', { priority: -10 }, BrandName),
shadow('sidebar.footer.action', { id: 'bot-account', order: -200, inject: face }, FooterAccount),
Expand Down
Loading
Loading