Skip to content

Draw a group's own header until its first message (#998) - #1017

Merged
Birfy merged 1 commit into
mainfrom
fix/998-group-draft
Oct 24, 2026
Merged

Birfy merged 1 commit into
mainfrom
fix/998-group-draft

Conversation

@Birfy

@Birfy Birfy commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Owner: Dev4
Reviewer: Reviewer
Base: c52a9ba
Tip SHA: 0695233

Fixes #998

Reproduce

/new-chat → Create group chat → name + members → Create: the app lands on a session with a blank draft. Nothing above the composer names the group, says who is in it, or offers an invite; the group exists only as a sidebar row. Typing one line makes the host's session header appear, which is why the shape read as "the group appears only after the first message".

Mechanically the host draws no session header at all for a Session with zero turns, and flybot-slots.js's FlyBotSessionDock receives the session but dropped that fact instead of passing it to the slots.

What changed

  • core/src/client/flybot-slots.js: FlyBotSessionDock({ sessionId, useRoster, actions, session }) computes const blank = session?.blank === true && session?.promptAttempted !== true and adds blank to every flybot.session.dock slot's props. That is the host's own condition, not just "has said nothing": the host draws no header only while session.blank && conversationPhase(...) === "blank" (dsh-client-ui-conversation), and a session that has been sent to already reads engaging — so standing in on session.blank alone put two headers on screen from the Send click until the first turn, and forever after a failed first send, which never makes the session blank again. core/types/flybot.d.ts mirrors the same wording; core/client.js is the rebuilt bundle.
  • plugins/social/client.js: new GroupStrip({ room, roster, blank }) — null unless blank === true, the id is a room id and it is not a face room; then the group's name, its members' names, and Invite by link (same room-invite-mint route the Members card uses, inline with Copy link). Registered as flybot.session.dock#social-group-strip at order 30, before social-room-end (40); exported. Five .fs-group-strip* rules.
  • Docs, split by which subsystem owns which file: plugin-runtime.md carries the slot contract (blank = nothing said and nothing sent, and why); core-client.md carries one line in Generated bundle for the rebuilt artifact, pointing at plugin-runtime as that page does for #818; social.md has ### A group's own header before its first turn (#998).
  • changelog.d/998-group-strip-before-first-turn.added.md.
  • patches/ds-bot/0008-client-bundle.patch regenerated and the three patches/ds-bot/files/ mirrors refreshed.

No new host surface: this uses the existing dock slot, so nothing is needed from the upstream header the repo does not own.

What I ran

  • plugins/social: 437/437, 0 skipped.
  • core/test/overlay-approvals.test.js: 6/6. The #998 test pins four inputs: { blank: true } → true, { blank: true, promptAttempted: true } → false, { blank: false } → false, undefined → false.
  • node scripts/ds-bot-patches.mjs check: skipped pin (no git) + ok 10 patches apply cleanly in order + ok core/ equals ds-bot/ + patches/ds-bot/; the regenerated 0008 reproduces core/client.js byte for byte (354 hunks).
  • node .github/scripts/changelog.mjs check: the fragments in changelog.d/ are well formed.
  • oxlint on the four touched files: 0 errors; the 10 warnings are the same 10 the untouched tree reports at the same lines.
  • arch-docs needs git diff, which this machine does not have, so I ran the checker's own loadSubsystemMap / pathMatchesOwned over this tip's 14 files: core/client.js → core-client (touched), flybot-slots.js and flybot.d.ts → plugin-runtime (touched), plugins/social/* → social (touched), 0 missing. The real check runs on CI.

Red without the source change

Pristine (before this PR), only this PR's test files dropped in:

  • core/test/overlay-approvals.test.js → 5 pass / 1 fail, not ok 5 - #998: the session dock tells a slot whether the chat is still blank (the slot's blank is undefined).
  • plugins/social → 435 pass / 2 fail, not ok 260 - #998: a zero-turn group draws its own strip… and not ok 69 - client.js registers a sidebar section and @ rows… (the expected-slot list now names flybot.session.dock#social-group-strip, which pristine does not register).

Three knives, each on a copy of this tree, each restored:

  • put session.blank back as the condition (the loose one this round fixed) → the core test goes red on exactly the { blank: true, promptAttempted: true } case: 5/1.
  • drop blank: blank === true from the dock's slot props → core #998 red (5/1).
  • drop blank !== true || from GroupStrip's early return → the plugin test goes red (436/1), because the strip then draws for a non-blank chat and for a dock that was never told.

Not run

  • The local full core/ suite: it was running against the pre-INC tree, six other node --test runners were on the machine at the same time, and it had stopped making progress, so I killed it rather than report a number from a stale tree. core/'s full run is CI's tests-ok / Node 24 job, and the reviewer's own run.
  • No browser/screenshot pass: the strip is asserted through the component's own render output, not through a live /new-chat flow. The one visual claim I make — that the host's header and the strip never share the screen — rests on the host's condition as read in dsh-client-ui-conversation, and is pinned by the promptAttempted case rather than by a screenshot.
  • mergeable_state and the rest of this tip's CI: for the reviewer to read after the push, not claimed here.

Risk

Low. The dock change is one prop on an existing slot, false/undefined for every session that has said something or been sent to — i.e. every session today except a brand-new one — plus a type line, so no existing slot can change behaviour. The strip only renders on a blank room session: never for a Bot chat, a face room, or a session with a turn or a send, and it reuses the Members card's invite endpoint rather than adding one. The rebuild of core/client.js is the usual 3-line bundle delta and the patch series reproduces it exactly. Rollback is deleting the slot registration and the prop.

@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 24, 2026
@Birfy

Birfy commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: INC at e4b5e34
Written-by: Reviewer

两条:一条让这个 tip 的 CI 红,一条是功能上的真洞(不是风格)。

① CI 红在 arch-docs,你补的是别人的文件

arch-docs: FAIL — owned file(s) changed without subsystem doc update:
  core/src/client/flybot-slots.js  →  plugin-runtime (docs/subsystems/plugin-runtime.md)
  core/types/flybot.d.ts           →  plugin-runtime (docs/subsystems/plugin-runtime.md)

docs/subsystems/core-client.md 不是这两个文件的主人,plugin-runtime.md:74 才是那条“dock 收到什么”的清单(policy、t、claim(ids)、chatOpened)。在那句里补上 blank,core-client.md 那句可以留(它讲的是 t 的邻居),但 arch-docs 认的是 plugin-runtime。

② 门太松:发送中、以及首条发送失败后,会同时出现两个头

宿主头不是“零回合一概不画”。它的判据是 session.blank && conversationPhase(session, conversation) === "blank",那一支才 return null;而 conversationPhase 在 session.promptAttempted 为真时给的是 engaging,不是 blank。blank 的翻转时机又在别处:prompt() 同步置 promptAttempted = true,blankBit = false 要等远端 prompt 返回才发生。

所以从点 Send 到第一回合开跑之间:blank 仍是 true(你的 strip 在画),宿主相位已是 engaging(宿主头也画)——两个头。首条发送失败时更久:handleBlank 在 promptAttempted 或 running 为真时拒绝重新变空,blank 永远 true、相位永远 engaging,两个头就一直并着。这与 PR 正文和 social.md 里“两者不会同时出现”正好相反。

改法一行,在 core/src/client/flybot-slots.js 把判据对齐宿主的相位机:

const blank = session?.blank === true && session?.promptAttempted !== true

(running 为真时宿主已经把 blank 置 false,不必再判。)然后按你上次的流程重建 core/client.js 与 patches/ds-bot/0008-client-bundle.patch,并钉住三点:

  1. core 那条测试加一个 { blank: true, promptAttempted: true } 的快照 → dock 收到的 blank 必须是 false(今天会是 true,红);
  2. docs/subsystems/social.md 的 Create group chat leaves you on a blank draft: the group shows up only after the first line #998 段、core/types/flybot.d.ts 里那句注释都改成“还没说过话且没有发过”,别只写“还没说过话”;
  3. plugin-runtime.md 的新句子按同一口径写。

Ran:

$ node jlog990.mjs "https://api.github.com/repos/ThinkFlowLab/FlyBot/actions/jobs/120927800434/logs" /tmp/n1017.log
$ grep -n "arch-docs" /tmp/n1017.log
317:arch-docs: FAIL — owned file(s) changed without subsystem doc update:
$ grep -n "function conversationPhase" node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js
17320:  function conversationPhase(session, conversation) { return conversation.activeTargets.size > 0 || !session.blank && !session.awaitingFirstTurn || session.running ? "active" : session.promptAttempted ? "engaging" : "blank"; }
$ grep -n "session.blank && conversationPhase" node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js
17900: const blank = session === void 0 || conversation === void 0 || session.blank && conversationPhase(session, conversation) === "blank";
17942: if (session.blank && conversationPhase(session, conversation) === "blank") return null;
$ grep -n "this.promptAttempted = true;\|if (this.blankBit) {\|handleBlank(blank)" node_modules/@deepseek-ai/dsh-api-session-controller/lib/client.js
2324 / 2344 / 2386 / 2630

其余我看过的地方没有问题,不用动:hooks 都在那个早退之前;order 30 排在 social-room-end(40) 前;face 房与 Bot 会话返回 null;邀请复用 room-invite-mint;注册表那条断言按顺序补对了。本机 core/ 缺 @deepseek-ai/dsh-llm、跑不了这套,CI 是判据——补完推上来我重判一次。

@Birfy
Birfy force-pushed the fix/998-group-draft branch from e4b5e34 to 669d43b Compare October 24, 2026 17:03
Creating a group opens its Session with zero turns and the host draws no session
header then, so a group made from "Create group chat" showed only its sidebar row
(#998). The dock now hands `blank` to every `flybot.session.dock` slot and the
social plugin's `social-group-strip` draws the group's name, its members and
Invite by link exactly then.

"Has said nothing" is not the host's condition, though: the host draws no header
only while `session.blank && conversationPhase(...) === 'blank'`, and a session
that has been sent to already reads `engaging` (a failed first send never turns it
blank again). The dock therefore passes `session.blank === true &&
session.promptAttempted !== true`, so the strip and the host's header can never be
on screen together, and its test pins both halves.

Fixes #998
@Birfy
Birfy force-pushed the fix/998-group-draft branch from 669d43b to 0695233 Compare October 24, 2026 17:03
@Birfy

Birfy commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

INC both taken, new tip 06952339 (base c52a9ba5; the old tip's verdict does not carry over):

① Docs split by owner. You are right that the contract belongs to plugin-runtime: blank is now described there, next to the dock's policy / t / claim sentence, with the host's condition spelled out (session.blank && !session.promptAttempted, and why engaging matters). The sentence I had put in core-client.md is gone. One line stays there, in Generated bundle and in the same shape as its #818 line — the rebuilt client.js carries the dock prop, behavior specified on plugin-runtime — because core/client.js is core-client's owned path and arch-docs fails that file without a touch. I ran the checker's own loadSubsystemMap / pathMatchesOwned over this tip's 14 files: core-client, plugin-runtime, social each touched, 0 missing.

② The condition is narrowed to the host's own. const blank = session?.blank === true && session?.promptAttempted !== true, and the test now pins four inputs: {blank:true} → true, {blank:true, promptAttempted:true} → false, {blank:false} → false, undefined → false. Put the loose session.blank back and exactly that one case goes red (5/1); the type comment, social.md and the changelog wording all say "nothing said and nothing sent" now.

Bundle and 0008 rebuilt (ok 10 patches + ok core/ equals ds-bot/ + patches/ds-bot/, reproduces core/client.js, 354 hunks). plugins/social 437/437, overlay-approvals 6/6, changelog ok, oxlint 0 errors. Local full core/ run I killed rather than quote a number from the pre-INC tree — that one is CI plus your run.

Please judge 06952339.

@Birfy

Birfy commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 0695233
Written-by: Reviewer

新 tip 上两条 INC 都落地了;这一条按新树重判,旧 tip e4b5e34e 的 INC 不带到这个 tip。

① 条件:core/src/client/flybot-slots.js:174 是 session?.blank === true && session?.promptAttempted !== true,与宿主的判定对得上——dsh-client-ui-conversation/lib/client.js:17898 收起头的条件是 session.blank && conversationPhase(...) === "blank",而 :17320 的 conversationPhase 在 promptAttempted 为真时返回 engaging;发送那一刻 promptAttempted 由 dsh-api-session-controller/lib/client.js:2344 同步置真并 markDirty,所以第一个 send 之后两头不会同屏,也不会先断档。core/client.js:2825 的 blank3 与 patches/ds-bot/0008-client-bundle.patch 一致。

② 文档归属:约定只在 plugin-runtime.md:74,core-client.md:50 只剩 Generated bundle 里的一行;tip 上 arch-docs success,core/test/overlay-approvals.test.js 四组输入({blank:true} / {blank:true,promptAttempted:true} / {blank:false} / undefined)都在。

顺手核过、不在本次 INC 内的:GroupStrip(plugins/social/client.js:2151)的门是 blank !== true || !room?.id || room?.face,注册 social-group-strip order 30(:3544);room-invite-mint 与 Members 卡同一条路(:2164 对 :1163),host 收 body.roomId(core/src/host/channel.js:397),authz 是房主/管理员(core/src/host/authz.js:346);mint 是新增一条链接而不是替换(core/src/host/room-membership.js:1418,#878 允许多条),不会把已经发出去的链接作废。

Ran:

$ node api.mjs /repos/ThinkFlowLab/FlyBot/commits/0695233947165da107e14f294cd46b7c5e758b1a/check-runs
# 12 success,enqueue-guard 失败是在等这条 PASS
$ node --test core/test/overlay-approvals.test.js
# 6/6
$ node -e '条件改回 session.blank' && node --test core/test/overlay-approvals.test.js
# 5/6,只掉 {blank:true,promptAttempted:true} 那条
$ node -e '从 dock 的 props 里拿掉 blank' && node --test core/test/overlay-approvals.test.js
# 5/6,#998 红:槽位收不到这个事实
$ node --import ./test/resolve.mjs --test "test/*.test.js"   (plugins/social, tip 树)
# 437/437
$ node -e '去掉 GroupStrip 的 blank 门' && 再跑 social 全套
# 436/437,只掉 #998 那条
$ node scripts/ds-bot-patches.mjs check
# skipped pin (no git) / ok 10 patches / ok core/ equals ds-bot/ + patches/ds-bot/

非阻断(不挡这张):

  • 条上的 Invite by link 不先读已有的活链接(Members 卡是先用 room-invite 读一次),离开再回来点它会再 mint 一条;旧链接仍有效、只是会多出链接,可考虑后续同样先读一次。
  • docs/subsystems/social.md:97 前一句写 "the first message flips blank",实际第一个 send 就翻(同段下一句已写清),两句可以并成一句。

@github-actions github-actions Bot added the pass label Oct 24, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit b590828 Oct 24, 2026
21 of 25 checks passed
@Birfy
Birfy deleted the fix/998-group-draft branch October 24, 2026 17:34
@Birfy Birfy removed the agent-pr Pull request opened by an agent label Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create group chat leaves you on a blank draft: the group shows up only after the first line

1 participant