Skip to content

Settings: a read-only window never says so on the panel it is showing — eight pages offer write controls with no sentence explaining the refusal - #2204

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/1-settings-readonly-head
Nov 2, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/1-settings-readonly-head

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2199

Owner: Dev-1
Written-by: Dev-1
Reviewer: Birfy
Claim-Id: 00a7651493e841de975b1f7a0eb6ead2
Branch: dev/1-settings-readonly-head
Base: e65d7e5
Tier: T1
Tree: 19b0a16734c4f72a1a4cb50c47b6069f9bc4d8af
Single parent: e65d7e5

The branch was recut on e65d7e54 (main after #2196) and the bundle was regenerated there, so core/client.js and patches/ds-bot/0008 carry #2196's delta plus this one; nothing here descends from the earlier c57efcfa rehearsal. The pushed head 501c64b24153581ff554ca0dc8e784c991a8f476 is one commit with single parent e65d7e54; its tree is 19b0a16734c4f72a1a4cb50c47b6069f9bc4d8af, and every check below ran on that tree at this tip. node core/scripts/build-client.mjs --check exits 0 on it, so core/client.js is what the sources build: a rebuild from this tip is byte-identical (sha256 ca618558…, as the independent advisory reproduced).

What changes

A window that holds the team read-only still opens every Settings page, and each page draws its full set of write controls. Until now nothing on that panel said the window was read-only, so the reader only learned it by pressing a control and being refused. SettingsDialog (core/src/client/settings.js) now prints the host's own sentence from roster.readOnly once, under .bt-settings-head, with role="status" and the same hostText() primitive the sidebar uses — not a second sentence we write ourselves.

The line sits outside .bt-settings-scroll, so it cannot scroll away (the rule #2194 established for a refused write), and it is drawn above the refused-write line: the standing state comes first, and the answer to one pressed control stays closest to that control. Order: .bt-settings-head → read-only sentence (role="status") → refused write (role="alert") → scrolling page. A blank or missing readOnly draws nothing at all, and no write behaviour changes: no control is hidden and no refusal is altered.

Red first

Test core/test/settings-readonly-head.test.js, run on this tree (e65d7e54 + regenerated bundle) with the render removed:

not ok 1 - a read-only window says so in the Settings header, in the host's own words (#2199)
  error: |-
    general: the host's sentence is drawn once

    0 !== 1

  code: 'ERR_ASSERTION'
  name: 'AssertionError'
  expected: 1
  actual: 0
ok 2 - a writable window, a blank sentence and a missing one draw no read-only line at all (#2199)
not ok 3 - the standing read-only line stays above the refusal one pressed control adds (#2199)
  error: 'the read-only sentence is still on screen after the refused write'
# tests 3
# pass 1
# fail 2

Test 2 (the baseline guard) passes before and after: with readOnly missing, '', ' ' or '\n\t ', the structural signature of every node above .bt-settings-scroll deep-equals the "field missing" baseline, so the render cannot be faked by drawing an extra node.

The two suites, at this tip

node --import ./test/harness-resolve.mjs --test test/settings-error-placement.test.js test/settings-readonly-head.test.js (from core/, on the tree above):

ok 1 - a refused write in Settings stays in view instead of scrolling away with the page (#2194)
ok 2 - a read-only window says so in the Settings header, in the host's own words (#2199)
ok 3 - a writable window, a blank sentence and a missing one draw no read-only line at all (#2199)
ok 4 - the standing read-only line stays above the refusal one pressed control adds (#2199)
# tests 4
# pass 4
# fail 0

Test 4 takes the real path: readOnly renders once, the Bot form's run is handed Promise.reject, the dialog re-renders, and in that one tree the role="status" line's index is asserted below the role="alert" line's index — both above the scrolling box.

Order mutation

Only the position moved — the sentence, className and role unchanged — so the read-only line is drawn after .bt-error. Run on this tree, the result is exactly one red, the ordering assertion, and everything else green:

not ok 3 - the standing read-only line stays above the refusal one pressed control adds (#2199)
  error: |-
    the standing sentence sits above the refusal a single press produced

    false !== true

  code: 'ERR_ASSERTION'
  expected: true
# tests 3
# pass 2
# fail 1

The mutation was reverted before the push: git status --porcelain is empty on the pushed tree, its tree sha is unchanged (19b0a167…), and both suites are green there (above).

Generated files

core/client.js and patches/ds-bot/0008-client-bundle.patch were rebuilt on e65d7e54 with node core/scripts/build-client.mjs and node scripts/ds-bot-patches.mjs generate (which also rewrote patches/ds-bot/0014-flybot-branding.patch and wrote patches/ds-bot/files/test/settings-readonly-head.test.js). node core/scripts/build-client.mjs --check exits 0, and node scripts/ds-bot-patches.mjs check exits 0 with:

ok   ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)
ok   10 patches apply cleanly in order
ok   core/ equals ds-bot/ + patches/ds-bot/

Verify

TZ=UTC ARCH_DOCS_BASE=e65d7e5442825b0da4314d9e5f7214f4d69be14d npm run verify — exit 0:

suite tests fail
core 2664 0
social 555 0
tree 127 0
timeline 140 0
workflow 368 0
.github/scripts 598 0
interop 37 0
live-e2e 24 0

arch-docs: OK (8 changed file(s)); patches check: ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5), 10 patches apply cleanly in order, core/ equals ds-bot/ + patches/ds-bot/, patches/ds-bot/ is what generate writes (generate is a no-op).

Ran: node core/scripts/build-client.mjs, node scripts/ds-bot-patches.mjs generate, node core/scripts/build-client.mjs --check, node scripts/ds-bot-patches.mjs check, the two suites above, npm run verify (which also runs the canonical gate, the changelog check and both lint gates).
Not run: the smoke test (no index.js or src/host/ change), and CI's package job checks (pack check, typecheck, actionlint), which npm run verify does not include.

Policy-sign is not written: none of the 8 changed paths is in policy/gates.json gateFiles.

Changelog: changelog.d/2199-settings-readonly-head.added.md (enhancement).

Checkpoint

{
"pr": 2204,
"claim": "Dev-1",
"issue": 2199,
"summary": "The Settings header now says a read-only window cannot write, in the host's own sentence from roster.readOnly, once and outside the scrolling page. The standing sentence is drawn above the refused-write line, so the answer to one pressed control stays nearest that control. A blank or missing sentence draws nothing, and no write control or refusal changes. Tests drive the real dialog across seven pages and compare against a missing-field baseline. Recut on main after #2196, with the bundle regenerated there.",
"files": ["core/src/client/settings.js", "core/test/settings-readonly-head.test.js", "core/client.js", "changelog.d/2199-settings-readonly-head.added.md", "docs/subsystems/core-client.md"],
"checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=e65d7e5442825b0da4314d9e5f7214f4d69be14d npm run verify", "result": "green", "at": "501c64b24153581ff554ca0dc8e784c991a8f476" },
"next": "none — draft until the independent advisory names 501c64b; then ready"
}

@HugoXK
HugoXK marked this pull request as draft November 2, 2026 16:47
@github-actions github-actions Bot added the risk:T1 Risk tier T1 (client, tests, generated files), set by the controller from the diff label Nov 2, 2026
@HugoXK
HugoXK force-pushed the dev/1-settings-readonly-head branch from e130963 to 501c64b Compare November 2, 2026 17:21
@HugoXK
HugoXK marked this pull request as ready for review November 2, 2026 17:28
@HugoXK

HugoXK commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy review request — #2204 at 501c64b24153581ff554ca0dc8e784c991a8f476

  • Tier T1, draft off. head 501c64b2…, single parent e65d7e54… (= live main), tree 19b0a167…, 8 files +351/−7, maintainer_can_modify: true, mergeable clean.
  • Checks at this tip: 16 success + 1 cancelled (agent-audience, non-blocking). tests-ok, merge-gate and enqueue-guard are success on 501c64b2….
  • The body was tightened after the last guard run — the unverifiable Verified commit line is gone, replaced by Tree: 19b0a167… plus the byte-identical rebuild (sha256 ca618558…); tip unchanged. Re-run the workflows on this tip if your gate reads the body.

Please review 501c64b2… and, if it conforms, send it to the queue.

@HugoXK

HugoXK commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Self-test: PASS at 501c64b

Advisory report (diff only, not an acceptance gate), run by FlyBot Test and posted here by FlyBot Lead at the reviewer's request so the queue has a report naming this tip.

Ran at head 501c64b2… / tree 19b0a167… / single parent e65d7e54…:

  • Red first on a clean e65d7e54 tree with only this tip's core/test/settings-readonly-head.test.js: # tests 3 / # pass 1 / # fail 2 — not ok 1 "the host's sentence is drawn once" (0 !== 1) and not ok 3 "the read-only sentence is still on screen after the refused write".
  • The two suites at this tip: 4 / 4 / 0 (settings-error-placement.test.js + settings-readonly-head.test.js).
  • Order mutation (only the read-only line's position moved, after .bt-error): exactly one red, not ok 3 "the standing sentence sits above the refusal a single press produced"; reverted, working tree clean.
  • Generation: node core/scripts/build-client.mjs --check exit 0; node scripts/ds-bot-patches.mjs check three lines ok (ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70, 10 patches apply cleanly in order, core/ equals ds-bot/ + patches/ds-bot/) and canonical reports generate is a no-op. An independent rebuild from this tree is byte-identical to the committed bundle (sha256 ca618558abd39b1eaaeffd8927677ea2a9c13c9eb2d06e66d95cb71222a19b86), and both deltas are present in it — #2196's sidebar sentence and rail CSS, and this change's settings line.
  • Verify at this tip: exit 0 — core 2664, social 555, tree 127, timeline 140, workflow 368, .github/scripts 598, interop 37, live-e2e 24; arch-docs: OK (8 changed file(s)).
  • Probes (independent of the repo test): the read-only line is drawn once on all 8 pages with role="status", read-write structure differs by exactly one node, undefined / '' / ' ' / '\n\t ' / \u00a0 draw nothing, the real refusal path still shows role="alert" below it, and a zh-CN host sentence goes through hostText.

@Birfy
Birfy added this pull request to the merge queue Nov 2, 2026
Merged via the queue into ThinkFlowLab:main with commit 77a6b4d Nov 2, 2026
16 of 17 checks passed
@Birfy
Birfy deleted the dev/1-settings-readonly-head branch November 2, 2026 17:39
HugoXK added a commit to HugoXK/FlyBot that referenced this pull request Nov 2, 2026
…owLab#2212)

Fixes ThinkFlowLab#2202

Owner: Dev-1
Written-by: Dev-1
Reviewer: Birfy
Claim-Id: c72b3131ad0140a8b3dfd681f98e9d00
Branch: dev/1-single-refusal-alert
Base: 77a6b4d
Tier: T2
Head: a13f5b8
Tree: 2ce353b
Single parent: 77a6b4d

## What changes

A refused Rename, Delete or Create in the Workflows panel is now
announced **once**. `ThinkFlowLab#2193` put the host's sentence inside the dialog
that is still open, but the section kept drawing the same sentence in
its own page-level `.wf-err` `role="alert"` behind the mask — and
`role="alert"` is a live region, so a copy behind a mask is still read
out. One refusal produced two announcements of the same words.

`plugins/workflow/client.js` (+5/−2): the section's page-level alert
(`.wf-err`, `role="alert"`) is no longer drawn while one of the three
dialogs that prints that same refusal is open — `error && !renaming &&
!deleting && !creating ? … : null`. The dialog's copy is the only
announcement, and it is the one the reader is looking at. Nothing is
hidden with geometry: the second node is not rendered at all, which is
what the issue accepts as an answer. A refusal with no dialog open
(`workflow/graph`, `workflow/reset`, `workflow/pause` and the rest) is
reported on the page exactly as before, and no refusal path, success
path, or write behaviour changed.

## Red first

The new tests count `role="alert"` over the **whole rendered tree**
(`alertsOf(view)`, which is `findAll(view.tree, …)`), never inside the
dialog — counting inside the dialog is exactly why `ThinkFlowLab#2193`'s own tests
missed the duplicate. They also assert that the one remaining
announcement is inside the open dialog, and that a refusal with no
dialog still announces once.

Before the change (`plugins/workflow/test/client.test.js`, the four new
tests among 130):

```
not ok 127 - ThinkFlowLab#2202: a refused rename is announced once, and inside its dialog
  error: |-
    the whole tree carries the refusal exactly once
    + actual - expected

      [
        'That Bot is already in this workflow',
    +   'That Bot is already in this workflow'
      ]

  code: 'ERR_ASSERTION'
  name: 'AssertionError'
  expected:
    0: 'That Bot is already in this workflow'
  actual:
    0: 'That Bot is already in this workflow'
    1: 'That Bot is already in this workflow'
  operator: 'deepStrictEqual'
not ok 128 - ThinkFlowLab#2202: a refused delete is announced once, and inside its dialog
    the whole tree carries the refusal exactly once
not ok 129 - ThinkFlowLab#2202: a refused create is announced once, and inside its dialog
    the whole tree carries the refusal exactly once
# tests 130 / # pass 127 / # fail 3
```

The fourth test — a refused reset with no dialog open — passes before
and after: that is the path the guard must not break.

## The same family: one read-only sentence, drawn once

The same rule applies to the host's read-only sentence.
`core/src/client/settings.js` already prints `roster.readOnly` once for
**every** Settings page, as `.bt-hint.bt-read-only` with `role="status"`
and `hostText(roster.readOnly)`. The plugin's own Settings page printed
the same sentence a second time in `.wf-read-only` inside its scroll
box, so a read-only window said it twice.

`plugins/workflow/client.js` (−1): the page's `wf-read-only` line is
gone; the host header is the only place the sentence is drawn. The rest
of the read-only page is unchanged — its controls stay disabled,
`locked` still returns early, and nothing writes. This is the plugin's
half only: the sentence now lives in the host header, and the
composition is pinned by the core test that came with `ThinkFlowLab#2204`. Two
assertions in this file changed with it (they asserted the page printed
the sentence): `ThinkFlowLab#2187`'s first test and `ThinkFlowLab#2200`'s sentence test now
assert the page draws no copy of its own and stays locked.

Red first for this half, with the line still present (`# tests 131 / #
pass 129 / # fail 2`):

```
not ok 117 - ThinkFlowLab#2187: a read-only window offers no Edit control and leaves the sentence to the host
    + actual - expected
    +       className: 'wf-muted wf-read-only',
not ok 131 - ThinkFlowLab#2202: the plugin Settings page draws no read-only sentence of its own
    no wf-read-only node is drawn
    + actual - expected
    +       className: 'wf-muted wf-read-only',
```

Correction on the red-first counts: the `130 / 127 / 3` above was
measured with only the alert tests added, and the `131 / 129 / 2` above
with only the read-only pair, so each counted the failures of its own
half. Measured again on the clean base `77a6b4d4` with every test this
change adds or rewrites in place, the file is `# tests 131 / # pass 125
/ # fail 6`, and the six `not ok` are 117, 126, 127, 128, 129 and 131 —
the fourth alert test (a refusal with no dialog) passes at the base by
design, because that path must not change.

After the change the plugin page draws no `wf-read-only` node, the host
sentence is absent from this page's tree, and the page's `Model` select
is still `disabled`.

## After the change

Every check below ran on the tree of this head (`2ce353bc…`, single
parent `77a6b4d4`): `node core/scripts/build-client.mjs --check` exits 0
on it, so nothing generated is stale.

`npm test` in `plugins/workflow`: **373 tests / 373 pass / 0 fail** (the
suite was 368 at the base; five tests are the addition).

`npm run verify` — exit 0:

| suite | tests | fail |
| --- | --- | --- |
| core | 2664 | 0 |
| social | 555 | 0 |
| tree | 127 | 0 |
| timeline | 140 | 0 |
| workflow | 373 | 0 |
| .github/scripts | 598 | 0 |
| interop | 37 | 0 |
| live-e2e | 24 | 0 |

`arch-docs: OK (4 changed file(s))`; patches check: `ds-bot/ =
Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)`, `10 patches apply
cleanly in order`, `core/ equals ds-bot/ + patches/ds-bot/`,
`patches/ds-bot/ is what generate writes (generate is a no-op)`;
`build-client --check` exit 0.

Ran: `npm run verify` with `TZ=UTC
ARCH_DOCS_BASE=77a6b4d4693b8833f5d3c58b82b9a4b155b8a9c0` (which also
runs the core lint, `lint-plugins`, `build-client --check`, the patch
check, `canonical`, and the changelog check), the `plugins/workflow`
suite before and after the change, and the four new tests on the base
tree with the fix removed.
Not run: the smoke test (no `index.js` or `src/host/` change) and CI's
`package` job checks (pack check, typecheck, actionlint), which `npm run
verify` does not include.

`Policy-sign` is not written: none of the 4 changed paths is in
`policy/gates.json` gateFiles.

Changelog: `changelog.d/2202-single-refusal-alert.fixed.md`.

## Checkpoint

<!-- flybot:checkpoint -->
{
  "pr": 2212,
  "claim": "Dev-1",
  "issue": 2202,
"summary": "A refused Rename, Delete or Create is announced once: while
one of those dialogs holds the host sentence the section draws no
page-level role=alert copy of it, and the plugin Settings page no longer
reprints the host read-only sentence the header already draws. A refusal
with no dialog open still reports once. Tests count alerts and the
read-only line over the whole tree.",
"files": ["plugins/workflow/client.js",
"plugins/workflow/test/client.test.js", "docs/subsystems/workflow.md",
"changelog.d/2202-single-refusal-alert.fixed.md"],
"checks": { "ran": "TZ=UTC
ARCH_DOCS_BASE=77a6b4d4693b8833f5d3c58b82b9a4b155b8a9c0 npm run verify",
"result": "green", "at": "a13f5b86c30553bc73064723a0c3b16faa832fb6" },
  "next": "none — draft until the independent advisory; then ready"
}
<!-- /flybot:checkpoint -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:T1 Risk tier T1 (client, tests, generated files), set by the controller from the diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Settings: a read-only window never says so on the panel it is showing — eight pages offer write controls with no sentence explaining the refusal

2 participants