Task tab recovery after a reload
刷新后任务标签页的恢复
The right Sidebar persists a Session's layout as tab records and not the navigation parameters an opener passed, per the [tab type and navigation decision](../architecture/2026-09-05-sidebar-tab-types-and-navigation.md). A `scheduleTask` tab restored by a reload therefore had no task identity: its body could show only its loading feedback and its chip only the constant label, until the user opened the task again from
English
Problem
The right Sidebar persists a Session's layout as tab records and not the navigation parameters an opener passed, per the tab type and navigation decision. A scheduleTask tab restored by a reload therefore had no task identity: its body could show only its loading feedback and its chip only the constant label, until the user opened the task again from the Session entry. Recovering content belongs to the tab type's provider, as the layout persistence decision established for terminals, and the Sidebar exposes no parameter store for a page type to reuse.
Decision
The task tab page keeps a durable binding to the task it last showed and resolves a restored record from it.
The binding key is the Sidebar Session plus the layout record's own id. dsh.sidebar-right.v1.<sessionId> persists the tab records and the counter that mints their ids, so a record keeps its id across a reload and no record minted later in that layout reuses it. The record's contentId is not a usable key: every tab of one page kind is recorded under the same sidebar://<kind> address, and page deduplication is per pane, so two panes may each hold a task tab. Each entry also stores the kind and contentId of the record it was written for, and a read drops and ignores an entry whose pair differs from the record reading it.
One localStorage document per Session, keyed dsh.schedule.task-tab.v1.<sessionId>, holds one entry per tab id, mapping that page to the Session and id of its last task. No task content is stored. A write keeps only the entries whose tab the Session's committed layout still holds, read through ctx.sidebarRight.tabsIn(sessionId); a Session whose layout is not adopted leaves its entries alone. An entry that a read succeeding after the tab appeared cannot resolve is removed, which leaves the tab with the unbound-tab notice, the same state as one that never had a binding.
The body writes the binding in an effect whenever a navigation carries this type's parameters, and reads it only while the record has no navigation parameters; parameters of another kind leave the record in its missing-task state. A recovered identity then follows the existing rules: the detail whenever the retained records hold the task, the catalog feedback until a read requested after the tab's baseline settles without the task, and the query-failure state with its retry when the read fails. A binding such a read cannot resolve shows the unbound-tab notice instead of the loading feedback, and never the missing-task notice, because that notice states that an opener named a task.
The body measures the answer from the catalog read request ordinal it observed for its current navigation, and asks for a read whenever the retained records leave it without a task and no read requested after that ordinal has succeeded, reporting the catalog feedback while that read is pending. The Sidebar navigates a tab again without remounting its body, so the navigation's revision moves the baseline; a read requested before the baseline never states that the tab's task is gone.
Alternatives considered
Key the binding by contentId. Rejected: every tab of one page kind is recorded under the same address, and page deduplication is per pane, so two task tabs in two panes would share one binding and restore the same task.
Persist navigation parameters in the Sidebar layout. Rejected: it moves content parameters into the layout snapshot that the navigation decision keeps transient, and every future page type would inherit the format.
Declare the kind multiple: true so every open mints a unique contentId. Rejected: it changes what opening a task does — a second task opens a second tab instead of showing in the existing one — to obtain a key, and it cannot recover records restored before the change.
Resolve the task from the catalog. Rejected: the catalog holds no per-tab association, and the Session entry the tab was opened from is gone after a reload.
Reuse the client store's localStorage persistence. Rejected: createSnapshotStore's persist option saves one whole store value under one name and rehydrates it when that store is created, so it needs a store per Session; the binding also has to reject reused ids and prune closed tabs on write, which a keyed document does in one place with no subscription for a body to re-render through.
Store the task title beside the binding to confirm the recovery. Rejected: a title is task content, it changes with an edit, and the settled catalog read already decides whether the bound id resolves.
Consequences
A reloaded Session restores every task tab's detail and chip without reopening the task. The binding is window-local browser state: another browser, a cleared origin, or a tab the layout kit duplicated has none, and such a record states that the tab has no task instead of claiming the task is gone. A discarded layout snapshot can mint a tab id that a binding still names; the kind and contentId guard rejects an entry of another kind, and an entry of the same kind is overwritten by the next navigation of that tab or removed once its task stops resolving.
Entries of Sessions whose layouts are never adopted again remain until their next write; write-time pruning bounds every adopted Session, and no entry holds content.
Testing
Unit tests cover writing on navigation, reading only without parameters, recovery into the detail and the chip, degradation of an unresolvable binding, committed-tab pruning, reused-id rejection, malformed stored documents, and storage that is absent or rejects writes.
中文
Problem
右 Sidebar 按 标签类型与导航决策 只把 Session 布局持久化为标签记录,不持久化打开方传入的导航参数。因此刷新恢复出的 scheduleTask 标签没有任务身份:正文只能显示加载反馈,chip 只能显示固定文案,直到用户从 Session 入口重新打开该任务。按 布局持久化决策 为终端确立的划分,恢复内容属于标签类型自身 provider 的职责,而 Sidebar 没有可供页面类型复用的参数存储。
Decision
任务标签页保存到它最后显示任务的持久关联,并据此恢复刷新出的记录。
关联键是 Sidebar Session 加布局记录自身的 id。dsh.sidebar-right.v1.<sessionId> 持久化标签记录以及铸造其 id 的计数器,因此记录刷新后保持 id,该布局之后铸造的记录也不会复用它。记录的 contentId 不能作为键:同一种页面类型的所有标签都记录在同一个 sidebar://<kind> 地址下,而页面去重按 pane 进行,两个 pane 可以各有一个任务标签。每条记录还保存写入时该记录的 kind 和 contentId;读取时若这一对与读取它的记录不一致,就删除该条并视为无关联。
每个 Session 一个 localStorage 文档,键为 dsh.schedule.task-tab.v1.<sessionId>,按 tab id 保存一条记录,把该页面映射到其最后任务的 Session 和 id。不保存任何任务内容。写入时只保留 Session 已提交布局仍然持有的标签对应的记录,该布局通过 ctx.sidebarRight.tabsIn(sessionId) 读取;布局未被采用的 Session 保持其记录不变。标签页出现之后成功的一次读取无法解析的记录会被删除,使该标签显示未关联页签提示,与从未有关联的标签相同。
只要一次导航携带本类型的参数,正文就在 effect 中写入该关联;只有记录完全没有导航参数时才读取它,其他类型的参数让记录停留在任务缺失状态。恢复出的身份随后遵循既有的渲染规则:保留的记录包含该任务时即显示详情,判据之后请求的读取仍未给出该任务时显示目录反馈,读取失败时显示带重试的查询失败状态。这样的读取仍无法解析的关联显示未关联页签提示,而不是加载反馈,也绝不产生任务缺失提示,因为该提示的含义是打开方指名了一个任务。
正文以它为当前导航观察到的目录读取请求序号作为判据,并在保留的记录使标签页没有任务、且该序号之后没有读取成功时补发一次读取,该读取未落地期间显示目录反馈。Sidebar 会在不重新挂载正文的情况下对同一标签再次导航,因此以导航的 revision 移动判据;早于判据发出的读取永远不能断言该标签的任务已消失。
Alternatives considered
用 contentId 作为关联键。 否决:同一种页面类型的所有标签都记录在同一地址下,且页面去重按 pane 进行,两个 pane 的任务标签会共用一个关联并恢复出同一个任务。
把导航参数持久化进 Sidebar 布局。 否决:这会把内容参数放进 导航决策 保持为临时的布局快照,且之后每种页面类型都会继承该格式。
把 kind 声明为 multiple: true,让每次打开都铸造唯一 contentId。 否决:为了取得一个键而改变打开任务的行为——打开第二个任务会新开一个标签,而不是在已有标签中显示;而且它无法恢复改动之前已存在的记录。
从目录解析任务。 否决:目录不保存按标签的关联,而打开该标签的 Session 入口在刷新后已不存在。
复用 client store 的 localStorage 持久化。 否决:createSnapshotStore 的 persist 选项把一个 store 的整体值保存在一个键下,并在该 store 创建时回填,因此需要按 Session 各建一个 store;而该关联还必须拒绝复用的 id 并在写入时清理已关闭标签,这些由一份按键文档在一处完成,也不需要正文为其订阅重渲染。
在关联旁保存任务标题以确认恢复。 否决:标题属于任务内容,会随编辑变化,而已定论的目录读取本身就能判断绑定的 id 是否可解析。
Consequences
刷新后的 Session 无需重新打开任务即可恢复每个任务标签的详情和 chip。该关联是窗口内的浏览器状态:其他浏览器、被清除的站点数据,或被布局工具包复制出的标签都没有它,这类记录会说明该页签未关联任务,而不会声称任务已不存在。被丢弃的布局快照可能铸造出关联仍然指名的 tab id;kind 与 contentId 守卫会拒绝其他类型的记录,而同类型记录会因该标签的下一次导航被覆盖,或在任务不再可解析时被删除。
布局不再被采用的 Session 的记录会保留到其下次写入;写入时的清理使每个已采用的 Session 有界,且任何记录都不含内容。
Testing
单元测试覆盖:导航时写入、仅在无参数时读取、详情与 chip 的恢复、无法解析关联的降级、已提交标签的清理、复用 id 的拒绝、损坏的存储文档,以及存储缺失或拒绝写入的情况。