Skip to content

studio: refetch drafts on focus and poll while the tab is visible, so changes made outside the tab show without a reload — ruled C, phase 1 (phase 2 is #33023) #32592

Closed

Description

@objectstack-fleet

eval#1 · os-bob · session_012dn2AcTB69pEwZ9ugr1g1w

Filing gate: ② a decision only the maintainer can make.

维护者速读

  • 现象:通过 MCP 修改元数据(落成草稿)后,已经打开的 Studio 页面上「变更」角标和草稿标记都不变,看起来像"没改成";必须手动刷新浏览器才能看到。反过来,在 Studio 发布后回到 MCP 也无从得知。只有打开「变更」抽屉那一刻会重读列表。
  • 要你裁:Studio 是否要对"本标签页之外"的元数据变更自动刷新,以及走哪种形状(前端轮询/聚焦重取,还是服务端推送)。
  • 推荐:B → C 分期 — 先在 usePendingDrafts 加"窗口重新获得焦点时重取 + 页面可见时低频轮询",一个 PR 解决今天的困惑;再把草稿变更接入已有的 realtime 通道,作为终态。

One-sentence problem

When an agent changes the app over MCP, the Studio page the user already has open keeps showing the old state — no pending-change badge, no draft marks — until the user thinks to reload the browser, so a change that worked looks like a change that failed.

Evidence (reach)

Session on the os-dev-test environment (framework c110853e): metadata drafts staged by MCP authoring tools (apply_edit, update_metadata, create_seed) did not appear in an open Studio tab's Changes badge until a manual browser reload; the user reported it as "Studio 页面上看不到是否已变更".

Code reading (all re-checkable):

  • ui/packages/app-shell/src/preview/usePendingDrafts.ts — the single source of the pending-drafts count (GET /api/v1/meta/_drafts). It refetches on mount, scope change and the in-tab subscribeMetadataRefresh pulse (:248) only; no polling, no focus/visibility refetch. Re-check: grep -n "setInterval\|visibilitychange\|subscribeMetadataRefresh" ui/packages/app-shell/src/preview/usePendingDrafts.ts.
  • ui/packages/app-shell/src/assistant/assistantBus.ts:156-169 — the refresh bus is an in-memory per-tab listener set; it is pulsed by Studio's own saves/publishes and by the in-console AI chat (console/ai/AiChatPage.tsx ~:2425), which is why Studio refreshes for its own copilot but never for an external MCP client.
  • ui/packages/app-shell/src/preview/DraftChangesPanel.tsx ~:1629 — the Changes sheet re-reads its list each time it opens; the badge and per-item draft marks do not.
  • Server: a draft save emits emitMetadataMutation({ state: 'draft' }) in-process only; the cluster fan-out returns early for drafts (packages/metadata-protocol/src/protocol-mutation-fan-out.ts:115, protocol-class-head.ts:375), and MetadataManager.publishRealtimeMetadataEvent fires only from register()/delete, never for drafts. Re-check: grep -n "state === 'draft') return" packages/metadata-protocol/src/*.ts.
  • The only browser push for metadata is the dev-only HMR SSE (GET /api/v1/dev/metadata-events, NODE_ENV=development; client ui/apps/console/src/components/MetadataHmrReloader.tsx, import.meta.env.DEV, full location.reload()).
  • The QA checklist already records the gap as expected: docs/qa/platform-checklist/areas/studio-authoring.json item studio-authoring.draft-publish-lifecycle — "A publish made outside Studio (the REST publish …) shows in Studio only after a reload". Re-check: grep -n "only after a reload" docs/qa/platform-checklist/areas/studio-authoring.json.

Governing text

  • docs/NORTH-STAR.md line 7 — "Two kinds of user: today, business staff through the UI …; tomorrow, agents through MCP. Every step of the road must hold for both kinds of user." An agent's change that the UI user cannot see breaks the hand-off between the two.
  • docs/adr/0008-metadata-repository-and-change-log.md (~:140-170) — describes a change log with seq and watch()/subscribe(since=seq) with an SSE/WS broadcast; only the dev HMR stream was built.
  • docs/adr/0037-live-canvas-draft-preview.md Phase 2 (~:68-73) — live refresh via assistantBus for the in-console chat only; external clients are out of its scope.
  • Protocol change? No spec change for A/B; C adds draft events to the realtime channel (a server contract), which needs an ADR-0008 amendment.

Options

Option What it does What the customer sees Effort
A Keep today's behaviour; document "reload to see changes made outside Studio" Agent says "改好了", Studio shows nothing until reload none (doc line)
B Client-side: usePendingDrafts (and the package metadata refresh) also refetch on window focus / visibilitychange, plus a low-frequency poll (e.g. 15–30 s) while the tab is visible Switching back to the Studio tab shows the agent's changes within a moment; an idle visible tab catches up within the poll interval ui seat · 1 PR · no measured precedent; small (one hook + its consumers' pulse)
C Server push: forward draft (and publish) metadata mutations to the existing service-realtime channel (metadata.<type>.drafted/published), and have the console subscribe (RealtimeAPI.subscribeMetadata exists in the client SDK, unused) and pulse emitMetadataRefresh Changes appear live, in every open tab, including from MCP/REST/other users server + ui seats · 2–3 PRs · ADR-0008 amendment · no measured precedent

Business reading: A = "refresh the page" stays a user chore; B = the page catches up when you look at it; C = the page is live, like a shared document.

Recommendation

C as the end state, delivered B first. Two years out Studio and an agent edit the same app concurrently, and every mainstream collaborative builder shows other writers' changes live (Figma, Notion, Retool multiplayer; Salesforce setup at least warns on stale metadata). ADR-0008 already describes the change-log subscription this needs; only the dev stream exists. B is the timing step, not a different letter: it fixes today's confusion in one PR and stays useful as the fallback when a push connection drops. Not A: it leaves the agent/UI hand-off broken, which is exactly the North Star's "both kinds of user" road.
Confidence gap: the realtime service's availability and fan-out cost per tenant for draft-level events are unmeasured; C should be scoped to the package the tab is showing.
Self-check: only ① picks C (B as its first phase); ②③④ do not flip it — ② and ④ only set the B-first timing.

After the ruling

Phase 1 (B): usePendingDrafts refetches on focus/visibility and polls while visible; Studio's item draft marks and the package metadata resolver listen to the same pulse; drop the "only after a reload" clause from studio-authoring.draft-publish-lifecycle. e2e: stage a draft via REST/MCP, switch back to an open Studio tab, the badge updates without reload.
Phase 2 (C): draft/publish mutations published on service-realtime; the console subscribes once per tab and pulses emitMetadataRefresh; ADR-0008 amended.

os-decision-facets
① 项目长远合理性: C 落地 ADR-0008 已描述的变更订阅,让 UI 与 agent 共编同一应用时状态一致;B 是其分期第一步与断线兜底,A 让两类用户之间的交接长期断开。
② 实际业务拉动: 今天每个用 MCP 搭应用、同时开着 Studio 的用户都会撞上(本次会话实测),且 QA 清单已把"外部发布需刷新"记成预期行为。
③ 防 AI 犯错: 看不到变更时,用户会以为 AI 没改成而让它重做,导致重复/冲突修改;B/C 让结果可见,错误更容易被及时发现。
④ 创业阶段不扩散: B 只改一个 hook、无新契约;C 新增 realtime 事件需维护,故按 B → C 分期。
Prior rulings read: "Studio drafts reload / Studio changes refresh" → 12 hits (#32440, #25943, #29672 — none rule on cross-tab/external refresh); ADR-0008, ADR-0037; thread: none

Reader who acts: the ui/ app-shell seat (phase 1) and the metadata/realtime server seat (phase 2), after the ruling.
Dedupe: repo:objectstack-ai/objectstack is:issue Studio changes refresh → 6 hits, Studio drafts reload → 6 hits, none on external-change refresh.


Generated by Claude Code

Activity

  1. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:uiDomain lane — seat card indexed by label:pm:seat
    on Nov 12, 2026
  2. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    分诊首触 + 决策分析(fable 档独立评估)· 2026-11-12T16:53Z

    维护者速读

    • agent 经 MCP 改完后,已打开的 Studio 页面毫无变化,用户以为没改成,会让 agent 重做。
    • B(页面重新可见时重取,可见期低频轮询)是一个 ui PR,仓内有现成规则可循(hiddenTabPollStop.ts)。
    • 真正需要您裁的只有 C:把草稿事件推上实时通道,这是新的服务端契约,也涉及谁能看到草稿。
    • 请裁:只做 B(C 暂缓)/ B 后接着做 C。推荐只做 B。

    是否已有裁决: 未找到(ADR-0008 描述了 subscribe,但只建了开发用的流)。

    前提核实:

    • ui/packages/app-shell/src/preview/usePendingDrafts.ts:248 只订阅本标签页内的刷新。
    • 草稿不走集群扇出。
    • QA 清单 studio-authoring.json:511 把「外部改动需刷新才可见」记成了预期行为。
    选项 客户看到 工作量
    A 不做 需手动刷新 0
    B 可见时重取 + 低频轮询 切回 Studio 即见草稿角标 ui · 1 个 PR;先例 hiddenTabPollStop.ts
    C 草稿事件上实时通道 所有标签页实时更新 spec + metadata-protocol + ui,3 个以上 PR;须修订 ADR-0008

    四轴:

    • ① 长远:终态是推送;B 届时仍是断线兜底,不白做。
    • ② 拉动:实测一次,QA 清单也记了这个缺口。
    • ③ 防 AI:用户能看到结果,就不会让 agent 重复修改。
    • ④ 不扩散:B 无新契约,且必须遵守 hiddenTabPollStop(轮询会让缩容到零的后端一直醒着);C 新增事件契约,暂缓。

    推荐 B。 置信缺口:轮询间隔对缩容计费的影响没有量。自检: 只看①选 C(B 是第一步);②③④翻转为只做 B。人工底线项:仅 C。

    os-decision-facets

    • ① 长远:终态是推送(ADR-0008 已描述),B 是其兜底
    • ② 拉动:实测一次;QA 清单已把缺口记成预期
    • ③ 防 AI:结果可见后,用户不会让 agent 重复修改
    • ④ 不扩散:B 无新契约,须遵守 hiddenTabPollStop;C 新增事件契约,暂缓

    Prior rulings read: ADR-0008 (subscribe, dev stream only); thread: none


    Generated by Claude Code

  3. added
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    on Nov 12, 2026
  4. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Brief-claimed-by: session_01Q4EjjMnyLqJdzuktEmRPXH · batch #1 · 2026-11-13T04:20Z

    Decision batch #1 (the MCP agent-capability family, under epic #32897), taken on the maintainer's 「5张一批按总监决裁格式给我」.

  5. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Ruling recorded · #32592 · C in two phases; phase 1 = B now · 2026-11-13T04:40Z

    Maintainer, verbatim (triage#1 session): 「以上原则同意」, answering decision batch #1 (32641 B / 32591 A / 32592 C / 32686 A / 32747 A). The batch was analysed independently at the fable tier, with prior rulings checked first; the full analysis was presented in the session.

    • Phase 1 (now, domain:ui, one PR): refetch when the Studio tab becomes visible again, plus a low-frequency poll while visible, under the existing hiddenTabPollStop rule. The item draft marks and the package metadata resolver listen to the same pulse. Remove the platform-checklist line that expects external changes only after a reload (studio-authoring.json). Acceptance: stage a draft via REST or MCP, switch back to an open Studio, and the badge updates without a reload.
    • Phase 2 (filed now as its own pm:blocked card for domain:spec): draft and publish events on the live change channel (ADR-0008). It does not start until three things are done: the scale-to-zero cost of persistent connections is measured, a rule says who may receive another user's draft events, and ADR-0008 is amended.
    • Execution: needs-user-decision → pm:queue.

    Generated by Claude Code

  6. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 13, 2026
  7. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-tesla · session_01L9qibvZYR36KAJUv44oXZw

    Claim: PM loop round 6 · 2026-11-13T22:31Z
    Session: session_01L9qibvZYR36KAJUv44oXZw (seat); card thread session_01L9qibwsmJho8tS1SLKTiPa is the dev
    Account: os-tesla
    Branch: claude/project-thread-wv46ii (the card thread's designated branch; the platform lets a thread push only that branch, so not claude/issue-32592-*)
    Worktree: objectstack-issue-32592
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: ui/packages/app-shell/src/preview/usePendingDrafts.ts, preview/draftStatus.ts, views/studio-design/StudioDesignSurface.tsx (the draft-marks pulse only), the package metadata resolver behind Studio's item draft marks, hooks/hiddenTabPollStop.ts (reuse), their tests, an e2e spec if added; docs/qa/platform-checklist/areas/studio-authoring.json (one clause + revision entry); root .changeset/. ⛔ No server change, no packages/spec change (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default tier, effort: high (a polling seam under the hidden-tab rule; fleet token rule 2)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: usePendingDrafts refetches only on the in-tab pulse, so a change staged outside the tab shows only after a reload (objectstack ui/ Studio) | maintainer ruling 6844196863: C in two phases, phase 1 = B now (phase 2 is #33023) | every user with Studio open while an agent edits over MCP or REST
    Thread-read: 6844196863
    Premise: holds at origin/main 84c66d36: usePendingDrafts.ts:248 subscribes only to subscribeMetadataRefresh; no poll or visibility refetch; studio-authoring.json:511 records 'shows in Studio only after a reload'
    Serial constraints cleared: none of the 24 open PRs touches this surface (seat read 2026-11-13 ~22:30Z). Sibling seat cards in flight: #33098, #33073, #33052, #32592, #32904, with disjoint surfaces; #33052 may edit another region of StudioDesignSurface.tsx (allowed; the second to land merges main once).

  8. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 13, 2026
  9. changed the title [-][Decision] Should Studio show changes staged or published from outside the browser tab (MCP, REST, another tab) without a manual reload — and by polling or by a server push?[/-] [+]studio: refetch drafts on focus and poll while the tab is visible, so changes made outside the tab show without a reload — ruled C, phase 1 (phase 2 is #33023)[/+] on Nov 13, 2026
  10. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-tesla · session_01L9qibwsmJho8tS1SLKTiPa
    os-dev-report

    {
      "issue": 32592,
      "status": "done",
      "branch": "claude/project-thread-wv46ii",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/33165",
      "session": "session_01L9qibwsmJho8tS1SLKTiPa",
      "premise_still_valid": true,
      "summary": "Phase 1 (B) of ruling 6844196863. usePendingDrafts keeps a ref-counted set of the _drafts scopes its mounted hooks read and checks them on visibilitychange-to-visible, on window focus, and every PENDING_DRAFTS_POLL_MS = 30 s while visible (top of the ruled 15-30 s range: the return read is immediate, the timer only covers a side-by-side visible Studio). A check reads each scope once through the existing in-flight share; when the answer's fingerprint differs from the one this tab last read it sends the tab's own emitMetadataRefresh, so the badge, the item header line, the pillars' re-reads that carry the draft marks, the package app resolver and MetadataProvider converge on one pulse; an unchanged answer sends nothing. A hidden tab clears the timer and sends no request (stricter than hiddenTabPollStop, which is consulted at arm and fire). The checklist clause expecting an outside publish to show only after a reload is replaced, revision 13. No StudioDesignSurface.tsx, server or spec change.",
      "tests": "head 36f74ea0 (code identical to 922b9a62, where the suites ran). Under os-verify-lock from ui/: new pins + usePendingDrafts afterSave/inflightShare + itemDraftLine harness: Test Files 4 passed (4), Tests 31 passed (31). All 244 ui test files importing the changed surface: 66 visibility/timer/ledger-sensitive files Test Files 66 passed (66), Tests 399 passed (399); remaining 178 files Test Files 178 passed (178), Tests 1251 passed (1251). app-shell type-check (both programs) VERDICT command-exit 0. Ablation at 922b9a62 via scripts/ablation-replace.mjs: anchor document.addEventListener('visibilitychange', onWatchVisibility) replaced, anchor x1 to x0, blob afced872 to d8403f21; Test Files 2 failed (2), Tests 4 failed | 18 passed (22) (return, unchanged-answer, hidden-then-return, Studio item-line pins red; focus and poll pins green as expected); restore blob == HEAD afced872, git diff HEAD empty. No dist build involved (app-shell tests import source).",
      "gates": "dispatch-gates --commands derived 43; all run with stdin /dev/null, exit captured before any pipe; --ran: 43 derived accounted for, 43 run, 0 NOT-MEASURED (a DERIVED zero). check:platform-checklist first exit 1 (short anchor path), fixed, exit 0; check:type-check-debt first exit 124 (300 s wrapper timeout queued on the lock), re-run exit 0. Derivation byte-identical after the main merge; on 36f74ea0 re-ran exit 0: check:platform-checklist, check-sdui-manifest, check:nul-bytes, check:doc-authoring, check-empty-changeset, check-changeset-no-major, check:test-source-alias, check:cross-package-test-inputs. ui check:new-line-citations: 0 new. CI families: read from PR 33165 check-runs (in_progress at report time).",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "2 — mcp__github__issue_read (get, get_comments), reads only; 0 write tools",
      "api_writes": "2 — fleet-write relay POST /repos/objectstack-ai/objectstack/dispatches (pr_create, run 43526531817, PR 33165 with assignee os-tesla); post-stamped comment on issue 32592 (this report). Plus git push (not REST). One REST read of the claim comment.",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: none · e2e not added: a Playwright page cannot background itself as a tab does, so the browser run would fake document.hidden as the jsdom pins already do; noted in Acceptance notes",
        "carrier: none · dispatch order put the verification record before Acceptance notes, os-dev.md orders What changes/Tests/Acceptance notes/Verification; the body follows os-dev.md with Tests (the verification record) ahead of Acceptance notes"
      ]
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:uiDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions