dsh-plugin-workbench 0.0.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/lib/index.js ADDED
@@ -0,0 +1,179 @@
1
+ //#region src/index.ts
2
+ const name = "dsh-plugin-workbench";
3
+ const inject = ["fs", "connection"];
4
+ /** Loopback-only logical RPC channel. */
5
+ const CHANNEL = "/dsh-plugin-files";
6
+ /** Files larger than this are never read for preview (client shows size + hint). */
7
+ const MAX_PREVIEW_BYTES = 524288;
8
+ /** Map an fs entry type onto the wire `kind` union. */
9
+ function kindOf(type) {
10
+ if (type === "directory") return "dir";
11
+ if (type === "file") return "file";
12
+ return "other";
13
+ }
14
+ /** Project a raw fs dir entry onto the wire-safe row shape. */
15
+ function mapDirEntry(entry, processPath) {
16
+ const kind = kindOf(entry.type);
17
+ return {
18
+ name: entry.name,
19
+ path: processPath(entry.target),
20
+ kind,
21
+ ...kind === "file" && entry.size !== void 0 ? { size: entry.size } : {}
22
+ };
23
+ }
24
+ /** Directories first, then case-insensitive name sort (numeric-aware). */
25
+ function sortEntries(entries) {
26
+ return [...entries].sort((a, b) => {
27
+ const ad = a.kind === "dir" ? 0 : 1;
28
+ const bd = b.kind === "dir" ? 0 : 1;
29
+ if (ad !== bd) return ad - bd;
30
+ return a.name.localeCompare(b.name, void 0, {
31
+ sensitivity: "base",
32
+ numeric: true
33
+ });
34
+ });
35
+ }
36
+ const FS_ERROR_MESSAGES = {
37
+ FS_NOT_FOUND: "path does not exist",
38
+ FS_NOT_DIRECTORY: "not a directory",
39
+ FS_NOT_REGULAR_FILE: "not a regular file",
40
+ FS_NOT_TEXT: "binary file",
41
+ FS_TOO_LARGE: "file too large",
42
+ FS_PERMISSION_DENIED: "permission denied",
43
+ FS_SANDBOX_DENIED: "sandbox denied",
44
+ FS_ABORTED: "aborted",
45
+ FS_IO_ERROR: "io error"
46
+ };
47
+ /** Human-readable message for a thrown value, honoring the fs error code taxonomy. */
48
+ function mapError(error) {
49
+ if (error instanceof Error) {
50
+ const code = error.code;
51
+ if (typeof code === "string" && FS_ERROR_MESSAGES[code] !== void 0) return FS_ERROR_MESSAGES[code];
52
+ return error.message;
53
+ }
54
+ return String(error);
55
+ }
56
+ function isFsErrorCode(error, code) {
57
+ return error instanceof Error && error.code === code;
58
+ }
59
+ function fail(message) {
60
+ return {
61
+ ok: false,
62
+ error: {
63
+ code: "internal",
64
+ message,
65
+ details: {}
66
+ }
67
+ };
68
+ }
69
+ function pathOf(payload) {
70
+ if (typeof payload === "object" && payload !== null) {
71
+ const path = payload.path;
72
+ if (typeof path === "string" && path.trim().length > 0) return path;
73
+ }
74
+ }
75
+ /**
76
+ * One filesystem-backed RPC endpoint pair. Reads never mutate; `signal`
77
+ * cancels the underlying fs call (or aborts between steps).
78
+ */
79
+ function apply(ctx) {
80
+ const handler = async (endpoint, payload, signal) => {
81
+ if (endpoint === "list") return listDir(ctx, payload, signal);
82
+ if (endpoint === "read") return readFile(ctx, payload, signal);
83
+ if (endpoint === "write") return writeFile(ctx, payload, signal);
84
+ return fail(`unknown endpoint: ${endpoint}`);
85
+ };
86
+ ctx.connection.rpc.handle(CHANNEL, handler, { authority: "loopback" });
87
+ }
88
+ async function listDir(ctx, payload, signal) {
89
+ const path = pathOf(payload);
90
+ if (path === void 0) return fail("list: payload.path must be a non-empty string");
91
+ try {
92
+ const target = await ctx.fs.resolve(path, { signal });
93
+ const info = await ctx.fs.stat(target, signal);
94
+ if (info === void 0) return fail(`path not found: ${path}`);
95
+ if (info.type !== "directory") return fail(`not a directory: ${path}`);
96
+ const entries = sortEntries((await ctx.fs.listDir(target, signal)).map((entry) => mapDirEntry(entry, (t) => ctx.fs.processPath(t))));
97
+ return {
98
+ ok: true,
99
+ value: {
100
+ root: ctx.fs.processPath(target),
101
+ entries
102
+ }
103
+ };
104
+ } catch (error) {
105
+ return fail(mapError(error));
106
+ }
107
+ }
108
+ async function readFile(ctx, payload, signal) {
109
+ const path = pathOf(payload);
110
+ if (path === void 0) return fail("read: payload.path must be a non-empty string");
111
+ try {
112
+ const target = await ctx.fs.resolve(path, { signal });
113
+ const info = await ctx.fs.stat(target, signal);
114
+ if (info === void 0) return fail(`path not found: ${path}`);
115
+ if (info.type !== "file") return fail(`not a regular file: ${path}`);
116
+ const resolvedPath = ctx.fs.processPath(target);
117
+ const size = info.size ?? 0;
118
+ if (size > 524288) return {
119
+ ok: true,
120
+ value: {
121
+ path: resolvedPath,
122
+ content: "",
123
+ size,
124
+ binary: false,
125
+ truncated: true
126
+ }
127
+ };
128
+ try {
129
+ return {
130
+ ok: true,
131
+ value: {
132
+ path: resolvedPath,
133
+ content: await ctx.fs.readText(target, signal),
134
+ size,
135
+ binary: false,
136
+ truncated: false
137
+ }
138
+ };
139
+ } catch (error) {
140
+ if (isFsErrorCode(error, "FS_NOT_TEXT")) return {
141
+ ok: true,
142
+ value: {
143
+ path: resolvedPath,
144
+ content: "",
145
+ size,
146
+ binary: true,
147
+ truncated: false
148
+ }
149
+ };
150
+ throw error;
151
+ }
152
+ } catch (error) {
153
+ return fail(mapError(error));
154
+ }
155
+ }
156
+ async function writeFile(ctx, payload, signal) {
157
+ const path = pathOf(payload);
158
+ const content = typeof payload === "object" && payload !== null ? payload.content : void 0;
159
+ if (path === void 0) return fail("write: payload.path must be a non-empty string");
160
+ if (typeof content !== "string") return fail("write: payload.content must be a string");
161
+ try {
162
+ const target = await ctx.fs.resolve(path, { signal });
163
+ await ctx.fs.writeText(target, content, void 0, signal, {
164
+ mode: "danger-full-access",
165
+ workspaceRoot: ctx.fs.processPath(target)
166
+ });
167
+ return {
168
+ ok: true,
169
+ value: {
170
+ path: ctx.fs.processPath(target),
171
+ size: content.length
172
+ }
173
+ };
174
+ } catch (error) {
175
+ return fail(mapError(error));
176
+ }
177
+ }
178
+ //#endregion
179
+ export { CHANNEL, MAX_PREVIEW_BYTES, apply, inject, kindOf, mapDirEntry, mapError, name, sortEntries };
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "dsh-plugin-workbench",
3
+ "description": "VS Code-style workspace file explorer + editable preview for the dsh web GUI",
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "packageManager": "pnpm@11.21.0",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "main": "lib/index.js",
11
+ "exports": {
12
+ ".": "./lib/index.js",
13
+ "./client": "./lib/client.js",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "dsh": {
17
+ "bundle": {
18
+ "patch": "./cordis.patch.yml"
19
+ },
20
+ "client": {
21
+ "inject": [],
22
+ "platform": "web"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsdown",
27
+ "watch": "tsdown --watch",
28
+ "typecheck": "tsc --noEmit",
29
+ "prepare": "pnpm run build"
30
+ },
31
+ "license": "MIT",
32
+ "author": "Pasumao <1830316810@qq.com>",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/Pasumao/dsh-plugin-workbench.git"
36
+ },
37
+ "homepage": "https://github.com/Pasumao/dsh-plugin-workbench",
38
+ "bugs": {
39
+ "url": "https://github.com/Pasumao/dsh-plugin-workbench/issues"
40
+ },
41
+ "keywords": [
42
+ "dsh",
43
+ "deepseek-harness",
44
+ "cordis",
45
+ "plugin",
46
+ "file-explorer",
47
+ "vscode",
48
+ "workbench"
49
+ ],
50
+ "peerDependencies": {
51
+ "@deepseek-ai/cordis": "^4.0.1"
52
+ },
53
+ "devDependencies": {
54
+ "@deepseek-ai/cordis": "^4.0.1",
55
+ "@types/node": "^26.2.0",
56
+ "@types/react": "~18.3.1",
57
+ "highlight.js": "^11.11.1",
58
+ "lightningcss": "^1.32.0",
59
+ "react": "^18.2.0",
60
+ "tsdown": "0.22.14",
61
+ "typescript": "^5.6.0"
62
+ },
63
+ "files": [
64
+ "lib",
65
+ "scripts",
66
+ "cordis.patch.yml",
67
+ "src",
68
+ "README.md",
69
+ "CHANGELOG.md"
70
+ ]
71
+ }
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Patch the compiled dsh-client-ui-layout client bundle to add a fourth
4
+ * "explorer" column (sidebar | explorer | center | details), its slot
5
+ * declaration, the layout-store panel, and a drag handle.
6
+ *
7
+ * The bundle is a compiled artifact (no source checkout on this machine), so
8
+ * this script performs precise, idempotent string replacements and verifies
9
+ * every anchor landed. It backs up the original before the first write.
10
+ *
11
+ * Usage:
12
+ * node scripts/patch-layout.mjs [--target <abs path to client.js>] [--force]
13
+ *
14
+ * Default target resolves the profile junction to its npx-cache copy:
15
+ * <DSH_HOME>/profiles/node_modules/@deepseek-ai/dsh-client-ui-layout/lib/client.js
16
+ */
17
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
18
+ import { basename, dirname, join, resolve } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+ import { homedir } from 'node:os'
21
+
22
+ const REPO_ROOT = fileURLToPath(new URL('..', import.meta.url))
23
+ const BACKUP_DIR = join(REPO_ROOT, 'patches', 'layout.backup')
24
+
25
+ const args = process.argv.slice(2)
26
+ const targetArg = args.includes('--target') ? args[args.indexOf('--target') + 1] : undefined
27
+ const force = args.includes('--force')
28
+
29
+ const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh')
30
+ const defaultTarget = join(dshHome, 'profiles', 'node_modules', '@deepseek-ai', 'dsh-client-ui-layout', 'lib', 'client.js')
31
+
32
+ /** @param {string[]} lines */
33
+ const L = (...lines) => lines.join('\n')
34
+
35
+ /** One precise replacement: `anchor` must occur exactly once in the file. */
36
+ const REPLACEMENTS = [
37
+ {
38
+ id: 'css.explorerCol.rule',
39
+ anchor: '.pI_x6G_detailsCol{border-left:1px solid var(--dsw-alias-border-l2);min-width:0;overflow:hidden}',
40
+ replacement: '.pI_x6G_detailsCol{border-left:1px solid var(--dsw-alias-border-l2);min-width:0;overflow:hidden}.pI_x6G_explorerCol{background:var(--dsw-specific-sidebar-fill);border-right:1px solid var(--dsw-alias-border-l1);min-width:0;overflow:hidden}',
41
+ },
42
+ {
43
+ id: 'css.explorerCol.handleContent',
44
+ anchor: '.pI_x6G_handle[data-side=details]:after{',
45
+ replacement: '.pI_x6G_handle[data-side=details]:after,.pI_x6G_handle[data-side=explorer]:after{',
46
+ },
47
+ {
48
+ id: 'css.explorerCol.handleOpacity',
49
+ anchor: '.pI_x6G_detailsCol:hover~.pI_x6G_handle[data-side=details]:after,.pI_x6G_handle[data-side=details]:hover:after,.pI_x6G_handle[data-side=details][data-dragging=true]:after{opacity:1}',
50
+ replacement: '.pI_x6G_detailsCol:hover~.pI_x6G_handle[data-side=details]:after,.pI_x6G_handle[data-side=details]:hover:after,.pI_x6G_handle[data-side=details][data-dragging=true]:after{opacity:1}.pI_x6G_explorerCol:hover~.pI_x6G_handle[data-side=explorer]:after,.pI_x6G_handle[data-side=explorer]:hover:after,.pI_x6G_handle[data-side=explorer][data-dragging=true]:after{opacity:1}',
51
+ },
52
+ {
53
+ id: 'css.explorerCol.handleHover',
54
+ anchor: '.pI_x6G_handle[data-side=details]:hover:after,.pI_x6G_handle[data-side=details][data-dragging=true]:after{background:var(--dsw-alias-button-floating-hover);border-color:var(--dsw-alias-border-l3)}',
55
+ replacement: '.pI_x6G_handle[data-side=details]:hover:after,.pI_x6G_handle[data-side=details][data-dragging=true]:after{background:var(--dsw-alias-button-floating-hover);border-color:var(--dsw-alias-border-l3)}.pI_x6G_handle[data-side=explorer]:hover:after,.pI_x6G_handle[data-side=explorer][data-dragging=true]:after{background:var(--dsw-alias-button-floating-hover);border-color:var(--dsw-alias-border-l3)}',
56
+ },
57
+ {
58
+ id: 'css.classMap.explorerCol',
59
+ anchor: '"centerCol": "pI_x6G_centerCol"',
60
+ replacement: '"centerCol": "pI_x6G_centerCol",\n\t\t\t"explorerCol": "pI_x6G_explorerCol"',
61
+ },
62
+ {
63
+ id: 'computeColumns',
64
+ anchor: L(
65
+ 'function computeColumns(viewport, sidebar, details) {',
66
+ '\t\t\tconst s = sidebar === 0 ? 56 : clampWidth(sidebar, 264, 420);',
67
+ '\t\t\tconst d0 = details === 0 ? 0 : clampWidth(details, 300, 520);',
68
+ '\t\t\tif (s + d0 + 640 <= viewport) return {',
69
+ '\t\t\t\tsidebar: s,',
70
+ '\t\t\t\tcenter: viewport - s - d0,',
71
+ '\t\t\t\tdetails: d0',
72
+ '\t\t\t};',
73
+ '\t\t\tconst d1 = d0 === 0 ? 0 : Math.max(300, viewport - s - 640);',
74
+ '\t\t\tif (s + d1 + 640 <= viewport) return {',
75
+ '\t\t\t\tsidebar: s,',
76
+ '\t\t\t\tcenter: 640,',
77
+ '\t\t\t\tdetails: d1',
78
+ '\t\t\t};',
79
+ '\t\t\treturn {',
80
+ '\t\t\t\tsidebar: s,',
81
+ '\t\t\t\tcenter: Math.max(0, viewport - s),',
82
+ '\t\t\t\tdetails: 0',
83
+ '\t\t\t};',
84
+ '\t\t}',
85
+ ),
86
+ replacement: L(
87
+ 'function computeColumns(viewport, sidebar, explorer, details) {',
88
+ '\t\t\tconst s = sidebar === 0 ? 56 : clampWidth(sidebar, 264, 420);',
89
+ '\t\t\tconst e0 = explorer === 0 ? 0 : clampWidth(explorer, 200, 420);',
90
+ '\t\t\tconst d0 = details === 0 ? 0 : clampWidth(details, 300, 520);',
91
+ '\t\t\tif (s + e0 + d0 + 640 <= viewport) return {',
92
+ '\t\t\t\tsidebar: s,',
93
+ '\t\t\t\texplorer: e0,',
94
+ '\t\t\t\tcenter: viewport - s - e0 - d0,',
95
+ '\t\t\t\tdetails: d0',
96
+ '\t\t\t};',
97
+ '\t\t\tconst d1 = d0 === 0 ? 0 : Math.max(300, viewport - s - e0 - 640);',
98
+ '\t\t\tif (s + e0 + d1 + 640 <= viewport) return {',
99
+ '\t\t\t\tsidebar: s,',
100
+ '\t\t\t\texplorer: e0,',
101
+ '\t\t\t\tcenter: 640,',
102
+ '\t\t\t\tdetails: d1',
103
+ '\t\t\t};',
104
+ '\t\t\treturn {',
105
+ '\t\t\t\tsidebar: s,',
106
+ '\t\t\t\texplorer: e0,',
107
+ '\t\t\t\tcenter: Math.max(0, viewport - s - e0),',
108
+ '\t\t\t\tdetails: 0',
109
+ '\t\t\t};',
110
+ '\t\t}',
111
+ ),
112
+ },
113
+ {
114
+ id: 'store.init.explorer',
115
+ anchor: '\t\t\t\t\tsidebar: 280,\n\t\t\t\t\tdetails: 0,',
116
+ replacement: '\t\t\t\t\tsidebar: 280,\n\t\t\t\t\texplorer: 260,\n\t\t\t\t\tdetails: 0,',
117
+ },
118
+ {
119
+ id: 'store.action.setExplorer',
120
+ anchor: '\t\t\t\t\tsetDetails: (d, px) => {\n\t\t\t\t\t\td.details = clampWidth(px, 300, 520);\n\t\t\t\t\t},',
121
+ replacement: '\t\t\t\t\tsetDetails: (d, px) => {\n\t\t\t\t\t\td.details = clampWidth(px, 300, 520);\n\t\t\t\t\t},\n\t\t\t\t\tsetExplorer: (d, px) => {\n\t\t\t\t\t\td.explorer = clampWidth(px, 200, 420);\n\t\t\t\t\t},',
122
+ },
123
+ {
124
+ id: 'appframe.computeCall',
125
+ anchor: 'const cols = computeColumns(viewport, sidebarCollapsed ? 0 : panels.sidebar === 0 ? 280 : panels.sidebar, detailsSession === void 0 ? 0 : panels.details);',
126
+ replacement: 'const explorerEffective = narrow ? 0 : panels.explorer;\n\t\t\tconst cols = computeColumns(viewport, sidebarCollapsed ? 0 : panels.sidebar === 0 ? 280 : panels.sidebar, explorerEffective, detailsSession === void 0 ? 0 : panels.details);',
127
+ },
128
+ {
129
+ id: 'appframe.explorerBase',
130
+ anchor: 'const detailsBase = (0, react.useRef)(0);',
131
+ replacement: 'const detailsBase = (0, react.useRef)(0);\n\t\t\tconst explorerBase = (0, react.useRef)(0);',
132
+ },
133
+ {
134
+ id: 'appframe.explorerDragCallbacks',
135
+ anchor: L(
136
+ '\t\t\tconst onDetailsDrag = (0, react.useCallback)((dx) => {',
137
+ '\t\t\t\tactions.setDetails(detailsBase.current - dx);',
138
+ '\t\t\t}, [actions]);',
139
+ ),
140
+ replacement: L(
141
+ '\t\t\tconst onDetailsDrag = (0, react.useCallback)((dx) => {',
142
+ '\t\t\t\tactions.setDetails(detailsBase.current - dx);',
143
+ '\t\t\t}, [actions]);',
144
+ '\t\t\tconst onExplorerStart = (0, react.useCallback)(() => {',
145
+ '\t\t\t\texplorerBase.current = colsRef.current.explorer;',
146
+ '\t\t\t\tsetDragging(true);',
147
+ '\t\t\t}, []);',
148
+ '\t\t\tconst onExplorerDrag = (0, react.useCallback)((dx) => {',
149
+ '\t\t\t\tactions.setExplorer(explorerBase.current + dx);',
150
+ '\t\t\t}, [actions]);',
151
+ ),
152
+ },
153
+ {
154
+ id: 'appframe.gridTemplate',
155
+ anchor: 'gridTemplateColumns: `${cols.sidebar}px minmax(0, 1fr) ${cols.details}px`',
156
+ replacement: 'gridTemplateColumns: `${cols.sidebar}px ${cols.explorer}px minmax(0, 1fr) ${cols.details}px`',
157
+ },
158
+ {
159
+ id: 'appframe.dataExplorerCollapsed',
160
+ anchor: '"data-details-collapsed": cols.details === 0 || void 0,',
161
+ replacement: '"data-details-collapsed": cols.details === 0 || void 0,\n\t\t\t\t"data-explorer-collapsed": cols.explorer === 0 || void 0,',
162
+ },
163
+ {
164
+ id: 'appframe.explorerColumn',
165
+ anchor: '\t\t\t\t\t(0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(CenterColumn, { children: renderSlot("conversation", {}) }), (0, react_jsx_runtime.jsx)(DetailsColumn, { children: renderSlot("details", {}) })] }),',
166
+ replacement: L(
167
+ '\t\t\t\t\t(0, react_jsx_runtime.jsx)("div", {',
168
+ '\t\t\t\t\t\tclassName: AppFrame_module_css_default.explorerCol,',
169
+ '\t\t\t\t\t\tchildren: renderSlot("explorer", {',
170
+ '\t\t\t\t\t\t\twidth: cols.explorer',
171
+ '\t\t\t\t\t\t})',
172
+ '\t\t\t\t\t}),',
173
+ '\t\t\t\t\t(0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(CenterColumn, { children: renderSlot("conversation", {}) }), (0, react_jsx_runtime.jsx)(DetailsColumn, { children: renderSlot("details", {}) })] }),',
174
+ ),
175
+ },
176
+ {
177
+ id: 'appframe.explorerHandle',
178
+ anchor: '\t\t\t\t\tcols.details > 0 && (0, react_jsx_runtime.jsx)(DragHandle, {',
179
+ replacement: L(
180
+ '\t\t\t\t\tcols.explorer > 0 && (0, react_jsx_runtime.jsx)(DragHandle, {',
181
+ '\t\t\t\t\t\tside: "explorer",',
182
+ '\t\t\t\t\t\tleft: cols.sidebar + cols.explorer,',
183
+ '\t\t\t\t\t\tonStart: onExplorerStart,',
184
+ '\t\t\t\t\t\tonDrag: onExplorerDrag,',
185
+ '\t\t\t\t\t\tonEnd: onDragEnd',
186
+ '\t\t\t\t\t}),',
187
+ '\t\t\t\t\tcols.details > 0 && (0, react_jsx_runtime.jsx)(DragHandle, {',
188
+ ),
189
+ },
190
+ {
191
+ id: 'apply.children.explorer',
192
+ anchor: L(
193
+ '\t\t\t\t\t\t"sidebar": {',
194
+ '\t\t\t\t\t\t\tkind: "single",',
195
+ '\t\t\t\t\t\t\tscope: "root"',
196
+ '\t\t\t\t\t\t},',
197
+ ),
198
+ replacement: L(
199
+ '\t\t\t\t\t\t"sidebar": {',
200
+ '\t\t\t\t\t\t\tkind: "single",',
201
+ '\t\t\t\t\t\t\tscope: "root"',
202
+ '\t\t\t\t\t\t},',
203
+ '\t\t\t\t\t\t"explorer": {',
204
+ '\t\t\t\t\t\t\tkind: "single",',
205
+ '\t\t\t\t\t\t\tscope: "root"',
206
+ '\t\t\t\t\t\t},',
207
+ ),
208
+ },
209
+ {
210
+ id: 'css.centerSplit',
211
+ anchor: '.pI_x6G_centerCol{flex-direction:column;min-width:0;display:flex;overflow:hidden}',
212
+ replacement: '.pI_x6G_centerCol{flex-direction:row;min-width:0;display:flex;overflow:hidden}.pI_x6G_conversationSeat{flex:1 1 0;min-width:0;overflow:hidden}',
213
+ },
214
+ {
215
+ id: 'css.classMap.conversationSeat',
216
+ anchor: '"centerCol": "pI_x6G_centerCol"',
217
+ replacement: '"centerCol": "pI_x6G_centerCol",\n\t\t\t"conversationSeat": "pI_x6G_conversationSeat"',
218
+ },
219
+ {
220
+ id: 'appframe.centerSplit',
221
+ anchor: '(0, react_jsx_runtime.jsx)(CenterColumn, { children: renderSlot("conversation", {}) })',
222
+ replacement: '(0, react_jsx_runtime.jsx)(CenterColumn, { children: [renderSlot("explorer.preview", {}), (0, react_jsx_runtime.jsx)("div", { className: AppFrame_module_css_default.conversationSeat, children: renderSlot("conversation", {}) })] })',
223
+ },
224
+ {
225
+ id: 'apply.children.explorerPreview',
226
+ anchor: L(
227
+ '\t\t\t\t\t\t"explorer": {',
228
+ '\t\t\t\t\t\t\tkind: "single",',
229
+ '\t\t\t\t\t\t\tscope: "root"',
230
+ '\t\t\t\t\t\t},',
231
+ ),
232
+ replacement: L(
233
+ '\t\t\t\t\t\t"explorer": {',
234
+ '\t\t\t\t\t\t\tkind: "single",',
235
+ '\t\t\t\t\t\t\tscope: "root"',
236
+ '\t\t\t\t\t\t},',
237
+ '\t\t\t\t\t\t"explorer.preview": {',
238
+ '\t\t\t\t\t\t\tkind: "single",',
239
+ '\t\t\t\t\t\t\tscope: "root"',
240
+ '\t\t\t\t\t\t},',
241
+ ),
242
+ },
243
+ ]
244
+
245
+ const PATCHED_MARKERS = ['"explorerCol": "pI_x6G_explorerCol"', 'setExplorer: (d, px) => {', 'renderSlot("explorer"', 'renderSlot("explorer.preview"', 'conversationSeat']
246
+
247
+ function main() {
248
+ const target = resolve(targetArg ?? defaultTarget)
249
+ if (!existsSync(target)) {
250
+ console.error(`[patch-layout] target not found: ${target}`)
251
+ console.error('[patch-layout] is DSH_HOME correct, or pass --target <abs path>?')
252
+ process.exit(1)
253
+ }
254
+
255
+ const real = realpathSync(target)
256
+ const original = readFileSync(real, 'utf8')
257
+
258
+ const alreadyPatched = PATCHED_MARKERS.every((marker) => original.includes(marker))
259
+ if (alreadyPatched && !force) {
260
+ console.log(`[patch-layout] already patched (${real}) — nothing to do.`)
261
+ return
262
+ }
263
+ if (alreadyPatched && force) {
264
+ console.log('[patch-layout] --force: re-patching from the current file. Consider restoring the .orig backup first.')
265
+ }
266
+
267
+ mkdirSync(BACKUP_DIR, { recursive: true })
268
+ const pristine = join(BACKUP_DIR, 'client.js.orig')
269
+ if (!existsSync(pristine)) {
270
+ copyFileSync(real, pristine)
271
+ console.log(`[patch-layout] pristine backup written: ${pristine}`)
272
+ } else {
273
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
274
+ copyFileSync(real, join(BACKUP_DIR, `client.js.${stamp}.bak`))
275
+ }
276
+
277
+ let patched = original
278
+ const failures = []
279
+ for (const item of REPLACEMENTS) {
280
+ const count = countOccurrences(patched, item.anchor)
281
+ if (count === 0) {
282
+ failures.push(`${item.id}: anchor not found`)
283
+ continue
284
+ }
285
+ if (count > 1) {
286
+ failures.push(`${item.id}: anchor found ${count} times (expected exactly 1)`)
287
+ continue
288
+ }
289
+ patched = patched.replace(item.anchor, item.replacement)
290
+ }
291
+
292
+ if (failures.length > 0) {
293
+ console.error('[patch-layout] ABORTED — the bundle does not match the expected compiled output:')
294
+ for (const failure of failures) console.error(` - ${failure}`)
295
+ console.error('[patch-layout] the dsh version may have changed; update scripts/patch-layout.mjs anchors.')
296
+ process.exit(1)
297
+ }
298
+
299
+ const missingMarkers = PATCHED_MARKERS.filter((marker) => !patched.includes(marker))
300
+ if (missingMarkers.length > 0) {
301
+ console.error('[patch-layout] verification failed — missing markers:')
302
+ for (const marker of missingMarkers) console.error(` - ${marker}`)
303
+ process.exit(1)
304
+ }
305
+
306
+ writeFileSync(real, patched, 'utf8')
307
+ console.log(`[patch-layout] patched: ${real}`)
308
+ console.log('[patch-layout] verified: explorerCol class, setExplorer action, explorer slot render.')
309
+ console.log('[patch-layout] restart dsh web to serve the new bundle rev.')
310
+ }
311
+
312
+ function countOccurrences(haystack, needle) {
313
+ if (needle.length === 0) return 0
314
+ let count = 0
315
+ let index = haystack.indexOf(needle)
316
+ while (index !== -1) {
317
+ count += 1
318
+ index = haystack.indexOf(needle, index + needle.length)
319
+ }
320
+ return count
321
+ }
322
+
323
+ main()