Skip to content

fix(preview): keep the words around a complete untrusted wrap - #2520

Open
HugoXK wants to merge 1 commit into
ThinkFlowLab:mainfrom
HugoXK:delivery1/main-preview-fence-words
Open

HugoXK wants to merge 1 commit into
ThinkFlowLab:mainfrom
HugoXK:delivery1/main-preview-fence-words

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2443

Owner: Dev-1
Reviewer: Birfy
Written-by: Delivery-1
Claim-Id: 64f1efea39d040bbb2460674a667c098
Base: 90a5944
Risk tier: T3

What changed

visibleText unwrapped a complete <untrusted-message …>…</untrusted-message> by keeping only the text between the first opener and the last closer, so a stored line that quoted a remote line and then answered it reached the screen as the quote alone — in the Bot chat's row, in a sidebar row, and in the host's room preview. The module's own comment promises the opposite ("words that never were part of a marker are kept, because a preview is still the line someone said"), so this is the code disagreeing with its documented contract.

After: each complete wrap loses its markers in place — the head before it, its inner text and the tail after it all survive. Two wraps in one line both unwrap, a line that is only a wrap still reads as just its words, and a broken fragment keeps today's behaviour. stripFence is unchanged.

The four readers this exercises, and what red first showed at the base:

  • core/src/client/text.js (readableLine → previewOf): drew quoted.
  • core/src/client/sidebar.js (RoomRow → visibleText(room.preview)): drew quoted.
  • core/src/host/channel.js (view → visibleText(last.text) on the /api/bot state answer): drew quoted.
  • core/src/client/exchange.js (exchangeText, the ⌘K sheet): never had this defect — it strips head and tail itself and only uses stripFence, so it stays a labelled control rather than a red assertion.

Generated, not hand-edited: core/client.js (the module is inlined in the bundle) and patches/ds-bot/0008-client-bundle.patch + the patches/ds-bot/files/** mirrors, all from node scripts/ds-bot-patches.mjs generate after npm run build. generate rewrote nothing else: the other nine patch files are byte-identical, and core/src/shared/preview.js is a new file with no .patch header of its own (topics.json has no entry; whole-file mirror only).

Verification at the tip

Tip SHA: afc1013

Ran:

  • red first on a clean 90a594471b5886737b433e116f6397aa8ca2b6f2 tree carrying only core/test/preview-fence-words.test.js: # tests 4 / # pass 1 / # fail 3 — the three consumer paths returned He said quoted and I answered done. as quoted; the exchange control passed.
  • after the fix, that suite: # tests 4 / # pass 4 / # fail 0
  • full core suite, TZ=UTC node --import ./test/harness-resolve.mjs --test "test/*.test.js": # tests 2931 / # pass 2931 / # fail 0
  • node scripts/build-client.mjs --check: exit 0
  • node scripts/ds-bot-patches.mjs check: 10 patches apply cleanly in order, core/ equals ds-bot/ + patches/ds-bot/
  • node scripts/ds-bot-patches.mjs canonical: ok patches/ds-bot/ is what generate writes
  • npm run lint: 82 warnings, 0 errors
  • node .github/scripts/changelog.mjs check: 36 fragment(s) in changelog.d/ are well formed
  • ARCH_DOCS_BASE=90a594471b5886737b433e116f6397aa8ca2b6f2 npm run arch-docs: OK (7 changed file(s))

Not run: the smoke test (no index.js or src/host behaviour change beyond the preview string) and CI itself.

Docs-not-needed: core/src/shared/preview.js is not on an owned_path of any subsystem page; this fixes the rule the module already documents, with no new module, contract or owned-path change.

Rebased onto 90a594471b5886737b433e116f6397aa8ca2b6f2 after #2516 merged (10:41:44Z); every number above was measured again on that base with a fresh npm run build + generate, and the readings taken at the earlier tip dabed9eb are dropped. This stays a draft and is not ready to enqueue until the client bundle lane says so; the single comment requesting a Verdict at this tip is posted then, not before.

Checkpoint

{
  "pr": 2520,
  "claim": "Dev-1",
  "issue": 2443,
  "summary": "A stored line that quotes a remote line and then answers it reached the Bot chat's row, a sidebar row and the host's room preview as the quote alone: visibleText kept only the text between the first opener and the last closer. Each complete wrap now loses its markers in place, so the words around it survive. Red first at 90a59447: 3 of 4 new tests failed; the exchange sheet never had the defect and stays a control. At the tip the full core suite is 2931/2931 and generate rewrote only the bundle patch and the two mirrors.",
  "files": [
    "core/src/shared/preview.js",
    "core/test/preview-fence-words.test.js",
    "core/client.js",
    "patches/ds-bot/0008-client-bundle.patch",
    "patches/ds-bot/files/src/shared/preview.js",
    "patches/ds-bot/files/test/preview-fence-words.test.js",
    "changelog.d/2443-preview-fence-words.fixed.md"
  ],
  "checks": {
    "ran": "TZ=UTC node --import ./test/harness-resolve.mjs --test \"test/*.test.js\"; node scripts/build-client.mjs --check; node scripts/ds-bot-patches.mjs check; node scripts/ds-bot-patches.mjs canonical; npm run lint",
    "result": "green",
    "at": "afc10134caf5c392d5c157bbb777cc210b59ae25"
  },
  "next": "stay a draft until the Lead releases the client lane; then post the one comment requesting a Verdict naming afc10134caf5c392d5c157bbb777cc210b59ae25"
}

@github-actions github-actions Bot added the risk:T3 Risk tier T3 (needs a human approval), set by the controller from the diff label Nov 5, 2026
@HugoXK
HugoXK force-pushed the delivery1/main-preview-fence-words branch from dabed9e to afc1013 Compare November 5, 2026 10:45
@HugoXK

HugoXK commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Written-by: Delivery-1
Owner: Delivery-1
Reviewer: Birfy

Requesting the Verdict at afc10134caf5c392d5c157bbb777cc210b59ae25 — this is the only comment this pull request gets.

Base 90a594471b5886737b433e116f6397aa8ca2b6f2 (#2516's merge commit, single parent d1ea853c5aa0d1c45295bb9d013284d2c36d7281). Head parent is that base; one commit; 7 files +276/−15; maintainer_can_modify: true.

Per-file numstat against the base:

  • changelog.d/2443-preview-fence-words.fixed.md 1/0
  • core/client.js 5/3
  • core/src/shared/preview.js 8/4
  • core/test/preview-fence-words.test.js 124/0
  • patches/ds-bot/0008-client-bundle.patch 6/4
  • patches/ds-bot/files/src/shared/preview.js 8/4
  • patches/ds-bot/files/test/preview-fence-words.test.js 124/0

Red first, on a clean 90a59447 tree carrying only core/test/preview-fence-words.test.js — # tests 4 / # pass 1 / # fail 3, the three real ones verbatim:

  1. Bot chat row (readableLine): expected He said quoted and I answered done. / actual quoted
  2. sidebar row: the row drew ["quoted"]
  3. host /api/bot state answer: actual quoted / expected He said quoted and I answered done.

The fourth (the ⌘K exchange sheet) is an explicitly labelled control: it always kept head and tail itself, so it passes on the clean base by design and asserts the contract, not the fix.

After the fix, the same suite: # tests 4 / # pass 4 / # fail 0. Full core suite: # tests 2931 / # pass 2931 / # fail 0.

The three generated-tree gates, verbatim at this tip:

  • node scripts/build-client.mjs --check → exit 0
  • node scripts/ds-bot-patches.mjs check → 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/
  • node scripts/ds-bot-patches.mjs canonical → ok patches/ds-bot/ is what generate writes (generate is a no-op)

Generated set is exactly {patches/ds-bot/0008-client-bundle.patch, patches/ds-bot/files/src/shared/preview.js, patches/ds-bot/files/test/preview-fence-words.test.js}, produced by npm run build + generate; the other nine patch files are a 0-byte change, and git status after generate is empty.

Tier: risk-tier: T3; basis: core/src/shared/preview.js=T3, …. Also green at this tip: npm run lint 82 warnings / 0 errors; changelog.mjs check 36 fragments; ARCH_DOCS_BASE=90a59447 arch-docs OK (7 changed files).

Held as a draft until the Lead flips it after the Verdict.

`visibleText` unwrapped a complete `<untrusted-message …>…</untrusted-message>` by
keeping only the text between the first opener and the last closer, so a stored line
that quotes a remote line and then answers it reached the screen as the quote alone.
The module's own comment promises the opposite — words that were never part of a marker
are kept — so this is the code disagreeing with its documented contract.

Each complete wrap now loses its markers in place: the head before it, its inner text
and the tail after it all survive. `stripFence` is unchanged.

Docs-not-needed: `core/src/shared/preview.js` is not on an owned_path of any subsystem page; no module, contract or owned path changes, only the reader's own rule.
@HugoXK
HugoXK marked this pull request as ready for review November 5, 2026 13:30
HugoXK added a commit to HugoXK/FlyBot that referenced this pull request Nov 5, 2026
…rlays offer no Reload (ThinkFlowLab#2523)

Fixes ThinkFlowLab#2521

Owner: Dev-2
Reviewer: Birfy
Written-by: Dev-2
Claim-Id: cce0d5e889b74566b96d54c6ec7ff13c
Base: d400edb
Risk tier: T1

## What changes

Four read-only surfaces are drawn **inside a full-screen overlay**,
whose scrim covers the sidebar's own Reload: the reader mid-task had to
close the overlay, press Reload, and open it again. Each already
repeated the host's sentence, so nothing looked broken until a try.

1. **Board** — `BoardView` (`core/src/client/kanban.js`) drew the host
sentence in `bt-board-hint` and nothing beside it. `KanbanDialog`
already receives `actions`, so `BoardView` now takes them and draws
`readOnlyLine(h, roster, actions, { key: 'readonly', className:
'bt-board-hint' })`.
2. **New chat, admin chosen** — the group block in `NewChat`
(`new-chat.js`) drew the bare sentence where **Create** was; it now
draws `readOnlyLine(h, roster, actions, { key: 'readonly-admin' })`.
3. **New chat, composer** — the otherwise branch of the same component
drew the same bare sentence; it now draws `readOnlyLine(h, roster,
actions, { key: 'readonly-compose' })`. The two are mutually exclusive
states of one component, so each keeps its own key and a mount covers
only one.
4. **Project settings panel** — `ProjectSettingsPanel`
(`project-board.js`) received only `call/onClose/onSaved`, so it had no
reload to offer. `ProjectBoardOverlay`, which mounts it, now passes
`actions` down and the locked branch draws `readOnlyLine(h, { readOnly
}, actions, { key: 'readonly' })` above **Back to board**.

Nothing new is invented: `readOnlyLine` is the piece added in ThinkFlowLab#2502
(`core/src/client/read-only.js`). The sentence stays the one
`role="status"` node (`bt-hint bt-read-only`), the Reload button sits
outside it so the live region remains text, a press runs
`actions.reloadTeam` (the client's own team read, never a page reload),
and a failed read is reported through `reportError(error, 'reload')`.
One screen draws at most one entry; a writable window draws neither.
Every write control a writable window has is untouched.

## Acceptance

In a read-only window, with the overlay open:

- each of the four surfaces draws exactly one Reload beside the host's
sentence (`readonly-reload-lines.test.js` counts one sentence and one
entry per mount);
- the sentence is drawn first and the control outside the live region;
- one press runs the client's team read once (`actions.reloadTeam`), and
no write is sent;
- the Board keeps its readable rail, columns and cards, and keeps
drawing no **Add task**, **Start**/**Complete** or drag; the New chat
pane keeps its pickers as a preview and draws no **Create**/**Send**;
the Project settings panel keeps its sentence and **Back to board** and
still draws none of **Bind**/**Unbind**, the digest box, **Save**, the
memory box or **Save memory**;
- a writable window is unchanged, and a host sentence of only whitespace
draws neither.

## Red first

On a clean `d400edb74798b71e233eccd46f221eaef782ff5d` tree whose source
is untouched and which holds only this branch's extended test file,
`TZ=UTC node --import ./test/harness-resolve.mjs --test
test/readonly-reload-lines.test.js` exits 1:

```
not ok 3 - every surface that is not the header or a drawer offers one Reload — its own way out of the modal
    Board dialog: one Reload entry
    0 !== 1
not ok 4 - one shared piece: the sentence is the host sentence, and the control sits outside the live region
    Board dialog: the sentence is read first
    expected: true
    actual: false
not ok 5 - a press runs the client's own team read, once, and sends no write
    Board dialog: the entry is drawn
    expected: true
    actual: false
# tests 7
# pass 4
# fail 3
# cancelled 0
```

The three failures are loop assertions (one per behaviour across all
surfaces), so the count is 3, not one per surface. A throwaway probe
measured each new surface individually before the fix — `Board dialog`,
`New chat: composer`, `New chat: admin chosen`, `Project settings panel`
— all `sentences=1 / reloads=0`. The extended `SURFACES` is **11**
entries (7 existing + 4 new; the two New chat states need one mount
each) and `mountFor` gained four branches. After the fix the same file
is **7/7** green.

## Why the base is `d400edb7` and the branch was rebased

`ThinkFlowLab#2538` merged into `main` at `14:16:25Z` (merge sha `d400edb7…`) and
rewrote `patches/ds-bot/0008-client-bundle.patch`; this branch carries
that same patch. A textual merge of the two is clean, but the merged
series is invalid — measured locally on a scratch worktree, `node
scripts/ds-bot-patches.mjs check` on the merge result fails with
`0008-client-bundle.patch does not apply cleanly: error: corrupt patch
at line 15523` and `patches/ds-bot/ is not what generate writes — the
committed series drifted`, while `build-client --check` alone still
passes. So the branch was rebased onto `d400edb7` and regenerated once;
the carrier patches are now re-cut against the new base.

## What I ran at the fork head (fork head
`81b9a7ac1940eb5949bb7b50e4b462e9be3dc742`, tree
`83d6706529e88a7db759634d31177e09fd3e466c`; `TZ=UTC`, clean tree)

- `TZ=UTC ARCH_DOCS_BASE=d400edb74798b71e233eccd46f221eaef782ff5d npm
run verify` — **exit 0**. Segment counts, each `# fail 0` and `#
cancelled 0`: core **2933 success + 0 cancelled**, plugins/social **577
+ 0**, plugins/tree **137 + 0**, plugins/timeline **141 + 0**,
plugins/workflow **440 + 0**, `.github/scripts` + `policy` **671 + 0**,
`test:interop` **37 + 0**, `test:live-e2e` **26 + 0** — **4962 success +
0 cancelled, 0 failed**. `arch-docs: OK (12 changed file(s))`; every
lint stage exit 0 with **0 errors** (warnings 81/36/9/2/16, plus 1 and 0
for the interop/live-e2e linters); the same run's `build-client.mjs
--check` exit 0.
- The 13 neighbouring suites (`create-bot-readonly`, `create-bot`,
`group-create-readonly`, `kanban`, `new-chat-menu`, `new-chat`,
`project-board`, `project-board-entry`, `project-board-levels`,
`project-board-panels`, `project-board-readonly`, `project-board-view`,
`readonly-reload-lines`): **183/183, 0 failed**.
- `node core/scripts/build-client.mjs --check` — exit 0, no output.
`node scripts/ds-bot-patches.mjs check` — three `ok` lines: `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/`.
`node scripts/ds-bot-patches.mjs canonical` — `ok patches/ds-bot/ is
what generate writes (generate is a no-op)`; a further `generate` run
leaves `git status --porcelain` empty.
- `git status --porcelain` — empty on the committed tree.

## What I did not run

- The host smoke test and a live service on a bench port — this change
is client-only (`core/src/client/**`), no host code.
- Anything on `ThinkFlowLab#2520`, `ThinkFlowLab#2517` or `ThinkFlowLab#2443` — untouched.

## Files

12 files, `+451/−354` (`git diff --numstat d400edb...HEAD`), tree
`83d67065`:

- `core/src/client/kanban.js` (+7/−5) — `BoardView` takes `actions`; the
hint becomes `readOnlyLine`.
- `core/src/client/new-chat.js` (+4/−3) — both locked branches draw
`readOnlyLine`, one key each.
- `core/src/client/project-board.js` (+4/−3) — `ProjectSettingsPanel`
takes `actions`; `ProjectBoardOverlay` passes it.
- `core/test/readonly-reload-lines.test.js` (+45/−2) — `SURFACES` 7 → 11
and four `mountFor` branches.
- `docs/subsystems/core-client.md` (+4/−4) — the four surfaces and the
panel's `actions` are documented.
- `changelog.d/2521-overlay-readonly-reload.fixed.md` (+1, new).
- `core/client.js` (+10/−8),
`patches/ds-bot/0003-flybot-api-client.patch` (+5/−5, carries
`new-chat.js`), `patches/ds-bot/0008-client-bundle.patch` (+315/−314,
carries `kanban.js` and `project-board.js`),
`patches/ds-bot/files/src/client/kanban.js` (+7/−5),
`patches/ds-bot/files/src/client/project-board.js` (+4/−3),
`patches/ds-bot/files/test/readonly-reload-lines.test.js` (+45/−2) —
generated by `build-client` + `ds-bot-patches generate`, never
hand-edited. The two carrier patches are exactly the ones whose source
files this branch changes — the carrier set `{0003, 0008}` is exactly
the set that carries the changed sources, and no third patch is touched
(the other eight patches are byte-identical to the base).

<!-- flybot:checkpoint -->
```json
{
  "pr": 2523,
  "claim": "Dev-2",
  "issue": 2521,
  "summary": "Four read-only surfaces drawn inside overlays — the Board hint, both New chat sites and the Project settings panel — now draw the shared readOnlyLine piece instead of their own copy of the host sentence. Each keeps the sentence in its one role=status node and puts a single Reload outside it, running actions.reloadTeam, never a page reload. The panel now receives actions from ProjectBoardOverlay, which previously passed no reload at all. A writable window and every read-only control are unchanged.",
  "files": [
    "core/src/client/kanban.js",
    "core/src/client/new-chat.js",
    "core/src/client/project-board.js",
    "core/test/readonly-reload-lines.test.js",
    "docs/subsystems/core-client.md",
    "changelog.d/2521-overlay-readonly-reload.fixed.md"
  ],
  "checks": {
    "ran": "TZ=UTC ARCH_DOCS_BASE=d400edb7 npm run verify; build-client --check; ds-bot-patches check/canonical; 13 neighbouring suites",
    "result": "green",
    "at": "81b9a7ac1940eb5949bb7b50e4b462e9be3dc742"
  },
  "next": "on the rebased base the branch carries a re-cut 0008; the source/tests/docs half is unchanged, so review the regenerated carrier patches only"
}
```
<!-- /flybot:checkpoint -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:T3 Risk tier T3 (needs a human approval), set by the controller from the diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

preview: words before and after a complete untrusted wrap are dropped from the row

1 participant