@huanlin/dsh-plugin-better-plan 0.4.0
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.md +141 -0
- package/cordis.patch.yml +27 -0
- package/lib/client.js +869 -0
- package/lib/index.js +1095 -0
- package/lib/types/client/PlanView.d.ts +35 -0
- package/lib/types/client/execution-launch.d.ts +65 -0
- package/lib/types/client/icons.d.ts +16 -0
- package/lib/types/client/index.d.ts +56 -0
- package/lib/types/client/locales.d.ts +94 -0
- package/lib/types/client/markdown-props.d.ts +44 -0
- package/lib/types/client/review-store.d.ts +76 -0
- package/lib/types/config.d.ts +53 -0
- package/lib/types/context.d.ts +80 -0
- package/lib/types/delivery-registry.d.ts +59 -0
- package/lib/types/first-heading.d.ts +25 -0
- package/lib/types/index.d.ts +74 -0
- package/lib/types/locale.d.ts +112 -0
- package/lib/types/plan-fold.d.ts +17 -0
- package/lib/types/prompt-override.d.ts +62 -0
- package/lib/types/resolve-cwd.d.ts +41 -0
- package/lib/types/review-gate.d.ts +107 -0
- package/lib/types/review-route.d.ts +90 -0
- package/lib/types/shadow-tool.d.ts +93 -0
- package/lib/types/trust-fence.d.ts +25 -0
- package/lib/types/ws-route.d.ts +58 -0
- package/package.json +116 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@huanlin/dsh-plugin-better-plan",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
//#region src/client/icons.tsx
|
|
10
|
+
/**
|
|
11
|
+
* Inline SVG icon for the Plan tab (client bundles must not value-import
|
|
12
|
+
* other plugins' internals, and a one-glyph icon does not justify a
|
|
13
|
+
* react-icons dependency).
|
|
14
|
+
*
|
|
15
|
+
* @module @huanlin/dsh-plugin-better-plan/client/icons
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The Plan tab glyph: a checklist document.
|
|
19
|
+
* @param props - size in pixels.
|
|
20
|
+
* @returns the icon element.
|
|
21
|
+
*/
|
|
22
|
+
function IconPlanOutline16(props) {
|
|
23
|
+
return (0, react.createElement)("svg", {
|
|
24
|
+
width: props.size,
|
|
25
|
+
height: props.size,
|
|
26
|
+
viewBox: "0 0 16 16",
|
|
27
|
+
fill: "none",
|
|
28
|
+
stroke: "currentColor",
|
|
29
|
+
strokeWidth: 1.3,
|
|
30
|
+
strokeLinecap: "round",
|
|
31
|
+
strokeLinejoin: "round",
|
|
32
|
+
"aria-hidden": true
|
|
33
|
+
}, (0, react.createElement)("path", { d: "M9.5 1.5H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5l-3.5-3.5Z" }), (0, react.createElement)("path", { d: "M9.5 1.5V5H13" }), (0, react.createElement)("path", { d: "M5.5 8.5 6.5 9.5 8.5 7" }), (0, react.createElement)("path", { d: "M5.5 11.5h5" }));
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/client/locales.ts
|
|
37
|
+
/**
|
|
38
|
+
* Client-half copy for the Plan tab, following the DSH i18n system: the
|
|
39
|
+
* dictionaries register into the shared locale registry (namespace
|
|
40
|
+
* `betterPlan`), and `t()` resolves the active locale from the attached
|
|
41
|
+
* `ctx.locale` service (`@deepseek-ai/dsh-client-locale`) — the Host-backed
|
|
42
|
+
* `locale.preference` wins over the raw browser language and switches live.
|
|
43
|
+
* Absent the service (locale plugin not mounted), the browser language is
|
|
44
|
+
* the fallback.
|
|
45
|
+
*
|
|
46
|
+
* The module-level attach mirrors better-sidebar's own locales.ts: the Plan
|
|
47
|
+
* tab renders inside the sidebar's React tree, so the component reads copy
|
|
48
|
+
* through `t()` at render time; the sidebar re-renders tab content on locale
|
|
49
|
+
* switches (its tab-content memo keys on the locale revision), so no extra
|
|
50
|
+
* subscription is needed here.
|
|
51
|
+
*
|
|
52
|
+
* @module @huanlin/dsh-plugin-better-plan/client/locales
|
|
53
|
+
*/
|
|
54
|
+
/** The zh dictionary (source of truth for the key set). */
|
|
55
|
+
const zhDict = {
|
|
56
|
+
tabTitle: "计划",
|
|
57
|
+
openInEditor: "在编辑器中打开",
|
|
58
|
+
copyPath: "复制路径",
|
|
59
|
+
copied: "已复制",
|
|
60
|
+
copy: "复制",
|
|
61
|
+
copiedLabel: "已复制",
|
|
62
|
+
reviewHint: "在此审阅计划——聊天中不会弹出审批卡。点「批准」在本对话执行,点「新开对话执行」移到全新对话执行,或附反馈选择「继续规划」。",
|
|
63
|
+
feedbackPlaceholder: "「继续规划」时可附反馈(可选)…",
|
|
64
|
+
approve: "批准",
|
|
65
|
+
keepPlanning: "继续规划",
|
|
66
|
+
approveNewSession: "新开对话执行",
|
|
67
|
+
delegatingStatus: "计划已批准——正在新建执行对话…",
|
|
68
|
+
delegatedStatus: "计划已批准——执行已移交到新对话。",
|
|
69
|
+
errDelegateFailed: "新开执行对话失败",
|
|
70
|
+
approvedStatus: "计划已批准——模型正在执行该计划。",
|
|
71
|
+
keptStatus: "反馈已发送——模型正在修改计划。",
|
|
72
|
+
loading: "正在读取计划…",
|
|
73
|
+
readFailed: "读取计划失败",
|
|
74
|
+
retry: "重试",
|
|
75
|
+
truncated: "文件因侧边栏读取上限被截断;其余内容请在编辑器中查看。",
|
|
76
|
+
noPath: "该计划标签页未携带文件路径",
|
|
77
|
+
binaryFile: "计划文件是二进制文件;请在编辑器中查看",
|
|
78
|
+
unexpectedRead: "fs.read 响应格式异常",
|
|
79
|
+
errStale: "当前面板已过期——有更新的计划正在审批。",
|
|
80
|
+
errNoPending: "当前没有等待审批的计划。",
|
|
81
|
+
errSubmitFailed: "提交决定失败"
|
|
82
|
+
};
|
|
83
|
+
/** The en dictionary (key-set-equal to zh, enforced by the type annotation). */
|
|
84
|
+
const enDict = {
|
|
85
|
+
tabTitle: "Plan",
|
|
86
|
+
openInEditor: "Open in editor",
|
|
87
|
+
copyPath: "Copy path",
|
|
88
|
+
copied: "Copied",
|
|
89
|
+
copy: "Copy",
|
|
90
|
+
copiedLabel: "Copied",
|
|
91
|
+
reviewHint: "Review this plan here — the chat shows no approval popup. Approve to execute in this conversation, execute in a new chat, or keep planning with feedback.",
|
|
92
|
+
feedbackPlaceholder: "Optional feedback for \"Keep planning\"…",
|
|
93
|
+
approve: "Approve",
|
|
94
|
+
keepPlanning: "Keep planning",
|
|
95
|
+
approveNewSession: "Execute in new chat",
|
|
96
|
+
delegatingStatus: "Plan approved — starting the execution conversation…",
|
|
97
|
+
delegatedStatus: "Plan approved — execution continues in a new conversation.",
|
|
98
|
+
errDelegateFailed: "Failed to start the execution conversation",
|
|
99
|
+
approvedStatus: "Plan approved — the model is carrying out the plan.",
|
|
100
|
+
keptStatus: "Feedback sent — the model is revising the plan.",
|
|
101
|
+
loading: "Loading plan…",
|
|
102
|
+
readFailed: "Failed to read the plan",
|
|
103
|
+
retry: "Retry",
|
|
104
|
+
truncated: "The file was truncated by the sidebar read limit; open it in the editor for the rest.",
|
|
105
|
+
noPath: "this plan tab carries no file path",
|
|
106
|
+
binaryFile: "the plan file is binary; review it in the editor instead",
|
|
107
|
+
unexpectedRead: "unexpected fs.read response",
|
|
108
|
+
errStale: "This panel is stale — a newer plan delivery is under review.",
|
|
109
|
+
errNoPending: "No plan is awaiting review.",
|
|
110
|
+
errSubmitFailed: "Submitting the decision failed"
|
|
111
|
+
};
|
|
112
|
+
const DICTS = {
|
|
113
|
+
zh: zhDict,
|
|
114
|
+
en: enDict
|
|
115
|
+
};
|
|
116
|
+
/** The locale namespace this plugin owns in the DSH locale registry. */
|
|
117
|
+
const LOCALE_NS = "betterPlan";
|
|
118
|
+
/** The DSH locale service attached by the client apply (absent → browser detection). */
|
|
119
|
+
let localeService;
|
|
120
|
+
/**
|
|
121
|
+
* Attach (or detach, with undefined) the DSH locale service.
|
|
122
|
+
* @param service - the client context's locale service.
|
|
123
|
+
*/
|
|
124
|
+
function attachLocale(service) {
|
|
125
|
+
localeService = service;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The active copy locale: the DSH locale service's snapshot when attached
|
|
129
|
+
* (zh → zh, anything else → en), else the browser language.
|
|
130
|
+
* @returns `'zh'` or `'en'`.
|
|
131
|
+
*/
|
|
132
|
+
function activeLocale() {
|
|
133
|
+
const active = localeService?.getSnapshot().active;
|
|
134
|
+
if (active !== void 0) return active === "zh" ? "zh" : "en";
|
|
135
|
+
return (typeof navigator !== "undefined" ? navigator.language : "en").toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Translate one copy key in the active locale.
|
|
139
|
+
* @param key - the copy key.
|
|
140
|
+
* @returns the localized string.
|
|
141
|
+
*/
|
|
142
|
+
function t(key) {
|
|
143
|
+
return DICTS[activeLocale()][key];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The kickoff prompt the delegation flow queues into the NEW conversation:
|
|
147
|
+
* the model there has no context, so the message anchors it on the approved
|
|
148
|
+
* plan file (an absolute path) and orders execution. It is both
|
|
149
|
+
* user-visible (a chat bubble) and model-directed, so it localizes with the
|
|
150
|
+
* view like the steer copy does.
|
|
151
|
+
* @param path - the absolute plan file path.
|
|
152
|
+
* @returns the kickoff prompt text.
|
|
153
|
+
*/
|
|
154
|
+
function executionKickoffPrompt(path) {
|
|
155
|
+
if (activeLocale() === "zh") return `[计划执行] 用户已在上一对话中制定并批准了以下计划文件:${path}。这是一个全新对话——请先读取该计划文件,然后严格按计划开始执行,无需重新规划或再次确认。`;
|
|
156
|
+
return `[Plan execution] The user planned and approved the following plan file in a previous conversation: ${path}. This is a fresh conversation — read that plan file first, then carry it out exactly as written; no re-planning or re-confirmation needed.`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Localize one review-route error for the action bar: known error codes map
|
|
160
|
+
* to copy; unknown codes fall back to the route's raw English message.
|
|
161
|
+
* @param code - the route's stable error code, when present.
|
|
162
|
+
* @param raw - the route's raw error message (or an HTTP fallback).
|
|
163
|
+
* @returns the text the action bar shows.
|
|
164
|
+
*/
|
|
165
|
+
function submitErrorText(code, raw) {
|
|
166
|
+
if (code === "stale_review") return t("errStale");
|
|
167
|
+
if (code === "no_pending") return t("errNoPending");
|
|
168
|
+
if (code === void 0 || code === "") return `${t("errSubmitFailed")}: ${raw}`;
|
|
169
|
+
return `${t("errSubmitFailed")}: ${raw}`;
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/client/markdown-props.ts
|
|
173
|
+
/** Build the dual-shape chrome labels from a flat copy-button pair. */
|
|
174
|
+
function markdownChromeLabels(labels) {
|
|
175
|
+
return {
|
|
176
|
+
copyLabel: labels.copyLabel,
|
|
177
|
+
copiedLabel: labels.copiedLabel,
|
|
178
|
+
code: {
|
|
179
|
+
copyLabel: labels.copyLabel,
|
|
180
|
+
copiedLabel: labels.copiedLabel
|
|
181
|
+
},
|
|
182
|
+
footnotes: ""
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* MarkdownText props carrying the labels under BOTH prop names. The cast is
|
|
187
|
+
* load-bearing: this plugin builds against one generation's declaration where
|
|
188
|
+
* the other prop does not exist.
|
|
189
|
+
* @param text - the markdown source.
|
|
190
|
+
* @param labels - the flat copy-button pair.
|
|
191
|
+
* @returns props spreadable onto `MarkdownText`.
|
|
192
|
+
*/
|
|
193
|
+
function markdownTextProps(text, labels) {
|
|
194
|
+
const chrome = markdownChromeLabels(labels);
|
|
195
|
+
return {
|
|
196
|
+
text,
|
|
197
|
+
codeLabels: chrome,
|
|
198
|
+
labels: chrome
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/client/execution-launch.ts
|
|
203
|
+
/**
|
|
204
|
+
* The delegation flow's client half: after a review settles as
|
|
205
|
+
* `delegated`, create the execution conversation through DSH's public
|
|
206
|
+
* sessions service (`ctx.get('sessions')`), queue the kickoff prompt into
|
|
207
|
+
* it, and navigate there.
|
|
208
|
+
*
|
|
209
|
+
* The faces below are STRUCTURAL (the minimal subset of
|
|
210
|
+
* `@deepseek-ai/dsh-api-session-controller/client`'s `ISessions` /
|
|
211
|
+
* `SessionFace` this flow calls) — the same pattern better-sidebar uses for
|
|
212
|
+
* cross-plugin services: no value import crosses the client-bundle purity
|
|
213
|
+
* gate and no new tsconfig path is needed; the runtime service arrives
|
|
214
|
+
* through the context proxy. The planning session's list summary supplies
|
|
215
|
+
* the new conversation's cwd, so the fresh agent runs on the same
|
|
216
|
+
* workspace the plan was written for.
|
|
217
|
+
*
|
|
218
|
+
* @module @huanlin/dsh-plugin-better-plan/client/execution-launch
|
|
219
|
+
*/
|
|
220
|
+
/**
|
|
221
|
+
* Launch the execution conversation for a delegated plan: create a blank
|
|
222
|
+
* session in the planning session's workspace, queue the kickoff prompt
|
|
223
|
+
* pointing at the approved plan file, then navigate to the new
|
|
224
|
+
* conversation. Every failure rejects with an Error whose message the
|
|
225
|
+
* plan panel shows (the plan path stays visible there, so the user can
|
|
226
|
+
* always start a conversation and send the plan manually).
|
|
227
|
+
* @param sessions - the sessions service (`ctx.get('sessions')`).
|
|
228
|
+
* @param planPath - the approved plan file's absolute path.
|
|
229
|
+
* @param sessionId - the planning session (its summary carries the cwd).
|
|
230
|
+
*/
|
|
231
|
+
async function launchExecutionConversation(sessions, planPath, sessionId) {
|
|
232
|
+
const cwd = sessions.list.getSnapshot().byId[sessionId]?.cwd;
|
|
233
|
+
const newId = await sessions.create(cwd === void 0 ? {} : { cwd });
|
|
234
|
+
const binding = sessions.binding(newId);
|
|
235
|
+
if (binding === void 0) throw new Error(`the new session "${newId}" is not locally addressable yet`);
|
|
236
|
+
const admitted = await binding.session.prompt([{
|
|
237
|
+
type: "text",
|
|
238
|
+
text: executionKickoffPrompt(planPath)
|
|
239
|
+
}], "queue");
|
|
240
|
+
if (!admitted.ok) throw new Error(`the kickoff prompt was rejected: ${admitted.error?.code ?? "unknown"}: ${admitted.error?.message ?? ""}`);
|
|
241
|
+
sessions.open(newId);
|
|
242
|
+
}
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/client/review-store.ts
|
|
245
|
+
/**
|
|
246
|
+
* The client-side review state: a tiny external store fed by the delivery
|
|
247
|
+
* WebSocket's `{ kind: 'review', review }` frames and by the decision POST's
|
|
248
|
+
* echoed response, consumed by the Plan tab's action bar through
|
|
249
|
+
* `useSyncExternalStore`.
|
|
250
|
+
*
|
|
251
|
+
* The state mirrors the host gate's per-session review; a session switch
|
|
252
|
+
* resets it (the reconnect's attach replay restores the current state).
|
|
253
|
+
*
|
|
254
|
+
* @module @huanlin/dsh-plugin-better-plan/client/review-store
|
|
255
|
+
*/
|
|
256
|
+
/** Whether an unknown wire value is a well-formed review state. */
|
|
257
|
+
function isReviewState(value) {
|
|
258
|
+
if (value === null || typeof value !== "object") return false;
|
|
259
|
+
const record = value;
|
|
260
|
+
return typeof record.id === "string" && record.id !== "" && typeof record.path === "string" && record.path !== "" && typeof record.title === "string" && (record.status === "pending" || record.status === "approved" || record.status === "delegated" || record.status === "kept" || record.status === "cancelled");
|
|
261
|
+
}
|
|
262
|
+
/** External store for the current session's review state. */
|
|
263
|
+
var ReviewStore = class {
|
|
264
|
+
state = null;
|
|
265
|
+
listeners = /* @__PURE__ */ new Set();
|
|
266
|
+
/** The current review state (null = nothing to review). */
|
|
267
|
+
get = () => this.state;
|
|
268
|
+
/** Replace the state and notify subscribers. */
|
|
269
|
+
set = (next) => {
|
|
270
|
+
this.state = next;
|
|
271
|
+
for (const listener of this.listeners) listener();
|
|
272
|
+
};
|
|
273
|
+
/** Clear the state (session switch); attach replay restores it. */
|
|
274
|
+
reset = () => {
|
|
275
|
+
this.set(null);
|
|
276
|
+
};
|
|
277
|
+
/** @returns the unsubscribe disposer. */
|
|
278
|
+
subscribe = (listener) => {
|
|
279
|
+
this.listeners.add(listener);
|
|
280
|
+
return () => {
|
|
281
|
+
this.listeners.delete(listener);
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
/** The singleton store the WS handler writes and the Plan tab reads. */
|
|
286
|
+
const reviewStore = new ReviewStore();
|
|
287
|
+
/** The exact pathname of the host's review decision route. */
|
|
288
|
+
const REVIEW_API_PATH = "/better-plan/api/review";
|
|
289
|
+
/**
|
|
290
|
+
* Bootstrap the review state over HTTP (the WS attach replay remains the
|
|
291
|
+
* live channel): fills the bar when a review frame was missed (stale bundle,
|
|
292
|
+
* reconnect gap). A live frame already in the store wins — the GET result is
|
|
293
|
+
* only applied while the store is empty, so a late null response can never
|
|
294
|
+
* clear a pending bar. The request reports the view's active locale so the
|
|
295
|
+
* host's user-facing copy follows the browser.
|
|
296
|
+
* @param sessionId - the session whose review state to read.
|
|
297
|
+
*/
|
|
298
|
+
async function fetchReviewState(sessionId) {
|
|
299
|
+
try {
|
|
300
|
+
const url = `${REVIEW_API_PATH}?session=${encodeURIComponent(sessionId)}&locale=${encodeURIComponent(activeLocale())}`;
|
|
301
|
+
const response = await fetch(url);
|
|
302
|
+
const parsed = await response.json().catch(() => null);
|
|
303
|
+
if (!response.ok || parsed === null || parsed.ok !== true) return;
|
|
304
|
+
if (reviewStore.get() !== null) return;
|
|
305
|
+
if (parsed.review === null) reviewStore.set(null);
|
|
306
|
+
else if (isReviewState(parsed.review)) reviewStore.set(parsed.review);
|
|
307
|
+
} catch {}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Post one review decision to the host route and, on success, echo the
|
|
311
|
+
* settled state into the store (the submitting view updates immediately;
|
|
312
|
+
* other views follow over the WebSocket). The body reports the view's active
|
|
313
|
+
* locale; failures map the route's stable error codes to localized copy.
|
|
314
|
+
* @param sessionId - the session whose plan is under review.
|
|
315
|
+
* @param decision - the user's choice.
|
|
316
|
+
* @param feedback - optional keep-planning feedback.
|
|
317
|
+
* @param reviewId - the reviewed delivery's id (stale-click guard).
|
|
318
|
+
* @returns the outcome; failures keep the pending bar up for a retry.
|
|
319
|
+
*/
|
|
320
|
+
async function submitReviewDecision(sessionId, decision, feedback, reviewId) {
|
|
321
|
+
try {
|
|
322
|
+
const response = await fetch(REVIEW_API_PATH, {
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers: { "content-type": "application/json" },
|
|
325
|
+
body: JSON.stringify({
|
|
326
|
+
session: sessionId,
|
|
327
|
+
decision,
|
|
328
|
+
locale: activeLocale(),
|
|
329
|
+
...decision === "keep" && feedback !== void 0 && feedback !== "" ? { feedback } : {},
|
|
330
|
+
id: reviewId
|
|
331
|
+
})
|
|
332
|
+
});
|
|
333
|
+
const parsed = await response.json().catch(() => null);
|
|
334
|
+
if (!response.ok || parsed === null || parsed.ok !== true) {
|
|
335
|
+
const raw = typeof parsed?.error === "string" ? parsed.error : `HTTP ${response.status}`;
|
|
336
|
+
return {
|
|
337
|
+
ok: false,
|
|
338
|
+
error: submitErrorText(typeof parsed?.code === "string" ? parsed.code : void 0, raw)
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (!isReviewState(parsed.review)) return {
|
|
342
|
+
ok: false,
|
|
343
|
+
error: t("errSubmitFailed")
|
|
344
|
+
};
|
|
345
|
+
reviewStore.set(parsed.review);
|
|
346
|
+
return {
|
|
347
|
+
ok: true,
|
|
348
|
+
review: parsed.review
|
|
349
|
+
};
|
|
350
|
+
} catch (cause) {
|
|
351
|
+
return {
|
|
352
|
+
ok: false,
|
|
353
|
+
error: cause instanceof Error ? cause.message : String(cause)
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region src/client/PlanView.tsx
|
|
359
|
+
/**
|
|
360
|
+
* The Plan tab view: a status header (title, path, actions), the review
|
|
361
|
+
* action bar (the sidebar approval surface — the chat shows no popup), and
|
|
362
|
+
* the plan rendered through DSH's shared `MarkdownText`.
|
|
363
|
+
*
|
|
364
|
+
* The file content comes from better-sidebar's `/sidebar/api/fs.read` route
|
|
365
|
+
* (same-origin, browser-authenticated) — the plan file lives in the session
|
|
366
|
+
* workspace, and the host half has no route of its own for reading it. The
|
|
367
|
+
* path is `tab.meta.path` (persisted with the tab, so a refresh restores the
|
|
368
|
+
* view) falling back to `tab.path`.
|
|
369
|
+
*
|
|
370
|
+
* The review state comes from the delivery WebSocket's review frames (via
|
|
371
|
+
* the shared review store, restored by the attach replay after a refresh).
|
|
372
|
+
* While a review is pending the bar offers Approve / Execute in new chat /
|
|
373
|
+
* Keep planning; a decision POSTs to the host's review route and the echo
|
|
374
|
+
* updates the store. The delegation choice then launches the execution
|
|
375
|
+
* conversation through the sessions service (see execution-launch.ts) and
|
|
376
|
+
* navigates there.
|
|
377
|
+
*
|
|
378
|
+
* @module @huanlin/dsh-plugin-better-plan/client/PlanView
|
|
379
|
+
*/
|
|
380
|
+
/**
|
|
381
|
+
* Extract the plan path from a tab (meta first, then the seed path).
|
|
382
|
+
* @param tab - the sidebar tab instance.
|
|
383
|
+
* @returns the plan file path, or undefined when the tab carries none.
|
|
384
|
+
*/
|
|
385
|
+
function planPathOf(tab) {
|
|
386
|
+
const meta = tab.meta;
|
|
387
|
+
if (meta !== null && typeof meta === "object" && typeof meta.path === "string" && meta.path !== "") return meta.path;
|
|
388
|
+
return typeof tab.path === "string" && tab.path !== "" ? tab.path : void 0;
|
|
389
|
+
}
|
|
390
|
+
/** Read the sidebar fs API for one session-scoped path. */
|
|
391
|
+
async function fsRead(sessionId, path, signal) {
|
|
392
|
+
const response = await fetch("/sidebar/api/fs.read", {
|
|
393
|
+
method: "POST",
|
|
394
|
+
headers: { "content-type": "application/json" },
|
|
395
|
+
body: JSON.stringify({
|
|
396
|
+
sessionId,
|
|
397
|
+
path
|
|
398
|
+
}),
|
|
399
|
+
signal
|
|
400
|
+
});
|
|
401
|
+
const parsed = await response.json().catch(() => null);
|
|
402
|
+
if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === void 0) throw new Error(parsed?.error?.message ?? `HTTP ${response.status}`);
|
|
403
|
+
const value = parsed.value;
|
|
404
|
+
if (value.kind === "binary") throw new Error(t("binaryFile"));
|
|
405
|
+
if (typeof value.content !== "string") throw new Error(t("unexpectedRead"));
|
|
406
|
+
return {
|
|
407
|
+
content: value.content,
|
|
408
|
+
truncated: value.truncated === true
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/** Shared style tokens (alias tokens with plain fallbacks, no hardcoded theme). */
|
|
412
|
+
const styles = {
|
|
413
|
+
root: {
|
|
414
|
+
display: "flex",
|
|
415
|
+
flexDirection: "column",
|
|
416
|
+
height: "100%",
|
|
417
|
+
minHeight: 0,
|
|
418
|
+
overflow: "hidden"
|
|
419
|
+
},
|
|
420
|
+
header: {
|
|
421
|
+
display: "flex",
|
|
422
|
+
flexDirection: "column",
|
|
423
|
+
gap: 4,
|
|
424
|
+
padding: "10px 14px",
|
|
425
|
+
borderBottom: "1px solid var(--ds-border, rgba(127,127,127,0.25))",
|
|
426
|
+
background: "var(--dsw-alias-bg-layer-1, transparent)"
|
|
427
|
+
},
|
|
428
|
+
titleRow: {
|
|
429
|
+
display: "flex",
|
|
430
|
+
alignItems: "center",
|
|
431
|
+
gap: 8,
|
|
432
|
+
minWidth: 0
|
|
433
|
+
},
|
|
434
|
+
title: {
|
|
435
|
+
fontSize: 13,
|
|
436
|
+
fontWeight: 600,
|
|
437
|
+
overflow: "hidden",
|
|
438
|
+
textOverflow: "ellipsis",
|
|
439
|
+
whiteSpace: "nowrap"
|
|
440
|
+
},
|
|
441
|
+
actions: {
|
|
442
|
+
display: "flex",
|
|
443
|
+
gap: 6,
|
|
444
|
+
marginLeft: "auto",
|
|
445
|
+
flexShrink: 0
|
|
446
|
+
},
|
|
447
|
+
button: {
|
|
448
|
+
fontSize: 11,
|
|
449
|
+
padding: "3px 8px",
|
|
450
|
+
borderRadius: 6,
|
|
451
|
+
cursor: "pointer",
|
|
452
|
+
border: "1px solid var(--ds-border, rgba(127,127,127,0.35))",
|
|
453
|
+
background: "transparent",
|
|
454
|
+
color: "inherit"
|
|
455
|
+
},
|
|
456
|
+
primaryButton: {
|
|
457
|
+
fontSize: 12,
|
|
458
|
+
padding: "5px 12px",
|
|
459
|
+
borderRadius: 6,
|
|
460
|
+
cursor: "pointer",
|
|
461
|
+
fontWeight: 600,
|
|
462
|
+
border: "1px solid var(--ds-border-strong, var(--ds-border, rgba(127,127,127,0.5)))",
|
|
463
|
+
background: "transparent",
|
|
464
|
+
color: "inherit"
|
|
465
|
+
},
|
|
466
|
+
path: {
|
|
467
|
+
fontSize: 11,
|
|
468
|
+
fontFamily: "var(--ds-font-mono, ui-monospace, monospace)",
|
|
469
|
+
color: "var(--ds-text-muted, var(--ds-text-3, #888))",
|
|
470
|
+
overflow: "hidden",
|
|
471
|
+
textOverflow: "ellipsis",
|
|
472
|
+
whiteSpace: "nowrap"
|
|
473
|
+
},
|
|
474
|
+
reviewBar: {
|
|
475
|
+
display: "flex",
|
|
476
|
+
flexDirection: "column",
|
|
477
|
+
gap: 8,
|
|
478
|
+
padding: "10px 14px",
|
|
479
|
+
borderBottom: "1px solid var(--ds-border, rgba(127,127,127,0.25))",
|
|
480
|
+
background: "var(--dsw-alias-bg-layer-2, transparent)"
|
|
481
|
+
},
|
|
482
|
+
reviewText: {
|
|
483
|
+
fontSize: 12,
|
|
484
|
+
lineHeight: 1.55
|
|
485
|
+
},
|
|
486
|
+
reviewInput: {
|
|
487
|
+
fontSize: 12,
|
|
488
|
+
padding: "5px 8px",
|
|
489
|
+
borderRadius: 6,
|
|
490
|
+
border: "1px solid var(--ds-border, rgba(127,127,127,0.35))",
|
|
491
|
+
background: "transparent",
|
|
492
|
+
color: "inherit",
|
|
493
|
+
width: "100%",
|
|
494
|
+
boxSizing: "border-box"
|
|
495
|
+
},
|
|
496
|
+
reviewButtons: {
|
|
497
|
+
display: "flex",
|
|
498
|
+
gap: 8
|
|
499
|
+
},
|
|
500
|
+
reviewError: {
|
|
501
|
+
fontSize: 11,
|
|
502
|
+
color: "var(--ds-text-critical, #b3261e)"
|
|
503
|
+
},
|
|
504
|
+
reviewStatus: {
|
|
505
|
+
padding: "8px 14px",
|
|
506
|
+
fontSize: 12,
|
|
507
|
+
lineHeight: 1.55,
|
|
508
|
+
borderBottom: "1px solid var(--ds-border, rgba(127,127,127,0.25))"
|
|
509
|
+
},
|
|
510
|
+
body: {
|
|
511
|
+
flex: 1,
|
|
512
|
+
minHeight: 0,
|
|
513
|
+
overflow: "auto",
|
|
514
|
+
padding: "14px 16px",
|
|
515
|
+
fontSize: 13,
|
|
516
|
+
lineHeight: 1.65
|
|
517
|
+
},
|
|
518
|
+
notice: {
|
|
519
|
+
padding: "14px 16px",
|
|
520
|
+
fontSize: 12,
|
|
521
|
+
lineHeight: 1.6
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
/** The one-line status a settled review renders (cancelled stays quiet). */
|
|
525
|
+
function settledReviewText(status, delegating) {
|
|
526
|
+
if (status === "approved") return t("approvedStatus");
|
|
527
|
+
if (status === "delegated") return delegating ? t("delegatingStatus") : t("delegatedStatus");
|
|
528
|
+
if (status === "kept") return t("keptStatus");
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* The Plan tab component (better-sidebar TabDescriptor.component).
|
|
533
|
+
* @param props - the tab component props (ctx/store/scope/tab/visible).
|
|
534
|
+
* @returns the rendered view.
|
|
535
|
+
*/
|
|
536
|
+
function PlanView(props) {
|
|
537
|
+
const { ctx, scope, tab } = props;
|
|
538
|
+
const path = planPathOf(tab);
|
|
539
|
+
const [load, setLoad] = (0, react.useState)({ status: "loading" });
|
|
540
|
+
const [attempt, setAttempt] = (0, react.useState)(0);
|
|
541
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
542
|
+
const review = (0, react.useSyncExternalStore)(reviewStore.subscribe, reviewStore.get);
|
|
543
|
+
const [feedback, setFeedback] = (0, react.useState)("");
|
|
544
|
+
const [submitting, setSubmitting] = (0, react.useState)(false);
|
|
545
|
+
const [submitError, setSubmitError] = (0, react.useState)(void 0);
|
|
546
|
+
const [delegating, setDelegating] = (0, react.useState)(false);
|
|
547
|
+
const [delegateError, setDelegateError] = (0, react.useState)(void 0);
|
|
548
|
+
const sessions = ctx.get("sessions");
|
|
549
|
+
const pendingReview = review !== null && review.status === "pending" && review.path === path ? review : null;
|
|
550
|
+
const settledReview = review !== null && review.path === path && review.status !== "pending" ? settledReviewText(review.status, delegating) : null;
|
|
551
|
+
(0, react.useEffect)(() => {
|
|
552
|
+
setFeedback("");
|
|
553
|
+
setSubmitError(void 0);
|
|
554
|
+
setDelegateError(void 0);
|
|
555
|
+
}, [review?.id]);
|
|
556
|
+
(0, react.useEffect)(() => {
|
|
557
|
+
fetchReviewState(scope.sessionId);
|
|
558
|
+
}, [scope.sessionId]);
|
|
559
|
+
(0, react.useEffect)(() => {
|
|
560
|
+
if (path === void 0) {
|
|
561
|
+
setLoad({
|
|
562
|
+
status: "error",
|
|
563
|
+
message: t("noPath")
|
|
564
|
+
});
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const controller = new AbortController();
|
|
568
|
+
setLoad({ status: "loading" });
|
|
569
|
+
fsRead(scope.sessionId, path, controller.signal).then((result) => {
|
|
570
|
+
setLoad({
|
|
571
|
+
status: "ok",
|
|
572
|
+
...result
|
|
573
|
+
});
|
|
574
|
+
}).catch((cause) => {
|
|
575
|
+
if (controller.signal.aborted) return;
|
|
576
|
+
setLoad({
|
|
577
|
+
status: "error",
|
|
578
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
return () => {
|
|
582
|
+
controller.abort();
|
|
583
|
+
};
|
|
584
|
+
}, [
|
|
585
|
+
path,
|
|
586
|
+
scope.sessionId,
|
|
587
|
+
attempt
|
|
588
|
+
]);
|
|
589
|
+
const betterSidebar = ctx.get("betterSidebar");
|
|
590
|
+
const canOpenInEditor = path !== void 0 && Array.isArray(betterSidebar?.features) === true && betterSidebar.features.includes("openFile") === true;
|
|
591
|
+
const copyPath = (0, react.useCallback)(() => {
|
|
592
|
+
if (path === void 0) return;
|
|
593
|
+
(0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(path);
|
|
594
|
+
setCopied(true);
|
|
595
|
+
window.setTimeout(() => {
|
|
596
|
+
setCopied(false);
|
|
597
|
+
}, 1500);
|
|
598
|
+
}, [path]);
|
|
599
|
+
const decide = (0, react.useCallback)((decision) => {
|
|
600
|
+
if (pendingReview === null) return;
|
|
601
|
+
setSubmitting(true);
|
|
602
|
+
setSubmitError(void 0);
|
|
603
|
+
submitReviewDecision(scope.sessionId, decision, decision === "keep" ? feedback : void 0, pendingReview.id).then((outcome) => {
|
|
604
|
+
if (outcome.ok === false) {
|
|
605
|
+
setSubmitError(outcome.error);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (decision !== "approve_new_session") return;
|
|
609
|
+
if (sessions === void 0) return;
|
|
610
|
+
setDelegating(true);
|
|
611
|
+
launchExecutionConversation(sessions, pendingReview.path, scope.sessionId).catch((cause) => {
|
|
612
|
+
setDelegateError(`${t("errDelegateFailed")}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
613
|
+
}).finally(() => {
|
|
614
|
+
setDelegating(false);
|
|
615
|
+
});
|
|
616
|
+
}).finally(() => {
|
|
617
|
+
setSubmitting(false);
|
|
618
|
+
});
|
|
619
|
+
}, [
|
|
620
|
+
pendingReview,
|
|
621
|
+
scope.sessionId,
|
|
622
|
+
feedback,
|
|
623
|
+
sessions
|
|
624
|
+
]);
|
|
625
|
+
return (0, react.createElement)("div", { style: styles.root }, (0, react.createElement)("div", { style: styles.header }, (0, react.createElement)("div", { style: styles.titleRow }, (0, react.createElement)("span", {
|
|
626
|
+
style: styles.title,
|
|
627
|
+
title: path ?? void 0
|
|
628
|
+
}, tab.title), (0, react.createElement)("span", { style: styles.actions }, canOpenInEditor && betterSidebar !== void 0 && path !== void 0 ? (0, react.createElement)("button", {
|
|
629
|
+
style: styles.button,
|
|
630
|
+
onClick: () => {
|
|
631
|
+
betterSidebar.openFile(scope, path);
|
|
632
|
+
}
|
|
633
|
+
}, t("openInEditor")) : null, path !== void 0 ? (0, react.createElement)("button", {
|
|
634
|
+
style: styles.button,
|
|
635
|
+
onClick: copyPath
|
|
636
|
+
}, copied ? t("copied") : t("copyPath")) : null)), path !== void 0 ? (0, react.createElement)("div", { style: styles.path }, path) : null), pendingReview !== null ? (0, react.createElement)("div", { style: styles.reviewBar }, (0, react.createElement)("div", { style: styles.reviewText }, t("reviewHint")), (0, react.createElement)("input", {
|
|
637
|
+
style: styles.reviewInput,
|
|
638
|
+
value: feedback,
|
|
639
|
+
placeholder: t("feedbackPlaceholder"),
|
|
640
|
+
onChange: (event) => {
|
|
641
|
+
setFeedback(event.target.value);
|
|
642
|
+
}
|
|
643
|
+
}), (0, react.createElement)("div", { style: styles.reviewButtons }, (0, react.createElement)("button", {
|
|
644
|
+
style: styles.primaryButton,
|
|
645
|
+
disabled: submitting,
|
|
646
|
+
onClick: () => {
|
|
647
|
+
decide("approve");
|
|
648
|
+
}
|
|
649
|
+
}, t("approve")), sessions !== void 0 ? (0, react.createElement)("button", {
|
|
650
|
+
style: styles.primaryButton,
|
|
651
|
+
disabled: submitting,
|
|
652
|
+
onClick: () => {
|
|
653
|
+
decide("approve_new_session");
|
|
654
|
+
}
|
|
655
|
+
}, t("approveNewSession")) : null, (0, react.createElement)("button", {
|
|
656
|
+
style: styles.button,
|
|
657
|
+
disabled: submitting,
|
|
658
|
+
onClick: () => {
|
|
659
|
+
decide("keep");
|
|
660
|
+
}
|
|
661
|
+
}, t("keepPlanning"))), submitError !== void 0 ? (0, react.createElement)("div", { style: styles.reviewError }, submitError) : null) : settledReview !== null ? (0, react.createElement)("div", { style: styles.reviewStatus }, settledReview, delegateError !== void 0 ? (0, react.createElement)("div", { style: styles.reviewError }, delegateError) : null) : null, load.status === "loading" ? (0, react.createElement)("div", { style: styles.notice }, t("loading")) : load.status === "error" ? (0, react.createElement)("div", { style: styles.notice }, (0, react.createElement)("div", null, `${t("readFailed")}: ${load.message}`), path !== void 0 ? (0, react.createElement)("button", {
|
|
662
|
+
style: {
|
|
663
|
+
...styles.button,
|
|
664
|
+
marginTop: 8
|
|
665
|
+
},
|
|
666
|
+
onClick: () => {
|
|
667
|
+
setAttempt(attempt + 1);
|
|
668
|
+
}
|
|
669
|
+
}, t("retry")) : null) : (0, react.createElement)("div", { style: styles.body }, (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, { ...markdownTextProps(load.content, {
|
|
670
|
+
copyLabel: t("copy"),
|
|
671
|
+
copiedLabel: t("copiedLabel")
|
|
672
|
+
}) }), load.truncated ? (0, react.createElement)("div", { style: {
|
|
673
|
+
...styles.notice,
|
|
674
|
+
paddingLeft: 0,
|
|
675
|
+
paddingRight: 0
|
|
676
|
+
} }, t("truncated")) : null));
|
|
677
|
+
}
|
|
678
|
+
//#endregion
|
|
679
|
+
//#region src/client/index.tsx
|
|
680
|
+
/**
|
|
681
|
+
* Client half of @huanlin/dsh-plugin-better-plan: registers the sidebar's
|
|
682
|
+
* "Plan" tab through better-sidebar's service and subscribes the per-session
|
|
683
|
+
* delivery WebSocket that opens it and feeds its review action bar.
|
|
684
|
+
*
|
|
685
|
+
* The tab is `single: true` (one Plan tab per session, dedupe-focused on
|
|
686
|
+
* repeat deliveries). Because the dedupe focus does NOT overwrite an already
|
|
687
|
+
* open tab's path, every push is followed by `updateTab` (feature-gated,
|
|
688
|
+
* v0.12.0+) so a re-delivered plan replaces the tab's content, then
|
|
689
|
+
* `activateTab` focuses it. `meta` rides the tab into better-sidebar's
|
|
690
|
+
* localStorage persistence, so a refresh restores the view and PlanView
|
|
691
|
+
* re-reads the file from `tab.meta.path`. Review frames feed the shared
|
|
692
|
+
* review store; the tab's action bar posts decisions back to
|
|
693
|
+
* `POST /better-plan/api/review` — the sidebar IS the approval surface, the
|
|
694
|
+
* chat shows no popup.
|
|
695
|
+
*
|
|
696
|
+
* With better-sidebar absent this half stays pending on its inject (legal
|
|
697
|
+
* per the client runner) and, defensively, apply() skips everything when the
|
|
698
|
+
* service is unreachable — the host half's `delivered: false` path covers
|
|
699
|
+
* the no-sidebar deployment.
|
|
700
|
+
*
|
|
701
|
+
* @module @huanlin/dsh-plugin-better-plan/client
|
|
702
|
+
*/
|
|
703
|
+
/** The Plan tab type id (also the minted tab id — single instance). */
|
|
704
|
+
const TAB_ID = "better-plan:plan";
|
|
705
|
+
/** The delivery WebSocket path (mirror of the host half's route). */
|
|
706
|
+
const DELIVERY_WS_PATH = "/better-plan/ws/delivery";
|
|
707
|
+
/** Reconnect attempts before the loop stops (mirrors the sidebar's socket loops). */
|
|
708
|
+
const FAILURE_LIMIT = 5;
|
|
709
|
+
/**
|
|
710
|
+
* Apply one delivery push to the sidebar: open (or focus) the Plan tab, then
|
|
711
|
+
* overwrite its content via updateTab and focus it (both v0.12.0+; on an
|
|
712
|
+
* older host the plain openTab dedupe-focus still lands the FIRST delivery).
|
|
713
|
+
* @param service - the better-sidebar service.
|
|
714
|
+
* @param payload - the parsed WS frame.
|
|
715
|
+
* @param sessionId - the session the socket is subscribed to.
|
|
716
|
+
*/
|
|
717
|
+
function applyDeliveryPush(service, payload, sessionId) {
|
|
718
|
+
if (payload === null || typeof payload !== "object") return;
|
|
719
|
+
const record = payload;
|
|
720
|
+
if (typeof record.path !== "string" || record.path === "") return;
|
|
721
|
+
const title = typeof record.title === "string" && record.title !== "" ? record.title : void 0;
|
|
722
|
+
const meta = {
|
|
723
|
+
path: record.path,
|
|
724
|
+
deliveredAt: Date.now()
|
|
725
|
+
};
|
|
726
|
+
const scope = { sessionId };
|
|
727
|
+
service.openTab({
|
|
728
|
+
type: TAB_ID,
|
|
729
|
+
id: TAB_ID,
|
|
730
|
+
path: record.path,
|
|
731
|
+
title,
|
|
732
|
+
meta
|
|
733
|
+
}, scope);
|
|
734
|
+
const features = service.features;
|
|
735
|
+
if (Array.isArray(features) === true && features.includes("updateTab") === true) {
|
|
736
|
+
service.updateTab(TAB_ID, {
|
|
737
|
+
...title !== void 0 ? { title } : {},
|
|
738
|
+
path: record.path,
|
|
739
|
+
meta
|
|
740
|
+
});
|
|
741
|
+
service.activateTab(TAB_ID, scope);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Route one WS frame: `deliver` opens/updates the Plan tab, `review` feeds
|
|
746
|
+
* the review store (the Plan tab's action bar). Malformed frames are ignored
|
|
747
|
+
* (the next push carries its own state).
|
|
748
|
+
* @param service - the better-sidebar service.
|
|
749
|
+
* @param store - the review store the review frames feed.
|
|
750
|
+
* @param frame - the parsed WS frame.
|
|
751
|
+
* @param sessionId - the session the socket is subscribed to.
|
|
752
|
+
*/
|
|
753
|
+
function applyDeliveryFrame(service, store, frame, sessionId) {
|
|
754
|
+
if (frame === null || typeof frame !== "object") return;
|
|
755
|
+
const kind = frame.kind;
|
|
756
|
+
if (kind === "review") {
|
|
757
|
+
const review = frame.review;
|
|
758
|
+
if (review === null) store.set(null);
|
|
759
|
+
else if (isReviewState(review)) store.set(review);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (kind === "deliver") applyDeliveryPush(service, frame, sessionId);
|
|
763
|
+
}
|
|
764
|
+
/** The betterSidebar and locale services this half resolves through the context proxy. */
|
|
765
|
+
const inject = ["betterSidebar", "locale"];
|
|
766
|
+
/**
|
|
767
|
+
* Client plugin body.
|
|
768
|
+
* @param ctx - the client cordis context.
|
|
769
|
+
*/
|
|
770
|
+
function apply(ctx) {
|
|
771
|
+
const betterSidebar = ctx.betterSidebar;
|
|
772
|
+
if (betterSidebar === void 0) return;
|
|
773
|
+
attachLocale(ctx.locale);
|
|
774
|
+
ctx.effect(() => {
|
|
775
|
+
const offZh = ctx.locale.register(LOCALE_NS, "zh", zhDict);
|
|
776
|
+
const offEn = ctx.locale.register(LOCALE_NS, "en", enDict);
|
|
777
|
+
return () => {
|
|
778
|
+
offZh();
|
|
779
|
+
offEn();
|
|
780
|
+
};
|
|
781
|
+
}, "dsh-plugin-better-plan: locale dictionaries");
|
|
782
|
+
ctx.effect(() => betterSidebar.registerTab({
|
|
783
|
+
id: TAB_ID,
|
|
784
|
+
title: () => t("tabTitle"),
|
|
785
|
+
icon: (size) => (0, react.createElement)(IconPlanOutline16, { size }),
|
|
786
|
+
order: 15,
|
|
787
|
+
single: true,
|
|
788
|
+
component: (props) => (0, react.createElement)(PlanView, props)
|
|
789
|
+
}), "dsh-plugin-better-plan: plan tab");
|
|
790
|
+
ctx.effect(() => {
|
|
791
|
+
let socket = null;
|
|
792
|
+
let retry;
|
|
793
|
+
let disposed = false;
|
|
794
|
+
let failures = 0;
|
|
795
|
+
let current;
|
|
796
|
+
const clearRetry = () => {
|
|
797
|
+
if (retry !== void 0) {
|
|
798
|
+
window.clearTimeout(retry);
|
|
799
|
+
retry = void 0;
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
const connect = (sessionId) => {
|
|
803
|
+
if (disposed) return;
|
|
804
|
+
clearRetry();
|
|
805
|
+
socket?.close();
|
|
806
|
+
const url = new URL(DELIVERY_WS_PATH, window.location.origin);
|
|
807
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
808
|
+
url.search = new URLSearchParams({
|
|
809
|
+
session: sessionId,
|
|
810
|
+
locale: activeLocale()
|
|
811
|
+
}).toString();
|
|
812
|
+
socket = new WebSocket(url.toString());
|
|
813
|
+
socket.onmessage = (event) => {
|
|
814
|
+
if (typeof event.data !== "string") return;
|
|
815
|
+
try {
|
|
816
|
+
applyDeliveryFrame(betterSidebar, reviewStore, JSON.parse(event.data), sessionId);
|
|
817
|
+
} catch {}
|
|
818
|
+
};
|
|
819
|
+
socket.onclose = () => {
|
|
820
|
+
if (disposed || current === void 0) return;
|
|
821
|
+
failures += 1;
|
|
822
|
+
if (failures >= FAILURE_LIMIT) {
|
|
823
|
+
console.error("[dsh-plugin-better-plan] delivery socket failed; stopping reconnect loop", current);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
retry = window.setTimeout(() => {
|
|
827
|
+
if (current !== void 0) connect(current);
|
|
828
|
+
}, 2e3);
|
|
829
|
+
};
|
|
830
|
+
socket.onerror = () => {
|
|
831
|
+
socket?.close();
|
|
832
|
+
};
|
|
833
|
+
};
|
|
834
|
+
const sync = () => {
|
|
835
|
+
const sessionId = betterSidebar.getSnapshot().sessionId;
|
|
836
|
+
if (sessionId === current) return;
|
|
837
|
+
current = sessionId;
|
|
838
|
+
reviewStore.reset();
|
|
839
|
+
failures = 0;
|
|
840
|
+
if (sessionId === void 0) {
|
|
841
|
+
clearRetry();
|
|
842
|
+
socket?.close();
|
|
843
|
+
socket = null;
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
connect(sessionId);
|
|
847
|
+
};
|
|
848
|
+
sync();
|
|
849
|
+
const unsubscribe = betterSidebar.subscribeState(sync);
|
|
850
|
+
return () => {
|
|
851
|
+
disposed = true;
|
|
852
|
+
unsubscribe();
|
|
853
|
+
clearRetry();
|
|
854
|
+
socket?.close();
|
|
855
|
+
};
|
|
856
|
+
}, "dsh-plugin-better-plan: delivery socket");
|
|
857
|
+
}
|
|
858
|
+
//#endregion
|
|
859
|
+
exports.DELIVERY_WS_PATH = DELIVERY_WS_PATH;
|
|
860
|
+
exports.TAB_ID = TAB_ID;
|
|
861
|
+
exports.apply = apply;
|
|
862
|
+
exports.applyDeliveryFrame = applyDeliveryFrame;
|
|
863
|
+
exports.applyDeliveryPush = applyDeliveryPush;
|
|
864
|
+
exports.inject = inject;
|
|
865
|
+
return module.exports;
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
//# sourceMappingURL=client.js.map
|