@deepseek-ai/dsh-client-ui-plan 0.1.6-alpha.1 → 0.1.7-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-plan/README.md
5
- README.md: 4ff275834ec069315580c205b8fd0bf0dee9cfeb
6
- README.zh.md: 588698b273bc52b1e601c7010516c24a9248e543
5
+ README.md: e9e38251ff69ed601e486b4c1ce351b236e7bcb7
6
+ README.zh.md: 91fe232d27007fea8264e98c895ca2788585710c
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- This package renders the plan-mode status chip in the Web GUI: when the host-computed projection's effective target is plan mode, the composer shows a warn-colored "Plan ×" button that turns plan mode off; otherwise the seat stays empty. Plan mode itself — the `/plan` command, the committed `plan/mode` state, the projection unit, and the policy section — belongs to `dsh-plan-mode`; this package only renders the projection and sends what a user could equally type. The model exits plan mode through the stable `exit_plan_mode` tool; its plan review uses the composed Web question channel.
12
+ Plan mode lets you review a plan before implementation. Enter with `/plan` and leave with the composer chip. Submitted plans open automatically in the right sidebar for review and remain available from cards in the completed Turn’s final artifact area after approval, rejection, or dismissal. Reopening a plan focuses its existing tab, and browser reload restores the document from Session history.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -29,7 +29,13 @@ Mount this plugin alongside `ui-conversation` and `dsh-plan-mode`; the chip then
29
29
 
30
30
  ### What the chip shows
31
31
 
32
- While the effective target is plan mode, the seat renders the warn-colored "Plan ×" status button, which executes `/plan off`. Otherwise the seat stays empty: a host without plan mode, or a Draft with no session, shows nothing. While plan mode is the effective target, the composer textarea's placeholder switches to the plan-task hint — "describe your task to generate plan" — unless the owning surface supplies its own placeholder.
32
+ While the effective target is plan mode, the seat renders the blue "Plan" status button — the plan glyph ahead of the label, swapped for a circled cross while the enabled button is hovered or keyboard-focused — which executes `/plan off`. Otherwise the seat stays empty: a host without plan mode, or a Draft with no session, shows nothing. While plan mode is the effective target, the composer textarea's placeholder switches to the plan-task hint — "describe your task to generate plan" — unless the owning surface supplies its own placeholder.
33
+
34
+ ### Reading submitted plans
35
+
36
+ When a Turn ends, each submitted plan appears in its final artifact area, using the file-delivery card treatment with a Markdown icon, title, and Open action. A pending plan opens automatically once per submission in the current browser session. Closing it stays effective across review remounts; a new submission opens its own plan. Historical cards open only when clicked. Use the card or review strip’s View full plan link to read the complete Markdown. Plan tabs show a plain text file icon. Different submissions retain separate tabs; the review buttons alone decide whether implementation may begin.
37
+
38
+ A review without a logged invocation also opens automatically. Its complete text lives only in the tab’s navigation memory, and the pending review card can reopen it. Reloading the page loses that text; an expired preview directs the user back to a pending review. Plans opened from an embedded child conversation use the visible sidebar while retaining the child’s address for document reads.
33
39
 
34
40
  ### Failures
35
41
 
@@ -43,7 +49,11 @@ Admission failures (`matched: false`, business errors, transport faults) surface
43
49
  <details>
44
50
  <summary>Implementation internals — click to expand</summary>
45
51
 
46
- The chip occupies the conversation-declared `conversation.input.plan` single seat; the node half is an empty apply (the roster row). Reads ride the generic projection pair through the standard-kit `useProjection`: the effective target is `pending ? !active : active` — a folded host value, not client optimism, so an arriving frame corrects the chip either way. The seat's injected face carries one verb, `exitPlanMode`, which executes `/plan off` through `ctx.remote.commands.execute` and maps admission failures to an inline error line. The placeholder and hint text live in ui-conversation's `conversation` locale namespace and are shared verbatim with the claimed `/plan` command hint. The accessible description is "Plan mode on, press to turn off".
52
+ The chip occupies the conversation-declared `conversation.input.plan` single seat; the node half is an empty apply (the roster row). Reads ride the generic projection pair through the standard-kit `useProjection`: the effective target is `pending ? !active : active` — a folded host value, not client optimism, so an arriving frame corrects the chip either way. The seat's injected face carries one verb, `exitPlanMode`, which executes `/plan off` through `ctx.remote.commands.execute` and maps admission failures to an inline error line. The placeholder and hint text live in ui-conversation's `conversation` locale namespace and are shared verbatim with the claimed `/plan` command hint.
53
+
54
+ Plan cards derive from native `tool/call` or PTC dispatch arguments through a Conversation Definition, with each invocation’s resolved Turn location. They contribute to the additive `conversation.chat.turnTail` list alongside file deliveries. The plan resource address identifies the invocation and its complete ordinary or direct-parent subagent Session address; its provider reads existing Session history, including older pages, without storing document text in sidebar layout. The question plugin owns the review action slot and supplies its request key, complete text, and optional invocation identity. The automatic open reads `ctx.sidebarRight.mounted` through a bound hook and runs once a seat is on screen: a review arriving while a global panel is active mounts ahead of the returning seat in the same commit, before that seat binds. The [decision](../../../.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.md) explains why review lifetime and document lifetime remain separate. Subagent plan addresses also preserve unknown mode so history reads can resolve the child descriptor.
55
+
56
+ The framework-bound `usePlans(turn)` exposes only submitted-plan data for that Turn. Chat indexes membership on Node updates and orders this collection when read; card rendering neither scans the transcript nor subscribes to other Turns or Node kinds.
47
57
 
48
58
  </details>
49
59
 
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- 本包在 Web GUI 中渲染 plan 模式状态徽章:当宿主计算的投影有效目标为 plan 模式时,composer 显示一个 warn 色「Plan ×」按钮,可关闭 plan 模式;否则该座位保持为空。plan 模式本身——`/plan` 命令、已提交的 `plan/mode` 状态、投影单元与 policy 段——归 `dsh-plan-mode` 所有;本包只渲染投影并发送用户同样可以手敲的内容。模型经稳定的 `exit_plan_mode` 工具退出 plan 模式;其 plan 评审走已组合的 Web question 通道。
12
+ 计划模式让你在实施前审阅计划。通过 `/plan` 进入,通过编辑器中的状态按钮退出。提交的计划自动在右侧边栏打开供审阅;批准、拒绝或关闭审批后,仍可通过已完成回合末尾的产物卡片查看。重新打开同一计划会聚焦已有标签页,刷新浏览器后会从会话历史恢复正文。
13
13
 
14
14
  ## 目录
15
15
 
@@ -25,11 +25,17 @@ kind: "package-reference"
25
25
  <a id="use-this-package"></a>
26
26
  ## 使用本包
27
27
 
28
- 与 `ui-conversation` 及 `dsh-plan-mode` 一起挂载本插件;plan 模式激活时,徽章随即占据 composer 的 plan 座位(访问模式控件右侧)。经 `/plan` 命令路径进入 plan 模式——从 composer 的 `+` Command 菜单选择 Plan,或键入 `/plan`——再用徽章将其关闭。
28
+ 与 `ui-conversation` 及 `dsh-plan-mode` 一起挂载本插件;plan 模式激活时,徽章随即占据 composer 的 plan 座位(访问模式控件右侧)。经 `/plan` 命令路径进入 plan 模式——从 composer 的 `+` Command 菜单选择「计划」,或键入 `/plan`——再用徽章将其关闭。
29
29
 
30
30
  ### 徽章显示什么
31
31
 
32
- 当有效目标为 plan 模式时,该座位渲染 warn 色「Plan ×」状态按钮,执行 `/plan off`。否则座位保持为空:未组合 plan-mode 的宿主,或尚无会话的 Draft,都不显示任何内容。plan 模式为有效目标期间,composer 文本框的 placeholder 切换为 plan 任务提示——「describe your task to generate plan」——除非所属 surface 提供自己的 placeholder。
32
+ 当有效目标为 plan 模式时,该座位渲染蓝色「计划」状态按钮——标签前是计划图标,按钮可用且被悬停或键盘聚焦时换成圆形 ×——点击执行 `/plan off`。否则座位保持为空:未组合 plan-mode 的宿主,或尚无会话的 Draft,都不显示任何内容。plan 模式为有效目标期间,composer 文本框的 placeholder 切换为 plan 任务提示——「describe your task to generate plan」——除非所属 surface 提供自己的 placeholder。
33
+
34
+ ### 查看已提交的计划
35
+
36
+ 回合结束后,每次提交的计划都会显示在该回合末尾的产物区域,并采用文件产物卡片的样式,包含 Markdown 图标、标题和“打开”操作。在当前浏览器会话中,每份待审计划自动打开一次;关闭后,重新挂载审批组件不会再次展开,新提交会打开其对应计划。历史卡片仅在点击后打开。通过卡片或审批条带上的“查看全文”链接可阅读完整 Markdown。计划标签页显示纯文本文件图标。不同提交保留独立标签页;是否开始实施仍由审批按钮决定。
37
+
38
+ 没有已记录调用标识的审批也会自动打开。完整正文只保存在标签页的导航内存中,待处理审批卡片可以重新打开它。刷新页面会丢失该正文;失效的预览会提示用户返回待处理审批。在嵌入式子对话中打开计划时,预览使用当前可见的侧栏,读取文档仍保留子会话地址。
33
39
 
34
40
  ### 失败
35
41
 
@@ -43,7 +49,11 @@ kind: "package-reference"
43
49
  <details>
44
50
  <summary>实现细节——点击展开</summary>
45
51
 
46
- 徽章占据 conversation 声明的 `conversation.input.plan` 单实例座位;node 半部是空 apply(roster 行)。读取经 standard-kit 的 `useProjection` 走通用投影对:有效目标是 `pending ? !active : active`——折叠的宿主值而非客户端乐观态,因此到达的帧无论哪个方向都会纠正徽章。座位注入面携带一个动词 `exitPlanMode`,经 `ctx.remote.commands.execute` 执行 `/plan off`,并把准入失败映射为一行内联错误。placeholder 与提示文案位于 ui-conversation 的 `conversation` locale 命名空间,与已认领 `/plan` 命令的提示逐字共用。无障碍描述是「Plan mode on, press to turn off」。
52
+ 徽章占据 conversation 声明的 `conversation.input.plan` 单实例座位;node 半部是空 apply(roster 行)。读取经 standard-kit 的 `useProjection` 走通用投影对:有效目标是 `pending ? !active : active`——折叠的宿主值而非客户端乐观态,因此到达的帧无论哪个方向都会纠正徽章。座位注入面携带一个动词 `exitPlanMode`,经 `ctx.remote.commands.execute` 执行 `/plan off`,并把准入失败映射为一行内联错误。placeholder 与提示文案位于 ui-conversation 的 `conversation` locale 命名空间,与已认领 `/plan` 命令的提示逐字共用。
53
+
54
+ 计划卡片通过 Conversation Definition 从原生 `tool/call` 或 PTC dispatch 参数派生,并使用每次调用已解析的回合位置。它们与文件产物一起贡献到可追加的 `conversation.chat.turnTail` 列表。计划资源地址标识调用及完整的普通会话地址或子会话直接父级地址;provider 读取已有会话历史及较早分页,不把正文存入侧边栏布局。提问插件拥有审批操作插槽,并提供请求键、完整正文和可选的调用标识。自动打开通过绑定的 hook 读取 `ctx.sidebarRight.mounted`,在座位出现在屏幕上后才执行:全局面板打开期间到达的审批,会在返回时与座位在同一次提交里先挂载、早于座位完成绑定。[决策记录](../../../.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.zh.md)说明审批与文档为何保持独立生命周期。 子代理计划地址也保留未知模式,以便历史读取解析子 descriptor。
55
+
56
+ 框架绑定的 `usePlans(turn)` 只提供该回合的已提交计划数据。Chat 随节点更新维护成员索引,并在读取时排列该集合;卡片渲染不扫描整段对话,也不订阅其他回合或节点类型。
47
57
 
48
58
  </details>
49
59
 
package/lib/client.js CHANGED
@@ -4,11 +4,474 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
7
8
  let react_jsx_runtime = require("react/jsx-runtime");
8
9
  let react = require("react");
9
- let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
+ require("@deepseek-ai/cordis");
11
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
12
+ //#region ../../util/crypto/src/index.ts
13
+ /**
14
+ * Random v4 UUID, minted from `crypto.getRandomValues`.
15
+ * @returns the UUID string.
16
+ */
17
+ function randomUUID() {
18
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
19
+ const hex = Array.from(bytes, (byte, index) => {
20
+ return (index === 6 ? byte & 15 | 64 : index === 8 ? byte & 63 | 128 : byte).toString(16).padStart(2, "0");
21
+ }).join("");
22
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
23
+ }
24
+ //#endregion
25
+ //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-plan/src/client/PlanPreview.module.css.mjs
26
+ const css$1 = ".k74WwW_cards{flex-direction:column;gap:10px;display:flex}.k74WwW_card{--plan-card-fill:var(--dsw-static-neutral-50);--plan-card-hover:var(--dsw-static-neutral-100);box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l1);background:var(--plan-card-fill);width:100%;min-width:0;height:60px;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;border-radius:18px;align-items:center;gap:10px;margin:0;padding:8px 10px;transition:background-color .12s;display:flex}body[data-ds-dark-theme] .k74WwW_card{--plan-card-fill:var(--dsw-static-neutral-850);--plan-card-hover:var(--dsw-static-neutral-800)}.k74WwW_card:hover{background:var(--plan-card-hover)}.k74WwW_card:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:3px}.k74WwW_cardIcon{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l1);background:var(--plan-card-fill);border-radius:10px;flex:none;place-items:center;width:40px;height:40px;display:grid}.k74WwW_cardDetails{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.k74WwW_cardTitle{text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;line-height:20px;overflow:hidden}.k74WwW_cardDescription{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:16px;overflow:hidden}.k74WwW_cardOpen{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-button-floating-fill);border-radius:10px;flex:none;align-items:center;height:28px;padding:4px 8px;font-size:12px;line-height:18px;display:inline-flex}.k74WwW_reviewLink{color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border:0;align-items:center;gap:4px;padding:0;display:inline-flex}.k74WwW_reviewLink:hover{color:var(--dsw-alias-label-primary)}.k74WwW_reviewLink:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:3px}.k74WwW_preview{box-sizing:border-box;height:100%;padding:20px 24px 40px;position:relative;overflow:auto}.k74WwW_document{color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;font-size:14px;line-height:1.75}.k74WwW_message{color:var(--dsw-alias-label-secondary);padding:24px;font-size:14px}.k74WwW_titleIcon{flex:none}@media (width<=767px){.k74WwW_preview{padding:16px 18px 32px}}";
27
+ const tagId$1 = "@deepseek-ai/dsh-client-ui-plan/PlanPreview.module.css";
28
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
29
+ const tag = document.createElement("style");
30
+ tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-plan";
31
+ tag.dataset.pluginCss = tagId$1;
32
+ tag.textContent = css$1;
33
+ document.head.appendChild(tag);
34
+ }
35
+ var PlanPreview_module_css_default = {
36
+ "card": "k74WwW_card",
37
+ "cardDescription": "k74WwW_cardDescription",
38
+ "cardDetails": "k74WwW_cardDetails",
39
+ "cardIcon": "k74WwW_cardIcon",
40
+ "cardOpen": "k74WwW_cardOpen",
41
+ "cardTitle": "k74WwW_cardTitle",
42
+ "cards": "k74WwW_cards",
43
+ "document": "k74WwW_document",
44
+ "message": "k74WwW_message",
45
+ "preview": "k74WwW_preview",
46
+ "reviewLink": "k74WwW_reviewLink",
47
+ "titleIcon": "k74WwW_titleIcon"
48
+ };
49
+ //#endregion
50
+ //#region lib/types/client/PlanCard.js
51
+ /** Persistent transcript cards and pending-review sidebar navigation. */
52
+ /**
53
+ * Render the completed Turn's submitted plans in invocation order.
54
+ * @param props - Logged plan, localized copy, and Session-bound navigation.
55
+ * @returns keyboard-accessible plan cards, or null for a Turn without plans.
56
+ */
57
+ function PlanCards({ turn, usePlans, openPlan, t }) {
58
+ const plans = usePlans(String(turn.turn));
59
+ if (plans === void 0 || plans.length === 0) return null;
60
+ return (0, react_jsx_runtime.jsx)("div", {
61
+ className: PlanPreview_module_css_default.cards,
62
+ "data-plan-artifacts": true,
63
+ children: plans.map((plan) => (0, react_jsx_runtime.jsxs)("button", {
64
+ type: "button",
65
+ className: PlanPreview_module_css_default.card,
66
+ "data-plan-card": plan.callId,
67
+ "aria-label": t("preview.openNamed", { title: plan.title }),
68
+ onClick: () => {
69
+ openPlan(plan.callId);
70
+ },
71
+ children: [
72
+ (0, react_jsx_runtime.jsx)("span", {
73
+ className: PlanPreview_module_css_default.cardIcon,
74
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.FileTypeIcon, {
75
+ kind: "markdown",
76
+ size: 20
77
+ })
78
+ }),
79
+ (0, react_jsx_runtime.jsxs)("span", {
80
+ className: PlanPreview_module_css_default.cardDetails,
81
+ children: [(0, react_jsx_runtime.jsx)("span", {
82
+ className: PlanPreview_module_css_default.cardTitle,
83
+ children: plan.title
84
+ }), (0, react_jsx_runtime.jsx)("span", {
85
+ className: PlanPreview_module_css_default.cardDescription,
86
+ children: t("preview.document")
87
+ })]
88
+ }),
89
+ (0, react_jsx_runtime.jsx)("span", {
90
+ className: PlanPreview_module_css_default.cardOpen,
91
+ children: t("preview.action")
92
+ })
93
+ ]
94
+ }, plan.callId))
95
+ });
96
+ }
97
+ /**
98
+ * Open each pending plan automatically and retain a manual opener without answering it.
99
+ *
100
+ * The automatic open waits for a mounted Sidebar seat: a review that arrives
101
+ * while the Conversation is off screen mounts in the same commit as the seat,
102
+ * ahead of it, and the seat binds from its own effect. Reading the bound
103
+ * session through the hook opens once that binding exists.
104
+ * @param props - Review identity, Session store, localized copy, and navigation.
105
+ * @returns an opener for either logged or temporary plan text.
106
+ */
107
+ function PlanReviewOpen({ review, requestKey, openReview, useSidebarMounted, t, useStore, actions }) {
108
+ const identity = review.callId === void 0 ? `review:${requestKey}` : `call:${review.callId}`;
109
+ const opened = useStore((state) => state.opened[identity] === true);
110
+ const mounted = useSidebarMounted((session) => session !== void 0);
111
+ (0, react.useEffect)(() => {
112
+ if (opened || !mounted) return;
113
+ openReview(review, requestKey);
114
+ actions.markOpened(identity);
115
+ }, [
116
+ identity,
117
+ opened,
118
+ mounted,
119
+ openReview,
120
+ review,
121
+ requestKey,
122
+ actions
123
+ ]);
124
+ return (0, react_jsx_runtime.jsxs)("button", {
125
+ type: "button",
126
+ className: PlanPreview_module_css_default.reviewLink,
127
+ title: t("preview.open"),
128
+ "aria-label": t("preview.open"),
129
+ onClick: () => {
130
+ openReview(review, requestKey);
131
+ },
132
+ children: [t("preview.full"), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { size: 14 })]
133
+ });
134
+ }
135
+ //#endregion
136
+ //#region lib/types/client/review-preview.js
137
+ /**
138
+ * Name one temporary review within its browser lifetime and Session.
139
+ * @param sessionId - Session displaying the review.
140
+ * @param requestKey - Browser-unique pending request identity.
141
+ * @returns the address used to focus or reopen its preview.
142
+ */
143
+ function reviewPreviewAddress(sessionId, requestKey) {
144
+ return `dsh-resource://plan-review/${encodeURIComponent(sessionId)}/${encodeURIComponent(requestKey)}`;
145
+ }
146
+ /**
147
+ * Recognize temporary plan navigation without interpreting it as logged history.
148
+ * @param address - Saved or caller-supplied navigation address.
149
+ * @returns whether the address identifies a temporary review preview.
150
+ */
151
+ function isReviewPreviewAddress(address) {
152
+ return /^dsh-resource:\/\/plan-review\/[^/?#]+\/[^/?#]+$/.test(address);
153
+ }
154
+ //#endregion
155
+ //#region lib/types/client/failure-line.js
156
+ /**
157
+ * Explain a failed plan read in the current locale.
158
+ * @param t - Plan namespace translator.
159
+ * @param failure - Failure reported by the resource provider.
160
+ * @returns localized plan copy, or the external failure's diagnostic.
161
+ */
162
+ function planFailureLine(t, failure) {
163
+ switch (failure.code) {
164
+ case "plan/invalid-address": return t("preview.invalidAddress");
165
+ case "plan/unavailable": return t("preview.historyUnavailable");
166
+ case "plan/not-found": return t("preview.notFound");
167
+ default: return failure.message;
168
+ }
169
+ }
170
+ //#endregion
171
+ //#region lib/types/client/PlanPreview.js
172
+ /** Read-only Markdown viewer for logged plans and temporary review documents. */
173
+ /**
174
+ * Render the submitted plan with its complete Markdown.
175
+ * @param props - Framework-bound tab identity, resource, and copy.
176
+ * @returns the plan document or a localized loading/failure state.
177
+ */
178
+ function PlanPreview({ useTabInfo, useResource, t }) {
179
+ const tab = useTabInfo();
180
+ const resource = useResource(tab.tab.navigation.address);
181
+ const temporary = isReviewPreviewAddress(tab.tab.navigation.address);
182
+ const params = tab.tab.navigation.params;
183
+ const plan = temporary ? params !== void 0 && "planReview" in params ? params.planReview : void 0 : resource.value;
184
+ const labels = (0, react.useMemo)(() => ({
185
+ code: {
186
+ copyLabel: t("copy"),
187
+ copiedLabel: t("copied")
188
+ },
189
+ footnotes: t("markdown.footnotes")
190
+ }), [t]);
191
+ if (plan === void 0) return (0, react_jsx_runtime.jsxs)("div", {
192
+ className: PlanPreview_module_css_default.message,
193
+ role: "status",
194
+ children: [temporary ? t("preview.expired") : resource.status === "none" ? t("preview.unavailable") : resource.status === "failed" ? t("preview.failed") : t("preview.loading"), !temporary && resource.failure !== void 0 && (0, react_jsx_runtime.jsx)("p", { children: planFailureLine(t, resource.failure) })]
195
+ });
196
+ return (0, react_jsx_runtime.jsx)("section", {
197
+ className: PlanPreview_module_css_default.preview,
198
+ "data-plan-preview": "callId" in plan ? plan.callId : tab.tab.navigation.address,
199
+ "aria-label": plan.title,
200
+ children: (0, react_jsx_runtime.jsx)("div", {
201
+ className: PlanPreview_module_css_default.document,
202
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
203
+ text: plan.markdown,
204
+ labels
205
+ })
206
+ })
207
+ });
208
+ }
209
+ /**
210
+ * Display a plain file icon and the heading in its tab after resource recovery.
211
+ * @param props - Framework-bound tab identity and resource reader.
212
+ * @returns a decorative file icon followed by the recovered title or initial localized label.
213
+ */
214
+ function PlanTitle({ useTabInfo, useResource }) {
215
+ const tab = useTabInfo();
216
+ const resource = useResource(tab.tab.navigation.address);
217
+ const params = tab.tab.navigation.params;
218
+ const plan = isReviewPreviewAddress(tab.tab.navigation.address) ? params !== void 0 && "planReview" in params ? params.planReview : void 0 : resource.value;
219
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.FileTypeIcon, {
220
+ kind: "other",
221
+ size: 16,
222
+ className: PlanPreview_module_css_default.titleIcon
223
+ }), plan?.title ?? tab.tab.title] });
224
+ }
225
+ //#endregion
226
+ //#region lib/types/client/plan.js
227
+ function record(value) {
228
+ return typeof value === "object" && value !== null && !Array.isArray(value);
229
+ }
230
+ /**
231
+ * Read a complete plan from untrusted logged arguments.
232
+ * @param event - Native call or PTC dispatch event from Session history.
233
+ * @returns the submitted plan, or undefined for unrelated or malformed data.
234
+ */
235
+ function submittedPlan(event) {
236
+ if (event.type !== "tool/call" && event.type !== "tool/ptc-dispatch-start" && event.type !== "tool/ptc-dispatch") return void 0;
237
+ const data = event.data;
238
+ if (!record(data) || data.name !== "exit_plan_mode") return void 0;
239
+ const callId = event.type === "tool/call" ? data.callId : data.subCallId;
240
+ if (typeof callId !== "string" || callId === "") return void 0;
241
+ let args = data.arguments;
242
+ if (event.type === "tool/call") {
243
+ if (typeof args !== "string") return void 0;
244
+ try {
245
+ args = JSON.parse(args);
246
+ } catch (_error) {
247
+ return;
248
+ }
249
+ }
250
+ if (!record(args) || typeof args.plan !== "string") return void 0;
251
+ const markdown = args.plan;
252
+ const title = /^#\s+(\S[^\r\n]*)/.exec(markdown.trim())?.[1];
253
+ return title === void 0 ? void 0 : {
254
+ callId,
255
+ markdown,
256
+ title
257
+ };
258
+ }
259
+ /**
260
+ * Encode the durable identity of a plan without retaining its text in layout storage.
261
+ * @param target - Session and tool-call identity.
262
+ * @returns the plan resource address.
263
+ */
264
+ function planAddress(target) {
265
+ const { session, callId } = target;
266
+ return `dsh-resource://plan/${(session.kind === "session" ? [session.sessionId, callId] : [
267
+ "subagent",
268
+ session.parentSessionId,
269
+ session.childSessionId,
270
+ session.mode,
271
+ callId
272
+ ]).map(encodeURIComponent).join("/")}`;
273
+ }
274
+ /**
275
+ * Validate a saved or caller-supplied plan resource address.
276
+ * @param address - Address submitted to the sidebar or resource provider.
277
+ * @returns the decoded identity, or undefined for an unsupported address.
278
+ */
279
+ function parsePlanAddress(address) {
280
+ const match = /^dsh-resource:\/\/plan\/([^?#]+)$/.exec(address);
281
+ if (match === null) return void 0;
282
+ try {
283
+ const parts = match[1].split("/").map(decodeURIComponent);
284
+ if (parts.some((part) => part === "")) return void 0;
285
+ if (parts.length === 2) return {
286
+ session: {
287
+ kind: "session",
288
+ sessionId: parts[0]
289
+ },
290
+ callId: parts[1]
291
+ };
292
+ if (parts.length === 5 && parts[0] === "subagent" && (parts[3] === "one-shot" || parts[3] === "continuable" || parts[3] === "unknown")) return {
293
+ session: {
294
+ kind: "subagent",
295
+ parentSessionId: parts[1],
296
+ childSessionId: parts[2],
297
+ mode: parts[3]
298
+ },
299
+ callId: parts[4]
300
+ };
301
+ return;
302
+ } catch (_error) {
303
+ return;
304
+ }
305
+ }
306
+ //#endregion
307
+ //#region lib/types/client/plan-definition.js
308
+ /** One card per invocation; a later PTC settlement retains the original card position. */
309
+ const planDefinition = {
310
+ kind: "submitted-plan",
311
+ target: "chat",
312
+ match: (event) => {
313
+ const plan = submittedPlan(event);
314
+ return plan === void 0 ? null : {
315
+ id: plan.callId,
316
+ role: event.type === "tool/ptc-dispatch" ? "update" : "start"
317
+ };
318
+ },
319
+ start: (_context, match) => submittedPlan(match.event),
320
+ update: (context) => context.state,
321
+ buildViewNode: (context) => {
322
+ const start = context.start ?? context.matches[0];
323
+ const data = context.state ?? (start === void 0 ? void 0 : submittedPlan(start.event));
324
+ if (data === void 0 || start === void 0) return null;
325
+ return {
326
+ key: context.key,
327
+ kind: "submitted-plan",
328
+ id: context.id,
329
+ target: "chat",
330
+ anchorSeq: start.event.seq,
331
+ location: start.location,
332
+ visibility: "hidden",
333
+ data
334
+ };
335
+ }
336
+ };
337
+ //#endregion
338
+ //#region ../../typert/protocol/src/remote-error.ts
339
+ /**
340
+ * One Remote call failure: a real Error carrying its stable code and typed
341
+ * details. Owners throw it at the failure point; the Host Gateway encodes it
342
+ * onto the wire unchanged; the Client face rebuilds an instance for the
343
+ * `RemoteResult` error branch, so `throw result.error` keeps throw semantics.
344
+ * Discrimination is always by `code`, never by instanceof.
345
+ */
346
+ var RemoteError = class extends Error {
347
+ code;
348
+ details;
349
+ /** Structural marker: cross-realm/bundle identification never uses instanceof. */
350
+ isDSHRemoteError = true;
351
+ /**
352
+ * @param code - stable failure code declared in {@link RemoteErrorDetailsMap}.
353
+ * @param message - human diagnostic carried across the wire.
354
+ * @param details - structured payload typed by the code.
355
+ * @param options - standard Error options (`cause` survives in-process only).
356
+ */
357
+ constructor(code, message, details, options) {
358
+ super(message, options);
359
+ this.code = code;
360
+ this.details = details;
361
+ this.name = "RemoteError";
362
+ }
363
+ };
364
+ /**
365
+ * Structurally identify a RemoteError thrown across module or realm copies of
366
+ * this class. Mechanism-internal: the Gateway and test assertions use it;
367
+ * business code receives typed failures and never needs it.
368
+ * @param value - a caught value.
369
+ * @returns the failure when the marker matches, otherwise undefined.
370
+ */
371
+ function remoteErrorOf(value) {
372
+ if (typeof value === "object" && value !== null && value.isDSHRemoteError === true && typeof value.code === "string") return value;
373
+ }
374
+ //#endregion
375
+ //#region ../../typert/protocol/src/index.ts
376
+ /**
377
+ * Remote decorators and explicit Gateway bindings backed by versioned
378
+ * descriptors carried on decorated class prototypes. Strict reflection
379
+ * remains a Typert compiler responsibility.
380
+ * @module @deepseek-ai/dsh-typert-protocol
381
+ */
382
+ //#endregion
383
+ //#region lib/types/client/plan-resource.js
384
+ /**
385
+ * Bind plan reads to the generated Session Remote face.
386
+ * Opening a follow reads projections and may activate a prepared Session on the Host.
387
+ * Generated Remote streams can throw carrier failures; the provider reports failed
388
+ * reads as resource failure frames and preserves Remote error codes.
389
+ * @param remote - Existing Session history API.
390
+ * @returns a provider whose reads stop after finding the exact invocation.
391
+ */
392
+ function planResourceProvider(remote) {
393
+ return {
394
+ protocol: "plan",
395
+ async *open(address, { signal }) {
396
+ const aborted = () => signal.aborted;
397
+ if (aborted()) return;
398
+ const target = parsePlanAddress(address);
399
+ if (target === void 0) {
400
+ yield {
401
+ ok: false,
402
+ error: new RemoteError("plan/invalid-address", "Invalid plan resource address.", {})
403
+ };
404
+ return;
405
+ }
406
+ const sessionAddress = target.session;
407
+ try {
408
+ let snapshot;
409
+ for await (const frame of remote.follow({ address: sessionAddress }, signal)) if (frame.type === "snapshot") {
410
+ snapshot = frame;
411
+ break;
412
+ }
413
+ if (aborted()) return;
414
+ if (snapshot === void 0) throw new RemoteError("plan/unavailable", "Session history ended before the plan could be read.", {});
415
+ let page = {
416
+ records: snapshot.records,
417
+ hasMore: snapshot.hasMore
418
+ };
419
+ while (true) {
420
+ for (const entry of page.records) {
421
+ const plan = submittedPlan(entry.event);
422
+ if (plan?.callId === target.callId) {
423
+ yield {
424
+ ok: true,
425
+ value: plan
426
+ };
427
+ return;
428
+ }
429
+ }
430
+ const beforeSeq = page.records[0]?.event.seq;
431
+ if (!page.hasMore || beforeSeq === void 0) break;
432
+ const next = await remote.page({
433
+ address: sessionAddress,
434
+ throughSeq: snapshot.cursor,
435
+ beforeSeq
436
+ }, signal);
437
+ if (aborted()) return;
438
+ if (!next.ok) {
439
+ yield next;
440
+ return;
441
+ }
442
+ page = next.value;
443
+ }
444
+ yield {
445
+ ok: false,
446
+ error: new RemoteError("plan/not-found", "The submitted plan was not found in this Session.", {})
447
+ };
448
+ } catch (error) {
449
+ if (!aborted()) yield {
450
+ ok: false,
451
+ error: remoteErrorOf(error) ?? new RemoteError("plan/read-failed", error instanceof Error ? error.message : String(error), {})
452
+ };
453
+ }
454
+ }
455
+ };
456
+ }
457
+ //#endregion
458
+ //#region lib/types/client/review-store.js
459
+ /** Session-owned memory of pending plans already opened automatically. */
460
+ /**
461
+ * Keep manual sidebar closure effective across review component remounts.
462
+ * @returns a transient store handle whose instances belong to Session scopes.
463
+ */
464
+ function createPlanReviewStore() {
465
+ return (0, _deepseek_ai_dsh_client_store.defineStore)({
466
+ init: () => ({ opened: {} }),
467
+ actions: { markOpened: (draft, reviewKey) => {
468
+ draft.opened[reviewKey] = true;
469
+ } }
470
+ });
471
+ }
472
+ //#endregion
10
473
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-plan/src/client/PlanModeControl.module.css.mjs
11
- const css = ".rS3zOq_wrap{align-items:center;gap:6px;display:inline-flex}.rS3zOq_chip{corner-shape:round;background:var(--dsw-alias-state-warn-tertiary);min-width:34px;color:var(--dsw-alias-state-warn-label);cursor:pointer;border:none;border-radius:999px;align-items:center;gap:4px;padding:2px 8px;font-size:13px;font-weight:500;line-height:20px;display:inline-flex}.rS3zOq_chip:hover:not(:disabled){color:var(--dsw-alias-state-warn-primary)}.rS3zOq_chip:focus-visible{outline:2px solid var(--dsw-alias-state-warn-label);outline-offset:2px}.rS3zOq_chip:disabled{opacity:.6;cursor:default}.rS3zOq_close{color:currentColor;align-items:center;display:inline-flex}.rS3zOq_error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}";
474
+ const css = ".rS3zOq_wrap{align-items:center;gap:6px;display:inline-flex}.rS3zOq_chip{background:var(--dsw-alias-state-business-tertiary);min-width:34px;height:28px;color:var(--dsw-alias-state-business-primary);cursor:pointer;border:none;border-radius:14px;align-items:center;gap:4px;padding:0 8px;font-size:13px;font-weight:500;line-height:20px;display:inline-flex}.rS3zOq_chip:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-business-tertiary), var(--dsw-alias-state-business-primary) 6%)}.rS3zOq_chip:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.rS3zOq_chip:disabled{opacity:.6;cursor:default}.rS3zOq_glyph{color:currentColor;flex:none;width:14px;height:14px;display:inline-flex}.rS3zOq_hoverGlyph,.rS3zOq_chip:hover:not(:disabled) .rS3zOq_restGlyph,.rS3zOq_chip:focus-visible .rS3zOq_restGlyph{display:none}.rS3zOq_chip:hover:not(:disabled) .rS3zOq_hoverGlyph,.rS3zOq_chip:focus-visible .rS3zOq_hoverGlyph{display:block}.rS3zOq_error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}";
12
475
  const tagId = "@deepseek-ai/dsh-client-ui-plan/PlanModeControl.module.css";
13
476
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
14
477
  const tag = document.createElement("style");
@@ -19,8 +482,10 @@ window.__ModuleLoader__.load({
19
482
  }
20
483
  var PlanModeControl_module_css_default = {
21
484
  "chip": "rS3zOq_chip",
22
- "close": "rS3zOq_close",
23
485
  "error": "rS3zOq_error",
486
+ "glyph": "rS3zOq_glyph",
487
+ "hoverGlyph": "rS3zOq_hoverGlyph",
488
+ "restGlyph": "rS3zOq_restGlyph",
24
489
  "wrap": "rS3zOq_wrap"
25
490
  };
26
491
  //#endregion
@@ -65,11 +530,17 @@ window.__ModuleLoader__.load({
65
530
  title: t("chip.on.title"),
66
531
  disabled: locked || leaving,
67
532
  onClick: off,
68
- children: [t("chip.label"), (0, react_jsx_runtime.jsx)("span", {
69
- className: PlanModeControl_module_css_default.close,
533
+ children: [(0, react_jsx_runtime.jsxs)("span", {
534
+ className: PlanModeControl_module_css_default.glyph,
70
535
  "aria-hidden": true,
71
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFill14, { size: 12 })
72
- })]
536
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlanOutlineRegular, {
537
+ className: PlanModeControl_module_css_default.restGlyph,
538
+ size: 14
539
+ }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseCircleFillRegular, {
540
+ className: PlanModeControl_module_css_default.hoverGlyph,
541
+ size: 14
542
+ })]
543
+ }), t("chip.label")]
73
544
  }), error !== null && (0, react_jsx_runtime.jsx)("span", {
74
545
  className: PlanModeControl_module_css_default.error,
75
546
  role: "status",
@@ -83,35 +554,63 @@ window.__ModuleLoader__.load({
83
554
  /** `plan` namespace dictionaries (the composer plan chip's copy). */
84
555
  /** Simplified Chinese dictionary (the key-set source of truth). */
85
556
  const zh = {
86
- "chip.label": "Plan",
87
- "chip.on.aria": "plan mode 已开启,按下关闭",
88
- "chip.on.title": "plan mode 已开启 — 点击关闭(/plan off)",
89
- "chip.off.aria": "plan mode 已关闭,按下开启",
90
- "chip.off.title": "plan mode 已关闭 — 点击开启(/plan)",
91
- "chip.exitFailed": "退出 plan mode 失败"
557
+ "chip.label": "计划",
558
+ "preview.title": "计划",
559
+ "preview.document": "计划 · Markdown",
560
+ "preview.action": "打开",
561
+ "preview.open": "在侧边栏打开计划",
562
+ "preview.full": "查看全文",
563
+ "preview.openNamed": "打开计划:{title}",
564
+ "preview.loading": "正在读取计划…",
565
+ "preview.failed": "无法读取计划",
566
+ "preview.invalidAddress": "计划地址无效",
567
+ "preview.historyUnavailable": "无法读取会话历史",
568
+ "preview.notFound": "未找到这份计划",
569
+ "preview.unavailable": "计划预览不可用",
570
+ "preview.expired": "临时计划预览已失效,请从仍在等待审批的卡片重新打开。",
571
+ "chip.on.aria": "计划模式已开启,按下关闭",
572
+ "chip.on.title": "计划模式已开启 — 点击关闭(/plan off)",
573
+ "chip.exitFailed": "退出计划模式失败"
92
574
  };
93
575
  /** English dictionary, checked complete against the zh key set. */
94
576
  const en = {
95
577
  "chip.label": "Plan",
578
+ "preview.title": "Plan",
579
+ "preview.document": "Plan · Markdown",
580
+ "preview.action": "Open",
581
+ "preview.open": "Open plan in sidebar",
582
+ "preview.full": "View full plan",
583
+ "preview.openNamed": "Open plan: {title}",
584
+ "preview.loading": "Loading plan…",
585
+ "preview.failed": "Could not load plan",
586
+ "preview.invalidAddress": "Invalid plan address",
587
+ "preview.historyUnavailable": "Session history is unavailable",
588
+ "preview.notFound": "This plan was not found",
589
+ "preview.unavailable": "Plan preview is unavailable",
590
+ "preview.expired": "This temporary plan preview has expired. Reopen it from the pending review card.",
96
591
  "chip.on.aria": "Plan mode on, press to turn off",
97
592
  "chip.on.title": "Plan mode on — click to turn off (/plan off)",
98
- "chip.off.aria": "Plan mode off, press to turn on",
99
- "chip.off.title": "Plan mode off — click to turn on (/plan)",
100
593
  "chip.exitFailed": "Failed to exit plan mode"
101
594
  };
102
595
  //#endregion
103
596
  //#region lib/types/client/index.js
104
597
  /** Dictionary namespace owned by this plugin. */
105
598
  const NS = "plan";
106
- /** Required services: the seat's slot registry, commands Remote, and locale registry. */
599
+ /** Services for plan controls, Conversation projection, and resource navigation. */
107
600
  const inject = [
108
601
  "slots",
109
602
  "remote",
110
603
  "remote.commands",
111
- "locale"
604
+ "remote.session",
605
+ "sessions",
606
+ "locale",
607
+ "uiConversation",
608
+ "resources",
609
+ "sidebarRight",
610
+ "sidebarRightTabs"
112
611
  ];
113
612
  /**
114
- * Client plugin body: register the plan chip over the command channel.
613
+ * Register plan controls, permanent Chat cards, and sidebar document reading.
115
614
  * @param ctx - client root context.
116
615
  */
117
616
  function apply(ctx) {
@@ -119,6 +618,80 @@ window.__ModuleLoader__.load({
119
618
  zh,
120
619
  en
121
620
  }), "ui-plan: dictionaries");
621
+ const previewId = "@deepseek-ai/dsh-client-ui-plan";
622
+ const t = ctx.locale.bind(NS);
623
+ ctx.effect(() => ctx.uiConversation.events.register(planDefinition), "ui-plan: conversation definition");
624
+ ctx.effect(() => ctx.resources.register(planResourceProvider(ctx.remote.session)), "ui-plan: resources");
625
+ ctx.effect(() => ctx.sidebarRightTabs.register({
626
+ id: previewId,
627
+ kind: "plan",
628
+ patterns: ["dsh-resource://plan/**", "dsh-resource://plan-review/**"],
629
+ priority: "builtin",
630
+ canOpen: (address) => parsePlanAddress(address) !== void 0 || isReviewPreviewAddress(address),
631
+ title: () => t("preview.title")
632
+ }), "ui-plan: sidebar type");
633
+ const open = (sessionId) => ({ openPlan: (callId) => {
634
+ const child = ctx.sessions.subagentAddress(sessionId);
635
+ const session = child === void 0 ? {
636
+ kind: "session",
637
+ sessionId
638
+ } : {
639
+ kind: "subagent",
640
+ ...child
641
+ };
642
+ ctx.sidebarRight.openResource(planAddress({
643
+ session,
644
+ callId
645
+ }));
646
+ } });
647
+ const reviewWindow = randomUUID();
648
+ const reviewStore = createPlanReviewStore();
649
+ ctx.slots.inject("conversation.chat.turnTail", () => ctx.slots.register({
650
+ name: "conversation.chat.turnTail",
651
+ id: previewId,
652
+ locale: NS,
653
+ inject: (sessionId) => {
654
+ const binding = ctx.sessions.binding(sessionId);
655
+ if (binding === void 0) throw new Error(`ui-plan: unknown session "${sessionId}"`);
656
+ const chat = ctx.uiConversation.binding(binding).target("chat");
657
+ return {
658
+ ...open(sessionId),
659
+ keyedHooks: { plans: (turn) => {
660
+ const snapshot = chat.getSnapshot();
661
+ if (snapshot === void 0) throw new Error("ui-plan: Chat target is unavailable");
662
+ return snapshot.nodes.turnDataSource(Number(turn), "submitted-plan");
663
+ } }
664
+ };
665
+ }
666
+ }, PlanCards));
667
+ ctx.slots.inject("conversation.plan-review.actions", () => ctx.slots.register({
668
+ name: "conversation.plan-review.actions",
669
+ id: previewId,
670
+ locale: NS,
671
+ store: reviewStore,
672
+ inject: (sessionId) => ({
673
+ openReview: (review, requestKey) => {
674
+ if (review.callId !== void 0) {
675
+ open(sessionId).openPlan(review.callId);
676
+ return;
677
+ }
678
+ ctx.sidebarRight.openResource(reviewPreviewAddress(sessionId, `${reviewWindow}:${requestKey}`), { params: { planReview: {
679
+ markdown: review.plan,
680
+ title: (0, _deepseek_ai_dsh_client_ui_primitives.extractMarkdownPlainText)(review.plan, { mode: "first-line" })
681
+ } } });
682
+ },
683
+ hooks: { sidebarMounted: ctx.sidebarRight.mounted }
684
+ })
685
+ }, PlanReviewOpen));
686
+ ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
687
+ name: "sidebar.right.pane.tab",
688
+ key: previewId,
689
+ locale: NS
690
+ }, PlanPreview));
691
+ ctx.slots.inject("sidebar.right.pane.tab.title", () => ctx.slots.register({
692
+ name: "sidebar.right.pane.tab.title",
693
+ key: previewId
694
+ }, PlanTitle));
122
695
  ctx.slots.inject("conversation.input.plan", () => ctx.slots.register({
123
696
  name: "conversation.input.plan",
124
697
  locale: NS,
@@ -0,0 +1,45 @@
1
+ import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
2
+ import type { SubmittedPlan } from './plan.ts';
3
+ import type { HostObservable, InjectFace, PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
4
+ import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand';
5
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
6
+ import type { createPlanReviewStore } from './review-store.ts';
7
+ /** Session-bound navigation for logged plans. */
8
+ export interface PlanOpenInjected {
9
+ /** Open or focus the exact submitted plan. */
10
+ openPlan: (callId: ToolCallId) => void;
11
+ }
12
+ /** Turn-keyed submitted plans and Session-bound navigation. */
13
+ export interface PlanCardsInjected extends PlanOpenInjected {
14
+ keyedHooks: {
15
+ /** Resolve only this Turn's submitted plans, in invocation order. */
16
+ plans: (turn: string) => ObservableSnapshot<readonly SubmittedPlan[]>;
17
+ };
18
+ }
19
+ /** Session-bound preview navigation for one pending review. */
20
+ export interface PlanReviewOpenInjected {
21
+ /** Open the logged plan, or the request's temporary document when no invocation exists. */
22
+ openReview: (review: PropsRuntime<'conversation.plan-review.actions'>['review'], requestKey: string) => void;
23
+ readonly hooks: {
24
+ /** The session whose right Sidebar seat is mounted; `undefined` while none is on screen. */
25
+ readonly sidebarMounted: HostObservable<SessionId | undefined>;
26
+ };
27
+ }
28
+ /**
29
+ * Render the completed Turn's submitted plans in invocation order.
30
+ * @param props - Logged plan, localized copy, and Session-bound navigation.
31
+ * @returns keyboard-accessible plan cards, or null for a Turn without plans.
32
+ */
33
+ export declare function PlanCards({ turn, usePlans, openPlan, t }: PropsRuntime<'conversation.chat.turnTail'> & InjectFace<PlanCardsInjected> & PropsLocale<'plan'>): import("react").JSX.Element | null;
34
+ /**
35
+ * Open each pending plan automatically and retain a manual opener without answering it.
36
+ *
37
+ * The automatic open waits for a mounted Sidebar seat: a review that arrives
38
+ * while the Conversation is off screen mounts in the same commit as the seat,
39
+ * ahead of it, and the seat binds from its own effect. Reading the bound
40
+ * session through the hook opens once that binding exists.
41
+ * @param props - Review identity, Session store, localized copy, and navigation.
42
+ * @returns an opener for either logged or temporary plan text.
43
+ */
44
+ export declare function PlanReviewOpen({ review, requestKey, openReview, useSidebarMounted, t, useStore, actions }: PropsRuntime<'conversation.plan-review.actions'> & InjectFace<PlanReviewOpenInjected> & PropsLocale<'plan'> & PropsStore<ReturnType<typeof createPlanReviewStore>>): import("react").JSX.Element;
45
+ //# sourceMappingURL=PlanCard.d.ts.map
@@ -0,0 +1,16 @@
1
+ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
2
+ type PlanPreviewProps = PropsRuntime<'sidebar.right.pane.tab'> & PropsLocale<'plan'>;
3
+ /**
4
+ * Render the submitted plan with its complete Markdown.
5
+ * @param props - Framework-bound tab identity, resource, and copy.
6
+ * @returns the plan document or a localized loading/failure state.
7
+ */
8
+ export declare function PlanPreview({ useTabInfo, useResource, t }: PlanPreviewProps): import("react").JSX.Element;
9
+ /**
10
+ * Display a plain file icon and the heading in its tab after resource recovery.
11
+ * @param props - Framework-bound tab identity and resource reader.
12
+ * @returns a decorative file icon followed by the recovered title or initial localized label.
13
+ */
14
+ export declare function PlanTitle({ useTabInfo, useResource }: PropsRuntime<'sidebar.right.pane.tab.title'>): import("react").JSX.Element;
15
+ export {};
16
+ //# sourceMappingURL=PlanPreview.d.ts.map
@@ -0,0 +1,11 @@
1
+ /** Localized plan-owned failures and unmodified external diagnostics. */
2
+ import type { RemoteFailure } from '@deepseek-ai/dsh-typert-protocol';
3
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client';
4
+ /**
5
+ * Explain a failed plan read in the current locale.
6
+ * @param t - Plan namespace translator.
7
+ * @param failure - Failure reported by the resource provider.
8
+ * @returns localized plan copy, or the external failure's diagnostic.
9
+ */
10
+ export declare function planFailureLine(t: TranslateNS<'plan'>, failure: RemoteFailure): string;
11
+ //# sourceMappingURL=failure-line.d.ts.map
@@ -15,10 +15,10 @@ export interface PlanChipInjected {
15
15
  */
16
16
  exitPlanMode: () => Promise<string | null>;
17
17
  }
18
- /** Required services: the seat's slot registry, commands Remote, and locale registry. */
18
+ /** Services for plan controls, Conversation projection, and resource navigation. */
19
19
  export declare const inject: string[];
20
20
  /**
21
- * Client plugin body: register the plan chip over the command channel.
21
+ * Register plan controls, permanent Chat cards, and sidebar document reading.
22
22
  * @param ctx - client root context.
23
23
  */
24
24
  export declare function apply(ctx: ClientContext): void;
@@ -2,10 +2,21 @@
2
2
  /** Simplified Chinese dictionary (the key-set source of truth). */
3
3
  export declare const zh: {
4
4
  'chip.label': string;
5
+ 'preview.title': string;
6
+ 'preview.document': string;
7
+ 'preview.action': string;
8
+ 'preview.open': string;
9
+ 'preview.full': string;
10
+ 'preview.openNamed': string;
11
+ 'preview.loading': string;
12
+ 'preview.failed': string;
13
+ 'preview.invalidAddress': string;
14
+ 'preview.historyUnavailable': string;
15
+ 'preview.notFound': string;
16
+ 'preview.unavailable': string;
17
+ 'preview.expired': string;
5
18
  'chip.on.aria': string;
6
19
  'chip.on.title': string;
7
- 'chip.off.aria': string;
8
- 'chip.off.title': string;
9
20
  'chip.exitFailed': string;
10
21
  };
11
22
  /** The plan namespace key union. */
@@ -13,10 +24,21 @@ export type PlanKey = keyof typeof zh;
13
24
  /** English dictionary, checked complete against the zh key set. */
14
25
  export declare const en: {
15
26
  'chip.label': string;
27
+ 'preview.title': string;
28
+ 'preview.document': string;
29
+ 'preview.action': string;
30
+ 'preview.open': string;
31
+ 'preview.full': string;
32
+ 'preview.openNamed': string;
33
+ 'preview.loading': string;
34
+ 'preview.failed': string;
35
+ 'preview.invalidAddress': string;
36
+ 'preview.historyUnavailable': string;
37
+ 'preview.notFound': string;
38
+ 'preview.unavailable': string;
39
+ 'preview.expired': string;
16
40
  'chip.on.aria': string;
17
41
  'chip.on.title': string;
18
- 'chip.off.aria': string;
19
- 'chip.off.title': string;
20
42
  'chip.exitFailed': string;
21
43
  };
22
44
  //# sourceMappingURL=locales.d.ts.map
@@ -0,0 +1,11 @@
1
+ import type { ConversationNodeDefinition } from '@deepseek-ai/dsh-client-ui-conversation/client';
2
+ import { type SubmittedPlan } from './plan.ts';
3
+ declare module '@deepseek-ai/dsh-client-ui-chat/client' {
4
+ interface ChatNodeDataMap {
5
+ /** Complete plan submitted through exit_plan_mode, including rejected or dismissed reviews. */
6
+ 'submitted-plan': SubmittedPlan;
7
+ }
8
+ }
9
+ /** One card per invocation; a later PTC settlement retains the original card position. */
10
+ export declare const planDefinition: ConversationNodeDefinition<SubmittedPlan>;
11
+ //# sourceMappingURL=plan-definition.d.ts.map
@@ -0,0 +1,32 @@
1
+ /** Read immutable plan arguments from a Session snapshot and earlier history pages. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ import type { ResourceProvider } from '@deepseek-ai/dsh-client-resources/client';
4
+ import { type SubmittedPlan } from './plan.ts';
5
+ declare module '@deepseek-ai/dsh-typert-protocol' {
6
+ interface RemoteErrorDetailsMap {
7
+ /** The resource URL does not identify a plan invocation. */
8
+ 'plan/invalid-address': Record<string, never>;
9
+ /** Session history ended before an opening snapshot. */
10
+ 'plan/unavailable': Record<string, never>;
11
+ /** The Session has no readable plan for this invocation. */
12
+ 'plan/not-found': Record<string, never>;
13
+ /** The Session history read failed. */
14
+ 'plan/read-failed': Record<string, never>;
15
+ }
16
+ }
17
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
18
+ interface ResourceProtocolMap {
19
+ /** Immutable Markdown from a logged exit_plan_mode invocation. */
20
+ plan: SubmittedPlan;
21
+ }
22
+ }
23
+ /**
24
+ * Bind plan reads to the generated Session Remote face.
25
+ * Opening a follow reads projections and may activate a prepared Session on the Host.
26
+ * Generated Remote streams can throw carrier failures; the provider reports failed
27
+ * reads as resource failure frames and preserves Remote error codes.
28
+ * @param remote - Existing Session history API.
29
+ * @returns a provider whose reads stop after finding the exact invocation.
30
+ */
31
+ export declare function planResourceProvider(remote: Pick<Context['remote']['session'], 'follow' | 'page'>): ResourceProvider<'plan'>;
32
+ //# sourceMappingURL=plan-resource.d.ts.map
@@ -0,0 +1,39 @@
1
+ /** Plan text and resource identities derived from logged native or PTC calls. */
2
+ import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand';
3
+ import type { SessionAddress } from '@deepseek-ai/dsh-api-session-controller/types';
4
+ /** Complete Markdown and the heading displayed by a plan preview. */
5
+ export interface PlanDocument {
6
+ readonly markdown: string;
7
+ readonly title: string;
8
+ }
9
+ /** One submitted plan, identified by its originating tool invocation. */
10
+ export interface SubmittedPlan extends PlanDocument {
11
+ readonly callId: ToolCallId;
12
+ }
13
+ /** A saved sidebar resource names one invocation in one Session. */
14
+ export interface PlanAddress {
15
+ readonly session: SessionAddress;
16
+ readonly callId: ToolCallId;
17
+ }
18
+ /**
19
+ * Read a complete plan from untrusted logged arguments.
20
+ * @param event - Native call or PTC dispatch event from Session history.
21
+ * @returns the submitted plan, or undefined for unrelated or malformed data.
22
+ */
23
+ export declare function submittedPlan(event: {
24
+ readonly type: string;
25
+ readonly data: unknown;
26
+ }): SubmittedPlan | undefined;
27
+ /**
28
+ * Encode the durable identity of a plan without retaining its text in layout storage.
29
+ * @param target - Session and tool-call identity.
30
+ * @returns the plan resource address.
31
+ */
32
+ export declare function planAddress(target: PlanAddress): string;
33
+ /**
34
+ * Validate a saved or caller-supplied plan resource address.
35
+ * @param address - Address submitted to the sidebar or resource provider.
36
+ * @returns the decoded identity, or undefined for an unsupported address.
37
+ */
38
+ export declare function parsePlanAddress(address: string): PlanAddress | undefined;
39
+ //# sourceMappingURL=plan.d.ts.map
@@ -0,0 +1,25 @@
1
+ /** Temporary review previews carry their document in in-memory tab navigation. */
2
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
3
+ import type { PlanDocument } from './plan.ts';
4
+ declare module '@deepseek-ai/dsh-client-ui-sidebar-right/client' {
5
+ interface SidebarRightResourceParamsMap {
6
+ /** Review text without a logged invocation; never persisted in sidebar layout. */
7
+ 'plan-review': {
8
+ planReview: PlanDocument;
9
+ };
10
+ }
11
+ }
12
+ /**
13
+ * Name one temporary review within its browser lifetime and Session.
14
+ * @param sessionId - Session displaying the review.
15
+ * @param requestKey - Browser-unique pending request identity.
16
+ * @returns the address used to focus or reopen its preview.
17
+ */
18
+ export declare function reviewPreviewAddress(sessionId: SessionId, requestKey: string): string;
19
+ /**
20
+ * Recognize temporary plan navigation without interpreting it as logged history.
21
+ * @param address - Saved or caller-supplied navigation address.
22
+ * @returns whether the address identifies a temporary review preview.
23
+ */
24
+ export declare function isReviewPreviewAddress(address: string): boolean;
25
+ //# sourceMappingURL=review-preview.d.ts.map
@@ -0,0 +1,15 @@
1
+ /** Session-owned memory of pending plans already opened automatically. */
2
+ import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-store';
3
+ interface PlanReviewState {
4
+ opened: Partial<Record<string, true>>;
5
+ }
6
+ type PlanReviewActions = {
7
+ markOpened: (draft: PlanReviewState, reviewKey: string) => void;
8
+ };
9
+ /**
10
+ * Keep manual sidebar closure effective across review component remounts.
11
+ * @returns a transient store handle whose instances belong to Session scopes.
12
+ */
13
+ export declare function createPlanReviewStore(): EngineStoreHandle<PlanReviewState, PlanReviewActions>;
14
+ export {};
15
+ //# sourceMappingURL=review-store.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-ui-plan",
3
- "description": "Plan-mode composer control: the conversation.input.plan seat over the plan projection and the /plan command channel",
4
- "version": "0.1.6-alpha.1",
3
+ "description": "Plan mode controls, persistent transcript plan cards, and sidebar Markdown previews",
4
+ "version": "0.1.7-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -30,29 +30,44 @@
30
30
  "inject": [
31
31
  "@deepseek-ai/dsh-api-remotes",
32
32
  "@deepseek-ai/dsh-client-locale",
33
- "@deepseek-ai/dsh-client-ui-conversation"
33
+ "@deepseek-ai/dsh-client-ui-conversation",
34
+ "@deepseek-ai/dsh-client-ui-sidebar-right",
35
+ "@deepseek-ai/dsh-client-ui-chat",
36
+ "@deepseek-ai/dsh-client-ui-user-questions",
37
+ "@deepseek-ai/dsh-client-resources",
38
+ "@deepseek-ai/dsh-api-session-controller"
34
39
  ],
35
40
  "platform": "web"
36
41
  }
37
42
  },
38
43
  "license": "MIT",
39
44
  "peerDependencies": {
40
- "@deepseek-ai/cordis": "^4.0.2"
45
+ "@deepseek-ai/cordis": "^4.0.3"
41
46
  },
42
47
  "devDependencies": {
43
48
  "@types/react": "~18.3.1",
44
49
  "react": "^18.2.0",
45
- "@deepseek-ai/dsh-api-remotes": "^0.1.6-alpha.1",
46
- "@deepseek-ai/dsh-client-locale": "^0.1.6-alpha.1",
47
- "@deepseek-ai/dsh-client-test-runtime": "^0.1.6-alpha.1",
48
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.6-alpha.1",
49
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.6-alpha.1",
50
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.6-alpha.1",
51
- "@deepseek-ai/dsh-plan-mode": "^0.1.6-alpha.1",
52
- "@deepseek-ai/cordis": "^4.0.2",
53
- "@deepseek-ai/dsh-session": "^0.1.6-alpha.1",
54
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.6-alpha.1",
55
- "@deepseek-ai/dsh-client-ui-session": "^0.1.6-alpha.1"
50
+ "@deepseek-ai/dsh-api-remotes": "^0.1.7-alpha.1",
51
+ "@deepseek-ai/dsh-client-locale": "^0.1.7-alpha.1",
52
+ "@deepseek-ai/dsh-client-store": "^0.1.7-alpha.1",
53
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.7-alpha.1",
54
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-alpha.1",
55
+ "@deepseek-ai/dsh-plan-mode": "^0.1.7-alpha.1",
56
+ "@deepseek-ai/dsh-client-test-runtime": "^0.1.7-alpha.1",
57
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-alpha.1",
58
+ "@deepseek-ai/cordis": "^4.0.3",
59
+ "@deepseek-ai/dsh-session": "^0.1.7-alpha.1",
60
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-alpha.1",
61
+ "@deepseek-ai/dsh-client-ui-session": "^0.1.7-alpha.1",
62
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.7-alpha.1",
63
+ "@deepseek-ai/dsh-client-ui-chat": "^0.1.7-alpha.1",
64
+ "@deepseek-ai/dsh-client-ui-sidebar-right": "^0.1.7-alpha.1",
65
+ "@deepseek-ai/dsh-client-ui-user-questions": "^0.1.7-alpha.1",
66
+ "@deepseek-ai/dsh-client-resources": "^0.1.7-alpha.1",
67
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.7-alpha.1",
68
+ "@deepseek-ai/dsh-llm": "^0.1.7-alpha.1",
69
+ "@deepseek-ai/dsh-tools": "^0.1.7-alpha.1",
70
+ "@deepseek-ai/dsh-util-crypto": "^0.1.7-alpha.1"
56
71
  },
57
72
  "files": [
58
73
  "lib/index.js",