dsh-hide-ui 0.0.0-stage → 0.4.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-hide-ui contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,134 @@
1
- # Temporary Holding Version
1
+ # dsh-hide-ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A DeepSeek Harness UI plugin that hides the interface regions you choose on
4
+ right-click, so the wallpaper behind them is visible.
5
+
6
+ ## Choose what to hide
7
+
8
+ `Ctrl` + right-click anywhere opens the surface picker. Toggle any combination:
9
+
10
+ - **Sidebar** - sessions, search, and navigation.
11
+ - **Cards & panels** - menus, popovers, and option panels.
12
+ - **Input & controls** - composer, input bar, and takeover controls.
13
+ - **Conversation text frame** - the message list and scrollable text flow.
14
+ - **Trajectory view** - the trajectory main view, when active.
15
+ - **Produced / highlights** - code blocks, tool cards, diffs, and changed files.
16
+ - **Header popovers** - dropdown menus opened from the header.
17
+ - **Header bar** - the conversation title row and its controls.
18
+ - **Window menu** - the Application and Edit buttons in the desktop title bar.
19
+ - **Right sidebar** - trajectory panel and side panels, when open.
20
+ - **Entire window content** - the whole app shell plus the desktop window menu; overrides every surface above.
21
+
22
+ A surface that is not currently mounted (a closed right sidebar, an inactive
23
+ trajectory view, a closed menu) simply has nothing to hide until it appears.
24
+
25
+ Each target is a card with a toggle switch. The choice is stored in
26
+ `localStorage` under `dsh-hide-ui:targets` and reused on the next start. Turn
27
+ every target off to disable the gesture. The same toggle list is registered as a
28
+ **Hide UI** section in the host Settings dialog, and both surfaces write the same
29
+ selection.
30
+
31
+ ## Gestures
32
+
33
+ | Gesture | Result |
34
+ | --- | --- |
35
+ | `Ctrl` + right-click (anywhere) | Open or close the target picker |
36
+ | Right-click the conversation column or any selected surface | Hide the selected surfaces |
37
+ | **Hide now** in the picker | Apply the current selection immediately and close the picker |
38
+ | **Restore** in the picker (while hidden) | Restore every surface |
39
+ | Right-click while hidden | Restore; the right-click is swallowed |
40
+ | Left-click while hidden | Restore; the click is swallowed |
41
+ | `Esc` while hidden | Restore |
42
+ | `Esc` with the picker open | Close the picker |
43
+ | Right-click outside the selected regions (while visible) | Left alone, so host context menus keep working |
44
+ | `Shift` + right-click | Left alone, for native menu and text-selection paths |
45
+
46
+ Hiding blurs a focused composer that sits inside a hidden region, so keystrokes
47
+ cannot land in an invisible input; restoring returns focus. Regions are hidden
48
+ with `visibility` and `opacity` and are not unmounted, so their state, scroll
49
+ position, and layout survive.
50
+
51
+ ## How it works
52
+
53
+ `client.js` is a plain-JS browser module registered through
54
+ `window.__ModuleLoader__`; there is no build step and no runtime dependency.
55
+
56
+ - The layout columns are discovered structurally, the way `dsh-any-background`
57
+ does it: the frame is the parent of `[data-shell-overlay]`, and its children
58
+ are `[sidebar, center, rightbar?, overlay]`. `data-rightbar-col` marks the
59
+ right column, so the left and centre columns are read by position around it.
60
+ CSS-module suffixes (`_centerCol`, `_sidebarCol`, `_frame`) remain only as a
61
+ fallback when that structure is missing.
62
+ - The nine surface groups reuse `dsh-any-background`'s stable host markers:
63
+ `[data-conversation-composer-overlay]` for the trajectory view,
64
+ `[data-composer-card]`/`[data-cordis-panel]` for input, the produced set
65
+ (`[data-code-block-content]`, `[data-diff]`, `[data-changed-files]`, ...),
66
+ and the header-popover set (`[role="menu"]`, `[data-menu-material]`, ...).
67
+ The header bar resolves from `[data-slot^="conversation.session.header"]`.
68
+ - The conversation target is `[data-chat-flow]`, not the centre column, so the
69
+ header bar and composer keep their own independent toggles. The shell target
70
+ is the frame itself.
71
+ - The selected elements get `.dsh-hide-ui-target`; the shell frame gets
72
+ `.dsh-hide-ui-frame`. One injected stylesheet hides marked targets while
73
+ `data-dsh-hide-ui="on"` is set on `<html>`.
74
+ - The shell frame normally paints a shell-wide fill. The plugin clears it for
75
+ region mode only, so the body-level wallpaper shows through the vacated
76
+ tracks. In whole-shell mode the window itself is hidden, so the fill is left
77
+ alone.
78
+ - Listeners are capture-phase, before the host's React handlers, and are
79
+ removed through `ctx.effect` on unload.
80
+ - The settings section registers through
81
+ `ctx.slots.inject('settings.section', ...)` with id `dsh-hide-ui`, order 70,
82
+ and locale namespace `settings.hideUi`.
83
+
84
+ ## Install
85
+
86
+ Copy or clone this directory anywhere, then link it into the `desktop` profile
87
+ (the plugin is referenced in place, not copied). `dsh plugin add` records it in
88
+ `dsh.profile.bundles`.
89
+
90
+ A running DSH window mounts its client bundles once at startup. After editing
91
+ `client.js`, reinstall if needed, then **restart the app** to load the new
92
+ bundle. Reloading only the renderer can leave the plugin unmounted.
93
+
94
+ Preferred: install the directory through Plugin Manager's *install bundle*
95
+ action with the absolute package directory as the target.
96
+
97
+ Manual fallback:
98
+
99
+ ```powershell
100
+ dsh plugin --profile desktop add "link:<path-to-dsh-hide-ui>"
101
+ ```
102
+
103
+ ## Uninstall
104
+
105
+ Remove `"dsh-hide-ui"` from `dsh.profile.bundles`, then run:
106
+
107
+ ```powershell
108
+ dsh plugin --profile desktop remove dsh-hide-ui
109
+ ```
110
+
111
+ ## Limits
112
+
113
+ - Region resolution depends on the shell structure (`[data-shell-overlay]`,
114
+ `data-rightbar-col`) and falls back to the `_centerCol`/`_sidebarCol` class
115
+ suffixes. A host that changes both breaks the affected target only.
116
+ - The Settings section expects the host's store spec shape (`create()`); if a
117
+ host build rejects it, the Ctrl+right-click picker still works.
118
+ - The right sidebar exists only while open. Selecting it while closed hides the
119
+ other selected regions; the right sidebar joins the next hide once it is
120
+ open.
121
+ - Whole-window mode hides every shell region, including both sidebars, and
122
+ overrides the region checkboxes while checked.
123
+ - The wallpaper must be a body-level layer behind the shell frame, as painted
124
+ by appearance plugins such as `dsh-any-background`.
125
+ - Hiding uses `visibility`, so a hidden region still occupies its grid track;
126
+ the cleared frame fill is what reveals the wallpaper.
127
+ - If the host cannot resolve `react`, the Settings section is disabled; the
128
+ picker and the gesture keep working.
129
+
130
+ ## Compatibility
131
+
132
+ DSH Web Client on the `0.2.x` line. The client half uses only the module loader
133
+ contract (`window.__ModuleLoader__.load`), Cordis `ctx.effect`, `ctx.slots`,
134
+ `ctx.locale`, and DOM APIs; it imports no Harness client package.
package/client.js ADDED
@@ -0,0 +1,912 @@
1
+ window.__ModuleLoader__.load({
2
+ id: 'dsh-hide-ui',
3
+ factory(require) {
4
+ /** Attribute mirrored onto <html> while the selected regions are hidden. */
5
+ const ACTIVE_ATTR = 'data-dsh-hide-ui';
6
+ /** Marker placed on every element that is hidden while active. */
7
+ const TARGET_CLASS = 'dsh-hide-ui-target';
8
+ /** Marker placed on the shell frame so its fill does not cover the wallpaper. */
9
+ const FRAME_CLASS = 'dsh-hide-ui-frame';
10
+ const STYLE_ID = 'dsh-hide-ui-style';
11
+ const STORAGE_KEY = 'dsh-hide-ui:targets';
12
+ const NS = 'settings.hideUi';
13
+ const REGION_IDS = [
14
+ 'sidebar',
15
+ 'cards',
16
+ 'input',
17
+ 'chat',
18
+ 'trajectory',
19
+ 'produced',
20
+ 'header',
21
+ 'headerBar',
22
+ 'windowMenu',
23
+ 'rightbar',
24
+ ];
25
+ const DEFAULT_SELECTION = { shell: false, regions: ['chat'] };
26
+
27
+ /**
28
+ * Selector sets borrowed from dsh-any-background's surface model. The
29
+ * structural targets (sidebar, chat column, right sidebar, shell) and the
30
+ * header bar are resolved separately; everything else matches stable host
31
+ * data-* markers rather than CSS-module class names.
32
+ */
33
+ const TARGET_SELECTORS = {
34
+ cards: [
35
+ '[role="menu"]:not([data-dockkit-tab-menu]):not(.dsh-hide-ui-config)',
36
+ '[data-menu-material]:not([data-dockkit-tab-menu])',
37
+ '[role="listbox"]',
38
+ 'body > [role="tree"]',
39
+ 'body > [role="dialog"]:not([aria-modal="true"]):not(.dsh-hide-ui-config)',
40
+ ],
41
+ input: [
42
+ '[data-composer-card]',
43
+ '[data-composer-seat]',
44
+ '[data-cordis-panel]',
45
+ '[data-approval-key] > div',
46
+ '[data-plan-review-key] > section',
47
+ '[data-question-key] > section',
48
+ ],
49
+ trajectory: ['[data-conversation-composer-overlay]'],
50
+ produced: [
51
+ '[data-code-block-content]',
52
+ '[data-code-block-banner]',
53
+ '[data-composer-chip]',
54
+ '[data-changed-files]',
55
+ '[data-terminal]',
56
+ '[data-read]',
57
+ '[data-diff]',
58
+ '[data-context-injection-body]',
59
+ '[data-search]',
60
+ '[data-web]',
61
+ '[class*="_ioCard"]',
62
+ '[data-inspect]',
63
+ '[data-turn-trigger]',
64
+ ],
65
+ windowMenu: ['[data-windows-menu]'],
66
+ header: [
67
+ '[data-dsh-any-header-popover]',
68
+ 'ul[class*="_menu"]',
69
+ 'div[role="dialog"][class*="_panel"]:not([aria-modal="true"])',
70
+ '[role="menu"][class*="_denseList"]:not([class*="_portal"])',
71
+ '[role="tree"][class*="_menu"]',
72
+ ],
73
+ };
74
+
75
+ let React = null;
76
+ try {
77
+ React = require('react');
78
+ } catch (error) {
79
+ console.warn('dsh-hide-ui: react is unavailable; the settings section will be disabled', error);
80
+ }
81
+
82
+ const en = {
83
+ nav: 'Hide UI',
84
+ desc: 'Choose which surfaces the right-click gesture hides. Right-click a selected surface to hide it; right-click again, click, or press Esc to restore.',
85
+ sidebar: 'Sidebar',
86
+ sidebarHint: 'Sessions, search, and navigation.',
87
+ cards: 'Cards & panels',
88
+ cardsHint: 'Menus, popovers, and option panels.',
89
+ input: 'Input & controls',
90
+ inputHint: 'Composer, input bar, and takeover controls.',
91
+ chat: 'Conversation text frame',
92
+ chatHint: 'Messages and the scrollable text flow.',
93
+ trajectory: 'Trajectory view',
94
+ trajectoryHint: 'The trajectory main view, when active.',
95
+ produced: 'Produced / highlights',
96
+ producedHint: 'Code blocks, tool cards, diffs, and changed files.',
97
+ header: 'Header popovers',
98
+ headerHint: 'Dropdown menus opened from the header.',
99
+ headerBar: 'Header bar',
100
+ headerBarHint: 'The conversation title row and its controls.',
101
+ windowMenu: 'Window menu',
102
+ windowMenuHint: 'Application and Edit buttons in the desktop title bar.',
103
+ rightbar: 'Right sidebar',
104
+ rightbarHint: 'Trajectory panel and side panels, when open.',
105
+ shell: 'Entire window content',
106
+ shellHint: 'Hides the whole app shell. Overrides every surface above.',
107
+ empty: 'Select at least one surface, or the gesture stays disabled.',
108
+ hideNow: 'Hide now',
109
+ restore: 'Restore',
110
+ reset: 'Reset to defaults',
111
+ close: 'Close',
112
+ };
113
+
114
+ const zh = {
115
+ nav: '隐藏界面',
116
+ desc: '选择右键手势要隐藏的界面区域。在选中的区域内右键即可隐藏;再次右键、点击或按 Esc 恢复。',
117
+ sidebar: '左侧边栏',
118
+ sidebarHint: '会话列表、搜索和导航。',
119
+ cards: '卡片与面板',
120
+ cardsHint: '菜单、浮层和选项面板。',
121
+ input: '输入框与控件',
122
+ inputHint: '输入框、输入栏和接管控件。',
123
+ chat: '对话文本框',
124
+ chatHint: '消息列表和可滚动的文本区域。',
125
+ trajectory: '轨迹页',
126
+ trajectoryHint: '轨迹主视图(打开时)。',
127
+ produced: '产出物 / 高亮内容',
128
+ producedHint: '代码块、工具卡片、差异和变更文件。',
129
+ header: '顶栏选项',
130
+ headerHint: '从顶栏打开的菜单。',
131
+ headerBar: '标题栏',
132
+ headerBarHint: '会话标题行及其控件。',
133
+ windowMenu: '窗口菜单',
134
+ windowMenuHint: '桌面标题栏中的“应用”和“编辑”按钮。',
135
+ rightbar: '右侧边栏',
136
+ rightbarHint: '轨迹面板和侧面板(打开时)。',
137
+ shell: '整个窗口内容',
138
+ shellHint: '隐藏整个应用外壳,并覆盖上面的所有区域。',
139
+ empty: '至少选择一个区域,否则手势不会生效。',
140
+ hideNow: '立即隐藏',
141
+ restore: '恢复',
142
+ reset: '恢复默认',
143
+ close: '关闭',
144
+ };
145
+
146
+ /**
147
+ * Selected targets are hidden with visibility/opacity; the shell frame fill
148
+ * is cleared so the body-level wallpaper shows through vacated grid tracks.
149
+ */
150
+ const CSS = `
151
+ .dsh-hide-ui-config {
152
+ position: fixed;
153
+ left: 50%;
154
+ top: 50%;
155
+ transform: translate(-50%, -50%);
156
+ z-index: 2147483000;
157
+ box-sizing: border-box;
158
+ min-width: 300px;
159
+ max-width: 340px;
160
+ max-height: calc(100vh - 64px);
161
+ overflow-y: auto;
162
+ padding: 12px;
163
+ border-radius: 10px;
164
+ border: 1px solid var(--dsw-alias-border-l2, rgba(127, 127, 127, .35));
165
+ background: var(--dsw-alias-bg-layer-2, #fff);
166
+ color: var(--dsw-alias-label-primary, #111);
167
+ box-shadow: 0 10px 30px rgba(0, 0, 0, .22);
168
+ font: 13px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
169
+ }
170
+ .dsh-hide-ui-config-title {
171
+ margin: 0 0 8px;
172
+ font-size: 13px;
173
+ font-weight: 600;
174
+ }
175
+ .dsh-hide-ui-config-row {
176
+ display: flex;
177
+ flex-direction: row-reverse;
178
+ align-items: center;
179
+ justify-content: space-between;
180
+ gap: 10px;
181
+ margin: 4px 0;
182
+ padding: 8px 10px;
183
+ border: 1px solid var(--dsw-alias-border-l2, rgba(127, 127, 127, .28));
184
+ border-radius: 8px;
185
+ background: var(--dsw-alias-bg-layer-1, rgba(127, 127, 127, .04));
186
+ cursor: pointer;
187
+ }
188
+ .dsh-hide-ui-config-row:has(input:checked) {
189
+ border-color: var(--dsw-alias-brand-primary, #4d6bfe);
190
+ }
191
+ .dsh-hide-ui-config-row input {
192
+ appearance: none;
193
+ -webkit-appearance: none;
194
+ position: relative;
195
+ flex: none;
196
+ width: 38px;
197
+ height: 22px;
198
+ margin: 0;
199
+ border-radius: 99px;
200
+ border: 1px solid var(--dsw-alias-border-l2, rgba(127, 127, 127, .45));
201
+ background: var(--dsw-alias-bg-layer-2, #e5e5e5);
202
+ cursor: pointer;
203
+ transition: background .2s ease, border-color .2s ease;
204
+ }
205
+ .dsh-hide-ui-config-row input::before {
206
+ content: "";
207
+ position: absolute;
208
+ top: 2px;
209
+ left: 2px;
210
+ width: 16px;
211
+ height: 16px;
212
+ border-radius: 50%;
213
+ background: var(--dsw-alias-label-secondary, #888);
214
+ transition: transform .2s ease, background .2s ease;
215
+ }
216
+ .dsh-hide-ui-config-row input:checked {
217
+ background: var(--dsw-alias-brand-primary, #4d6bfe);
218
+ border-color: var(--dsw-alias-brand-primary, #4d6bfe);
219
+ }
220
+ .dsh-hide-ui-config-row input:checked::before {
221
+ transform: translateX(16px);
222
+ background: #fff;
223
+ }
224
+ .dsh-hide-ui-config-row input:disabled {
225
+ opacity: .45;
226
+ cursor: default;
227
+ }
228
+ .dsh-hide-ui-config-row > span {
229
+ display: flex;
230
+ flex: 1;
231
+ flex-direction: column;
232
+ gap: 2px;
233
+ }
234
+ .dsh-hide-ui-config-hint {
235
+ display: block;
236
+ font-size: 11px;
237
+ line-height: 1.4;
238
+ opacity: .65;
239
+ }
240
+ .dsh-hide-ui-config-actions {
241
+ display: flex;
242
+ justify-content: flex-end;
243
+ gap: 8px;
244
+ margin-top: 10px;
245
+ }
246
+ .dsh-hide-ui-config-apply {
247
+ font: inherit;
248
+ font-size: 12px;
249
+ padding: 4px 10px;
250
+ border-radius: 6px;
251
+ border: 1px solid var(--dsw-alias-brand-primary, #4d6bfe);
252
+ background: var(--dsw-alias-brand-primary, #4d6bfe);
253
+ color: #fff;
254
+ cursor: pointer;
255
+ }
256
+ .dsh-hide-ui-config-close {
257
+ font: inherit;
258
+ font-size: 12px;
259
+ padding: 4px 10px;
260
+ border-radius: 6px;
261
+ border: 1px solid var(--dsw-alias-border-l2, rgba(127, 127, 127, .35));
262
+ background: transparent;
263
+ color: inherit;
264
+ cursor: pointer;
265
+ }
266
+ html[${ACTIVE_ATTR}="on"] .${TARGET_CLASS} {
267
+ visibility: hidden !important;
268
+ opacity: 0 !important;
269
+ pointer-events: none !important;
270
+ }
271
+ html[${ACTIVE_ATTR}="on"] .${FRAME_CLASS} {
272
+ background: transparent !important;
273
+ }
274
+ `;
275
+
276
+ function normalize(raw) {
277
+ const source = raw !== null && typeof raw === 'object' ? raw : {};
278
+ const regions = Array.isArray(source.regions)
279
+ ? [...new Set(source.regions.filter((id) => REGION_IDS.includes(id)))]
280
+ : [...DEFAULT_SELECTION.regions];
281
+ return { shell: source.shell === true, regions };
282
+ }
283
+
284
+ function loadSelection() {
285
+ try {
286
+ const raw = window.localStorage.getItem(STORAGE_KEY);
287
+ if (raw === null) return normalize(DEFAULT_SELECTION);
288
+ return normalize(JSON.parse(raw));
289
+ } catch {
290
+ return normalize(DEFAULT_SELECTION);
291
+ }
292
+ }
293
+
294
+ function storeSelection(selection) {
295
+ try {
296
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(selection));
297
+ } catch {
298
+ // localStorage may be unavailable in private contexts.
299
+ }
300
+ }
301
+
302
+ function apply(ctx) {
303
+ // A reloaded factory must not leave a previous copy of the stylesheet behind.
304
+ document.getElementById(STYLE_ID)?.remove();
305
+
306
+ const style = document.createElement('style');
307
+ style.id = STYLE_ID;
308
+ style.dataset.plugin = 'dsh-hide-ui';
309
+ style.textContent = CSS;
310
+ (document.head ?? document.documentElement).appendChild(style);
311
+
312
+ let selection = loadSelection();
313
+ let active = false;
314
+ let focusBefore = null;
315
+ /** Swallow the rest of the click that restored the selected regions. */
316
+ let swallowLeft = false;
317
+ let swallowLeftTimer = 0;
318
+ /** Lightweight configuration popover opened with Ctrl+right-click. */
319
+ let configPanel = null;
320
+
321
+ function fallbackT(key) {
322
+ return en[key] ?? key;
323
+ }
324
+
325
+ function t(key) {
326
+ try {
327
+ if (ctx.locale !== undefined && typeof ctx.locale.bind === 'function') {
328
+ return ctx.locale.bind(NS)(key);
329
+ }
330
+ } catch {
331
+ // Fall through to the bundled English copy.
332
+ }
333
+ return fallbackT(key);
334
+ }
335
+
336
+ function clearSwallowLeft() {
337
+ swallowLeft = false;
338
+ if (swallowLeftTimer !== 0) {
339
+ window.clearTimeout(swallowLeftTimer);
340
+ swallowLeftTimer = 0;
341
+ }
342
+ }
343
+
344
+ function beginSwallowLeft() {
345
+ swallowLeft = true;
346
+ if (swallowLeftTimer !== 0) window.clearTimeout(swallowLeftTimer);
347
+ swallowLeftTimer = window.setTimeout(clearSwallowLeft, 700);
348
+ }
349
+
350
+ /**
351
+ * Resolve the layout columns from the shell structure, the way
352
+ * dsh-any-background does: the frame's children are [sidebar, center,
353
+ * rightbar?, overlay], so the columns are read by position around the
354
+ * stable `[data-shell-overlay]` and `data-rightbar-col` anchors instead of
355
+ * CSS-module suffixes that a host build can rename.
356
+ */
357
+ function discoverParts() {
358
+ const overlay = document.querySelector('[data-shell-overlay]');
359
+ const frame =
360
+ overlay?.parentElement ??
361
+ document.querySelector('[class*="_frame"]') ??
362
+ document.getElementById('root');
363
+ if (frame === null) return { frame: null, center: null, sidebar: null, rightbar: null };
364
+
365
+ const children = frame.children;
366
+ let overlayIndex = -1;
367
+ let rightbar = null;
368
+ let rightIndex = -1;
369
+ for (let index = 0; index < children.length; index += 1) {
370
+ const child = children[index];
371
+ if (child === overlay) overlayIndex = index;
372
+ else if (child instanceof HTMLElement && child.dataset.rightbarCol !== undefined) {
373
+ rightbar = child;
374
+ rightIndex = index;
375
+ }
376
+ }
377
+
378
+ let sidebar = null;
379
+ let center = null;
380
+ if (rightIndex >= 0) {
381
+ sidebar = children[rightIndex - 2] ?? null;
382
+ center = children[rightIndex - 1] ?? null;
383
+ } else if (overlayIndex >= 0) {
384
+ sidebar = children[overlayIndex - 3] ?? null;
385
+ center = children[overlayIndex - 2] ?? null;
386
+ }
387
+ return { frame, center, sidebar, rightbar };
388
+ }
389
+
390
+ function findFrame() {
391
+ return discoverParts().frame ?? document.getElementById('root');
392
+ }
393
+
394
+ function resolveTargetElements(id) {
395
+ const parts = discoverParts();
396
+
397
+ if (id === 'sidebar') {
398
+ const element = document.querySelector('[class*="_sidebarCol"]') ?? parts.sidebar;
399
+ return element === null ? [] : [element];
400
+ }
401
+ if (id === 'rightbar') {
402
+ const element =
403
+ document.querySelector('[data-rightbar-col]') ??
404
+ document.querySelector('[data-sidebar-right-panel]') ??
405
+ parts.rightbar;
406
+ return element === null ? [] : [element];
407
+ }
408
+ if (id === 'shell') {
409
+ const element = parts.frame ?? document.getElementById('root');
410
+ return element === null ? [] : [element];
411
+ }
412
+ if (id === 'chat') {
413
+ // Only the text flow, never the centre column: the column also owns
414
+ // the header bar and composer, which have their own toggles.
415
+ const flow = document.querySelector('[data-chat-flow]');
416
+ if (flow !== null) return [flow];
417
+ const content = document.querySelector('[data-conversation-content]');
418
+ if (content !== null) return [content];
419
+ return [];
420
+ }
421
+ if (id === 'headerBar') {
422
+ const anchor = document.querySelector('[data-slot^="conversation.session.header"]');
423
+ const bar =
424
+ anchor?.closest('header') ??
425
+ anchor?.querySelector('header') ??
426
+ anchor ??
427
+ document.querySelector('[class*="_headerBar"]');
428
+ return bar === null ? [] : [bar];
429
+ }
430
+
431
+ const seen = new Set();
432
+ const elements = [];
433
+ for (const selector of TARGET_SELECTORS[id] ?? []) {
434
+ for (const element of document.querySelectorAll(selector)) {
435
+ if (!seen.has(element)) {
436
+ seen.add(element);
437
+ elements.push(element);
438
+ }
439
+ }
440
+ }
441
+ return elements;
442
+ }
443
+
444
+ function selectedElements() {
445
+ if (selection.shell) {
446
+ const elements = resolveTargetElements('shell');
447
+ // The desktop window menu lives outside the shell frame, so "entire
448
+ // window content" has to claim it explicitly.
449
+ const menu = document.querySelector('[data-windows-menu]');
450
+ if (menu !== null && !elements.includes(menu)) elements.push(menu);
451
+ return elements;
452
+ }
453
+
454
+ const seen = new Set();
455
+ const elements = [];
456
+ for (const id of selection.regions) {
457
+ for (const element of resolveTargetElements(id)) {
458
+ if (!seen.has(element)) {
459
+ seen.add(element);
460
+ elements.push(element);
461
+ }
462
+ }
463
+ }
464
+ return elements;
465
+ }
466
+
467
+ function markTargets() {
468
+ for (const element of selectedElements()) element.classList.add(TARGET_CLASS);
469
+ if (!selection.shell) {
470
+ const frame = findFrame();
471
+ if (frame !== null) frame.classList.add(FRAME_CLASS);
472
+ }
473
+ }
474
+
475
+ function unmarkTargets() {
476
+ for (const element of document.querySelectorAll(`.${TARGET_CLASS}`)) {
477
+ element.classList.remove(TARGET_CLASS);
478
+ }
479
+ for (const element of document.querySelectorAll(`.${FRAME_CLASS}`)) {
480
+ element.classList.remove(FRAME_CLASS);
481
+ }
482
+ }
483
+
484
+ function setActive(next) {
485
+ if (next === active) return;
486
+
487
+ if (next) {
488
+ const elements = selectedElements();
489
+ if (elements.length === 0) return;
490
+ active = true;
491
+ markTargets();
492
+
493
+ // A hidden composer must not keep receiving keystrokes.
494
+ const focused = document.activeElement;
495
+ if (focused !== null && elements.some((element) => element.contains(focused))) {
496
+ if (typeof focused.blur === 'function') {
497
+ focusBefore = focused;
498
+ focused.blur();
499
+ }
500
+ }
501
+ document.documentElement.setAttribute(ACTIVE_ATTR, 'on');
502
+ } else {
503
+ active = false;
504
+ document.documentElement.removeAttribute(ACTIVE_ATTR);
505
+ unmarkTargets();
506
+
507
+ const focused = focusBefore;
508
+ focusBefore = null;
509
+ if (focused !== null && focused.isConnected && typeof focused.focus === 'function') {
510
+ try {
511
+ focused.focus({ preventScroll: true });
512
+ } catch {
513
+ // The element may have unmounted between activation and restore.
514
+ }
515
+ }
516
+ }
517
+ }
518
+
519
+ function updateSelection(next) {
520
+ selection = normalize(next);
521
+ storeSelection(selection);
522
+ syncConfigInputs();
523
+
524
+ if (!active) return;
525
+ unmarkTargets();
526
+ if (selectedElements().length === 0) {
527
+ setActive(false);
528
+ return;
529
+ }
530
+ // Keep the active attribute and re-mark the new target set.
531
+ markTargets();
532
+ }
533
+
534
+ function syncConfigInputs() {
535
+ if (configPanel === null) return;
536
+ const shellInput = configPanel.querySelector('[data-region="shell"]');
537
+ const shell = shellInput !== null && shellInput.checked;
538
+ for (const input of configPanel.querySelectorAll('[data-region]')) {
539
+ if (input.dataset.region === 'shell') {
540
+ input.checked = selection.shell;
541
+ } else {
542
+ input.checked = selection.regions.includes(input.dataset.region);
543
+ input.disabled = shell;
544
+ }
545
+ }
546
+ }
547
+
548
+ function closeConfig() {
549
+ if (configPanel === null) return;
550
+ configPanel.remove();
551
+ configPanel = null;
552
+ }
553
+
554
+ function openConfig() {
555
+ closeConfig();
556
+
557
+ const panel = document.createElement('div');
558
+ panel.className = 'dsh-hide-ui-config';
559
+ panel.setAttribute('role', 'dialog');
560
+ panel.setAttribute('aria-label', t('nav'));
561
+
562
+ const title = document.createElement('div');
563
+ title.className = 'dsh-hide-ui-config-title';
564
+ title.textContent = t('nav');
565
+ panel.appendChild(title);
566
+
567
+ const inputs = new Map();
568
+ const addRow = (id, labelText, hintText) => {
569
+ const row = document.createElement('label');
570
+ row.className = 'dsh-hide-ui-config-row';
571
+
572
+ const input = document.createElement('input');
573
+ input.type = 'checkbox';
574
+ input.dataset.region = id;
575
+ input.addEventListener('change', () => {
576
+ const regions = REGION_IDS.filter((regionId) => inputs.get(regionId).checked);
577
+ updateSelection({ shell: inputs.get('shell').checked, regions });
578
+ });
579
+ inputs.set(id, input);
580
+
581
+ const text = document.createElement('span');
582
+ const label = document.createElement('span');
583
+ label.textContent = labelText;
584
+ const hint = document.createElement('span');
585
+ hint.className = 'dsh-hide-ui-config-hint';
586
+ hint.textContent = hintText;
587
+ text.appendChild(label);
588
+ text.appendChild(hint);
589
+
590
+ row.appendChild(input);
591
+ row.appendChild(text);
592
+ panel.appendChild(row);
593
+ };
594
+
595
+ for (const id of REGION_IDS) addRow(id, t(id), t(`${id}Hint`));
596
+ addRow('shell', t('shell'), t('shellHint'));
597
+
598
+ const actions = document.createElement('div');
599
+ actions.className = 'dsh-hide-ui-config-actions';
600
+
601
+ const apply = document.createElement('button');
602
+ apply.type = 'button';
603
+ apply.className = 'dsh-hide-ui-config-apply';
604
+ apply.textContent = t('hideNow');
605
+ apply.addEventListener('click', () => {
606
+ if (selectedElements().length === 0) return;
607
+ setActive(true);
608
+ closeConfig();
609
+ });
610
+ actions.appendChild(apply);
611
+
612
+ if (active) {
613
+ const restore = document.createElement('button');
614
+ restore.type = 'button';
615
+ restore.className = 'dsh-hide-ui-config-close';
616
+ restore.textContent = t('restore');
617
+ restore.addEventListener('click', () => {
618
+ setActive(false);
619
+ closeConfig();
620
+ });
621
+ actions.appendChild(restore);
622
+ }
623
+
624
+ const close = document.createElement('button');
625
+ close.type = 'button';
626
+ close.className = 'dsh-hide-ui-config-close';
627
+ close.textContent = t('close');
628
+ close.addEventListener('click', closeConfig);
629
+ actions.appendChild(close);
630
+ panel.appendChild(actions);
631
+
632
+ document.body.appendChild(panel);
633
+ configPanel = panel;
634
+ syncConfigInputs();
635
+
636
+ }
637
+
638
+ function consume(event) {
639
+ event.preventDefault();
640
+ event.stopImmediatePropagation();
641
+ }
642
+
643
+ /** Whether a right-click belongs to the hide gesture. */
644
+ function ownsRightClick(target) {
645
+ if (active) return true;
646
+ // The conversation column is the gesture home: a sidebar-only or
647
+ // header-bar-only selection still hides when the user right-clicks the
648
+ // main area, instead of requiring a click on the exact target.
649
+ const parts = discoverParts();
650
+ if (parts.center !== null && parts.center.contains(target)) return true;
651
+ return selectedElements().some((element) => element.contains(target));
652
+ }
653
+
654
+ /**
655
+ * Right-click inside a selected region toggles the hidden state; once
656
+ * hidden, a right-click anywhere restores. Left-clicking or Escape while
657
+ * hidden also restores, and the restoring click is swallowed. Alt+right-
658
+ * click opens the target picker without hiding anything.
659
+ */
660
+ function onPointerDown(event) {
661
+ if (configPanel !== null) {
662
+ if (configPanel.contains(event.target)) return;
663
+ closeConfig();
664
+ if (event.button === 0 || event.button === 2) consume(event);
665
+ return;
666
+ }
667
+
668
+ if (event.button === 2) {
669
+ if (event.ctrlKey && !event.shiftKey) {
670
+ consume(event);
671
+ openConfig();
672
+ return;
673
+ }
674
+ if (event.shiftKey || !ownsRightClick(event.target)) return;
675
+ consume(event);
676
+ setActive(!active);
677
+ return;
678
+ }
679
+
680
+ if (active && event.button === 0) {
681
+ // If a host re-render removed the markers, do not swallow the click.
682
+ if (document.querySelector(`.${TARGET_CLASS}`) === null) {
683
+ setActive(false);
684
+ return;
685
+ }
686
+ consume(event);
687
+ setActive(false);
688
+ beginSwallowLeft();
689
+ }
690
+ }
691
+
692
+ function onContextMenu(event) {
693
+ if (configPanel !== null || event.ctrlKey) {
694
+ consume(event);
695
+ return;
696
+ }
697
+ if (event.shiftKey || !ownsRightClick(event.target)) return;
698
+ consume(event);
699
+ }
700
+
701
+ /**
702
+ * Own the rest of the gesture as well: compatibility mouse events fire
703
+ * after pointerdown, and the restore click must not reach a control that
704
+ * just became visible again.
705
+ */
706
+ function onMouseEvent(event) {
707
+ if (configPanel !== null) {
708
+ if (configPanel.contains(event.target)) return;
709
+ if (event.button === 0 || event.button === 2) consume(event);
710
+ return;
711
+ }
712
+
713
+ if (event.button === 2) {
714
+ if (event.ctrlKey) {
715
+ consume(event);
716
+ return;
717
+ }
718
+ if (event.shiftKey || !ownsRightClick(event.target)) return;
719
+ consume(event);
720
+ return;
721
+ }
722
+
723
+ if (swallowLeft && event.button === 0) {
724
+ consume(event);
725
+ if (event.type === 'click') clearSwallowLeft();
726
+ }
727
+ }
728
+
729
+ function onKeyDown(event) {
730
+ if (configPanel !== null) {
731
+ if (event.key === 'Escape' || event.key === 'Esc') {
732
+ consume(event);
733
+ closeConfig();
734
+ }
735
+ return;
736
+ }
737
+ if (!active) return;
738
+ if (event.key === 'Escape' || event.key === 'Esc') {
739
+ consume(event);
740
+ setActive(false);
741
+ }
742
+ }
743
+
744
+ function SettingsSection(props) {
745
+ const [value, setValue] = React.useState(loadSelection);
746
+ const translate = typeof props.t === 'function' ? props.t : t;
747
+
748
+ const update = (next) => {
749
+ const normalized = normalize(next);
750
+ setValue(normalized);
751
+ updateSelection(normalized);
752
+ };
753
+
754
+ const toggleRegion = (id) => {
755
+ const regions = value.regions.includes(id)
756
+ ? value.regions.filter((region) => region !== id)
757
+ : [...value.regions, id];
758
+ update({ ...value, regions });
759
+ };
760
+
761
+ const row = (id, label, hint, disabled) => React.createElement(
762
+ 'label',
763
+ {
764
+ key: id,
765
+ className: 'dsh-hide-ui-config-row',
766
+ style: {
767
+ opacity: disabled ? 0.5 : 1,
768
+ cursor: disabled ? 'default' : 'pointer',
769
+ },
770
+ },
771
+ [
772
+ React.createElement('input', {
773
+ key: 'input',
774
+ type: 'checkbox',
775
+ checked: value.regions.includes(id),
776
+ disabled,
777
+ onChange: () => toggleRegion(id),
778
+ style: { marginTop: 3, accentColor: 'var(--dsw-alias-brand-primary, #4d6bfe)' },
779
+ }),
780
+ React.createElement(
781
+ 'span',
782
+ { key: 'text', style: { display: 'flex', flexDirection: 'column', gap: 2 } },
783
+ [
784
+ React.createElement('span', { key: 'label', style: { fontWeight: 500 } }, label),
785
+ React.createElement(
786
+ 'span',
787
+ { key: 'hint', style: { fontSize: 12, lineHeight: 1.45, opacity: 0.65 } },
788
+ hint,
789
+ ),
790
+ ],
791
+ ),
792
+ ],
793
+ );
794
+
795
+ return React.createElement(
796
+ 'div',
797
+ { style: { display: 'flex', flexDirection: 'column', gap: 14, maxWidth: 640, padding: '4px 0' } },
798
+ [
799
+ React.createElement(
800
+ 'p',
801
+ { key: 'description', style: { margin: 0, fontSize: 13, lineHeight: 1.55, opacity: 0.8 } },
802
+ translate('desc'),
803
+ ),
804
+ React.createElement('div', { key: 'regions', style: { display: 'flex', flexDirection: 'column', gap: 10 } }, [
805
+ ...REGION_IDS.map((id) => row(id, translate(id), translate(`${id}Hint`), value.shell)),
806
+ ]),
807
+ row('shell', translate('shell'), translate('shellHint'), false),
808
+ !value.shell && value.regions.length === 0
809
+ ? React.createElement(
810
+ 'p',
811
+ {
812
+ key: 'empty',
813
+ style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary, #d33)' },
814
+ },
815
+ translate('empty'),
816
+ )
817
+ : null,
818
+ React.createElement('div', { key: 'actions', style: { display: 'flex', gap: 8 } }, [
819
+ React.createElement(
820
+ 'button',
821
+ {
822
+ key: 'reset',
823
+ type: 'button',
824
+ onClick: () => update({ shell: false, regions: ['chat'] }),
825
+ style: {
826
+ font: 'inherit',
827
+ fontSize: 12,
828
+ padding: '5px 10px',
829
+ borderRadius: 6,
830
+ border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.35))',
831
+ background: 'transparent',
832
+ color: 'inherit',
833
+ cursor: 'pointer',
834
+ },
835
+ },
836
+ translate('reset'),
837
+ ),
838
+ ]),
839
+ ],
840
+ );
841
+ }
842
+
843
+ function registerSettings() {
844
+ if (React === null) {
845
+ console.warn('dsh-hide-ui: settings section disabled because React could not be resolved');
846
+ return;
847
+ }
848
+ if (ctx.slots === undefined || ctx.locale === undefined) {
849
+ console.warn('dsh-hide-ui: settings section disabled because slots/locale are unavailable');
850
+ return;
851
+ }
852
+
853
+ try {
854
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-hide-ui: i18n');
855
+ } catch (error) {
856
+ console.warn('dsh-hide-ui: locale registration failed', error);
857
+ }
858
+
859
+ try {
860
+ // The settings slot expects a store spec with create(), not a bare
861
+ // snapshot: the host calls create() once and binds useStore/actions.
862
+ const emptyStore = {
863
+ create: () => ({
864
+ getSnapshot: () => ({ selection: loadSelection() }),
865
+ subscribe: () => () => {},
866
+ actions: {},
867
+ }),
868
+ };
869
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
870
+ name: 'settings.section',
871
+ id: 'dsh-hide-ui',
872
+ order: 70,
873
+ label: () => ctx.locale.bind(NS)('nav'),
874
+ locale: NS,
875
+ store: emptyStore,
876
+ inject: () => ({}),
877
+ }, SettingsSection));
878
+ } catch (error) {
879
+ console.warn('dsh-hide-ui: settings section registration failed', error);
880
+ }
881
+ }
882
+
883
+ window.addEventListener('pointerdown', onPointerDown, true);
884
+ window.addEventListener('mousedown', onMouseEvent, true);
885
+ window.addEventListener('mouseup', onMouseEvent, true);
886
+ window.addEventListener('pointerup', onMouseEvent, true);
887
+ window.addEventListener('click', onMouseEvent, true);
888
+ window.addEventListener('auxclick', onMouseEvent, true);
889
+ window.addEventListener('contextmenu', onContextMenu, true);
890
+ window.addEventListener('keydown', onKeyDown, true);
891
+
892
+ registerSettings();
893
+
894
+ ctx.effect(() => () => {
895
+ window.removeEventListener('pointerdown', onPointerDown, true);
896
+ window.removeEventListener('mousedown', onMouseEvent, true);
897
+ window.removeEventListener('mouseup', onMouseEvent, true);
898
+ window.removeEventListener('pointerup', onMouseEvent, true);
899
+ window.removeEventListener('click', onMouseEvent, true);
900
+ window.removeEventListener('auxclick', onMouseEvent, true);
901
+ window.removeEventListener('contextmenu', onContextMenu, true);
902
+ window.removeEventListener('keydown', onKeyDown, true);
903
+ closeConfig();
904
+ clearSwallowLeft();
905
+ setActive(false);
906
+ style.remove();
907
+ }, 'dsh-hide-ui: hide UI regions');
908
+ }
909
+
910
+ return { name: 'dsh-hide-ui', inject: ['slots', 'locale'], apply };
911
+ },
912
+ });
@@ -0,0 +1,5 @@
1
+ # Bundle patch for dsh-hide-ui. The Node half is intentionally inert; the
2
+ # browser half (client.js) owns the right-click peek gesture and its styles.
3
+ - insert:
4
+ - id: dsh-hide-ui
5
+ name: dsh-hide-ui
package/icon.svg ADDED
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Hide UI peek">
2
+ <rect width="64" height="64" rx="14" fill="#1f2430"/>
3
+ <path d="M10 32c6-9 13-13 22-13s16 4 22 13c-6 9-13 13-22 13S16 41 10 32z" fill="#e8ecf7"/>
4
+ <circle cx="32" cy="32" r="7" fill="#1f2430"/>
5
+ <path d="M14 50 50 14" stroke="#4d6bfe" stroke-width="6" stroke-linecap="round"/>
6
+ </svg>
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Node half of the dsh-hide-ui bundle.
3
+ *
4
+ * The bundle exists to load the browser module declared by `dsh.client`; there
5
+ * is no server-side work, so this plugin row deliberately does nothing.
6
+ */
7
+ export function apply() {}
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Hide UI Surfaces",
4
+ "description": "Right-click to hide selected DSH surfaces: sidebar, cards, input, conversation, trajectory, produced content, header, right sidebar, or the whole shell."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "隐藏界面区域",
4
+ "description": "右键隐藏选中的界面区域:侧边栏、卡片、输入框、对话、轨迹、产出内容、顶栏、右侧边栏或整个窗口。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "dsh-hide-ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.4",
4
+ "description": "Right-click to hide selected DeepSeek Harness surfaces (sidebar, cards, input, conversation, trajectory, produced content, header, right sidebar) and reveal the background wallpaper.",
5
+ "keywords": [
6
+ "deepseek-harness",
7
+ "dsh",
8
+ "dsh-plugin",
9
+ "wallpaper",
10
+ "peek"
11
+ ],
12
+ "license": "MIT",
13
+ "type": "module",
14
+ "exports": {
15
+ ".": "./index.js",
16
+ "./client": "./client.js",
17
+ "./locale/*.json": "./locale/*.json",
18
+ "./icon": "./icon.svg"
19
+ },
20
+ "files": [
21
+ "index.js",
22
+ "client.js",
23
+ "cordis.patch.yml",
24
+ "locale/*.json",
25
+ "icon.svg",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "dsh": {
30
+ "bundle": {
31
+ "patch": "./cordis.patch.yml"
32
+ },
33
+ "client": {
34
+ "platform": "web",
35
+ "immediately": true,
36
+ "inject": []
37
+ }
38
+ }
39
+ }