dsh-coding-sidebar 1.0.3 → 1.0.5
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 +27 -42
- package/lib/client-editor.js +315 -4179
- package/lib/client-registry.js +913 -1303
- package/lib/client-terminal.js +189 -261
- package/lib/client.js +911 -1301
- package/lib/index.js +13 -110
- package/lib/types/bundle-route.d.ts +1 -1
- package/lib/types/client/LiveView.d.ts +1 -0
- package/lib/types/client/SideCardSection.d.ts +3 -3
- package/lib/types/client/TextEditor.d.ts +20 -10
- package/lib/types/client/add-plugin-modal.d.ts +0 -4
- package/lib/types/client/api.d.ts +16 -11
- package/lib/types/client/builtins/index.d.ts +10 -10
- package/lib/types/client/chunk-loader.d.ts +7 -8
- package/lib/types/client/locales.d.ts +6 -26
- package/lib/types/client/native-avoid.d.ts +2 -0
- package/lib/types/client/plugins-shared.d.ts +6 -6
- package/lib/types/client/service.d.ts +13 -83
- package/lib/types/index.d.ts +1 -1
- package/lib/types/prefs-shared.d.ts +1 -24
- package/lib/types/wire.d.ts +1 -1
- package/package.json +26 -29
- package/src/bundle-route.ts +1 -1
- package/src/client/BrowserView.tsx +18 -2
- package/src/client/EditorHost.tsx +52 -122
- package/src/client/LiveView.tsx +229 -0
- package/src/client/SandboxStatusBar.tsx +12 -9
- package/src/client/SideCardSection.tsx +28 -81
- package/src/client/TextEditor.tsx +89 -266
- package/src/client/add-plugin-modal.tsx +16 -24
- package/src/client/api.ts +4 -25
- package/src/client/builtins/index.ts +10 -14
- package/src/client/chunk-loader.ts +7 -8
- package/src/client/index.tsx +12 -3
- package/src/client/locales-ar.ts +0 -6
- package/src/client/locales-de.ts +0 -6
- package/src/client/locales-fr.ts +0 -6
- package/src/client/locales-hi.ts +0 -6
- package/src/client/locales-id.ts +0 -6
- package/src/client/locales-it.ts +0 -6
- package/src/client/locales-ja.ts +0 -6
- package/src/client/locales-ko.ts +0 -6
- package/src/client/locales-nl.ts +0 -6
- package/src/client/locales-pl.ts +0 -6
- package/src/client/locales-pt.ts +0 -6
- package/src/client/locales-ru.ts +0 -6
- package/src/client/locales-sv.ts +0 -6
- package/src/client/locales-th.ts +0 -6
- package/src/client/locales-tr.ts +0 -6
- package/src/client/locales-vi.ts +0 -6
- package/src/client/locales-zh-HK.ts +0 -6
- package/src/client/locales-zh-MO.ts +0 -6
- package/src/client/locales-zh-TW.ts +0 -6
- package/src/client/locales.ts +12 -52
- package/src/client/native-avoid.ts +155 -0
- package/src/client/plugins-shared.ts +6 -6
- package/src/client/prefs.ts +0 -7
- package/src/client/service.ts +13 -150
- package/src/client/sidebar.module.css +28 -420
- package/src/config.ts +1 -4
- package/src/index.ts +17 -74
- package/src/prefs-shared.ts +1 -27
- package/src/wire.ts +1 -0
- package/lib/client-mermaid.js +0 -200776
- package/lib/types/client/MarkdownHtml.d.ts +0 -32
- package/lib/types/client/PdfView.d.ts +0 -6
- package/lib/types/client/builtins/viewers.d.ts +0 -3
- package/lib/types/client/chunks/mermaid.d.ts +0 -10
- package/lib/types/client/editor-load.d.ts +0 -66
- package/lib/types/client/image-types.d.ts +0 -3
- package/lib/types/client/markdown-html.d.ts +0 -96
- package/lib/types/client/markdown-images.d.ts +0 -45
- package/lib/types/client/md-toc.d.ts +0 -4
- package/lib/types/client/mermaid-blocks.d.ts +0 -47
- package/lib/types/client/mermaid-sanitize.d.ts +0 -2
- package/lib/types/client/mermaid.d.ts +0 -12
- package/lib/types/client/pdf-types.d.ts +0 -2
- package/lib/types/client/plugins-viewers.d.ts +0 -3
- package/lib/types/html-route.d.ts +0 -59
- package/src/client/MarkdownHtml.tsx +0 -296
- package/src/client/PdfView.tsx +0 -110
- package/src/client/builtins/viewers.tsx +0 -125
- package/src/client/chunks/mermaid.tsx +0 -10
- package/src/client/editor-load.ts +0 -92
- package/src/client/image-types.ts +0 -8
- package/src/client/markdown-html.ts +0 -331
- package/src/client/markdown-images.ts +0 -137
- package/src/client/md-toc.tsx +0 -127
- package/src/client/mermaid-blocks.ts +0 -110
- package/src/client/mermaid-sanitize.ts +0 -93
- package/src/client/mermaid.tsx +0 -406
- package/src/client/pdf-types.ts +0 -4
- package/src/client/plugins-viewers.ts +0 -30
- package/src/html-route.ts +0 -106
|
@@ -124,22 +124,6 @@ export interface SidebarPrefs {
|
|
|
124
124
|
* starts `strip` px below its top edge.
|
|
125
125
|
*/
|
|
126
126
|
titleBarStripPx: number;
|
|
127
|
-
/**
|
|
128
|
-
* Whether the HTML previewer drops its sandboxed iframe. Sandbox ON (the
|
|
129
|
-
* default) renders previewed HTML in an opaque-origin iframe that cannot
|
|
130
|
-
* touch the GUI; turning it OFF runs the previewed page with the GUI's
|
|
131
|
-
* own origin — full read/write access to session files and internal
|
|
132
|
-
* APIs. Only for trusted local content; the setting copy warns.
|
|
133
|
-
*/
|
|
134
|
-
htmlViewerNoSandbox: boolean;
|
|
135
|
-
/**
|
|
136
|
-
* Whether a newly opened HTML preview starts UNSANDBOXED (the per-surface
|
|
137
|
-
* temporary unlock pre-applied). Off by default: previews open sandboxed
|
|
138
|
-
* and the status row offers the one-tap unlock; when on, previews open
|
|
139
|
-
* in the red unsandboxed state and the status row offers a one-tap
|
|
140
|
-
* restore for the current file.
|
|
141
|
-
*/
|
|
142
|
-
htmlViewerDefaultUnsafe: boolean;
|
|
143
127
|
/**
|
|
144
128
|
* Whether the browser tab drops its sandboxed iframe. Sandbox ON (the
|
|
145
129
|
* default) keeps browsed sites in an opaque origin with no GUI access;
|
|
@@ -194,16 +178,9 @@ export interface SidebarPrefs {
|
|
|
194
178
|
* own layouts" rule.
|
|
195
179
|
*/
|
|
196
180
|
tabsEnabled: Record<string, boolean>;
|
|
197
|
-
/**
|
|
198
|
-
* Per-viewer enable switches, keyed by file viewer descriptor id
|
|
199
|
-
* (`'image'`, `'my-plugin:csv'`). An ABSENT key means enabled; a disabled
|
|
200
|
-
* viewer is skipped by `matchFileViewer` so files fall through to the
|
|
201
|
-
* next matching viewer (or the download button when none match).
|
|
202
|
-
*/
|
|
203
|
-
viewersEnabled: Record<string, boolean>;
|
|
204
181
|
/**
|
|
205
182
|
* Plugin-owned settings blobs (v0.12.0+), keyed by descriptor id: each
|
|
206
|
-
* registered tab
|
|
183
|
+
* registered tab that declares `settings.pluginToggles` (or writes
|
|
207
184
|
* through `settings.render`'s `updatePluginSetting`) persists its values
|
|
208
185
|
* here — an open map, so third-party keys need no host PrefsSchema field.
|
|
209
186
|
* Values are JSON-serializable (the row controls produce strings /
|
package/lib/types/wire.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { SidebarHttpRequest, SidebarHttpResponse } from './context-types.ts';
|
|
8
8
|
/** Machine-readable error codes of the sidebar API. */
|
|
9
|
-
export type SidebarErrorCode = 'bad-request' | 'not-found' | 'forbidden' | 'method-error' | 'too-large' | 'fs-error' | 'git-error' | 'pty-error' | 'pty-deps-missing' | 'job-error' | 'sidechat-error' | 'subagents-unavailable' | 'settings-rejected' | 'settings-conflict' | 'internal';
|
|
9
|
+
export type SidebarErrorCode = 'bad-request' | 'not-found' | 'forbidden' | 'method-error' | 'too-large' | 'fs-error' | 'git-error' | 'pty-error' | 'pty-deps-missing' | 'job-error' | 'cdp-down' | 'sidechat-error' | 'subagents-unavailable' | 'settings-rejected' | 'settings-conflict' | 'internal';
|
|
10
10
|
/** One API failure with its wire code and HTTP status. */
|
|
11
11
|
export declare class SidebarError extends Error {
|
|
12
12
|
readonly code: SidebarErrorCode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-coding-sidebar",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes the betterSidebar service for other plugins to register sidebar tabs and file viewers. KCoder-maintained fork of DSH-better-sidebar 0.17.2 (bottom panel removed, upstream-decoupled release line).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -61,7 +61,6 @@
|
|
|
61
61
|
"lib/client-registry.js",
|
|
62
62
|
"lib/client-terminal.js",
|
|
63
63
|
"lib/client-editor.js",
|
|
64
|
-
"lib/client-mermaid.js",
|
|
65
64
|
"lib/types/**/*.d.ts",
|
|
66
65
|
"src",
|
|
67
66
|
"scripts/install.sh",
|
|
@@ -85,18 +84,18 @@
|
|
|
85
84
|
"license": "MIT",
|
|
86
85
|
"peerDependencies": {
|
|
87
86
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
88
|
-
"@deepseek-ai/dsh-agent": "^0.1.5-rc.
|
|
89
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.
|
|
90
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.
|
|
91
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.
|
|
92
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.
|
|
93
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.
|
|
94
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.
|
|
95
|
-
"@deepseek-ai/dsh-llm": "^0.1.5-rc.
|
|
96
|
-
"@deepseek-ai/dsh-session": "^0.1.5-rc.
|
|
97
|
-
"@deepseek-ai/dsh-settings": "^0.1.5-rc.
|
|
98
|
-
"@deepseek-ai/dsh-subagent": "^0.1.5-rc.
|
|
99
|
-
"@deepseek-ai/dsh-tools": "^0.1.5-rc.
|
|
87
|
+
"@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
|
|
88
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
|
|
89
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
|
|
90
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
|
|
91
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
|
|
92
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
|
|
93
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
|
|
94
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
95
|
+
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
96
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
|
|
97
|
+
"@deepseek-ai/dsh-subagent": "^0.1.5-rc.2",
|
|
98
|
+
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
100
99
|
"@huanlin/dsh-plugin-better-locale": "^0.1.0",
|
|
101
100
|
"react": "^18.2.0",
|
|
102
101
|
"react-dom": "^18.2.0"
|
|
@@ -130,8 +129,6 @@
|
|
|
130
129
|
"@codemirror/view": "^6.43.8",
|
|
131
130
|
"@lezer/highlight": "^1.2.3",
|
|
132
131
|
"clsx": "^2.1.1",
|
|
133
|
-
"dompurify": "^3.4.14",
|
|
134
|
-
"mermaid": "^11.16.1",
|
|
135
132
|
"node-pty": "^1.1.0",
|
|
136
133
|
"react-icons": "5.7.0",
|
|
137
134
|
"rxjs": "^7.8.2",
|
|
@@ -139,20 +136,20 @@
|
|
|
139
136
|
"ws": "^8.18.0"
|
|
140
137
|
},
|
|
141
138
|
"devDependencies": {
|
|
142
|
-
"@deepseek-ai/dsh-invariants": "0.1.5-rc.
|
|
139
|
+
"@deepseek-ai/dsh-invariants": "0.1.5-rc.2",
|
|
143
140
|
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
|
|
144
|
-
"@deepseek-ai/dsh-agent": "0.1.5-rc.
|
|
145
|
-
"@deepseek-ai/dsh-client-locale": "0.1.5-rc.
|
|
146
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.
|
|
147
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.
|
|
148
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.
|
|
149
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.
|
|
150
|
-
"@deepseek-ai/dsh-host-webserver": "0.1.5-rc.
|
|
151
|
-
"@deepseek-ai/dsh-llm": "0.1.5-rc.
|
|
152
|
-
"@deepseek-ai/dsh-session": "0.1.5-rc.
|
|
153
|
-
"@deepseek-ai/dsh-settings": "0.1.5-rc.
|
|
154
|
-
"@deepseek-ai/dsh-subagent": "0.1.5-rc.
|
|
155
|
-
"@deepseek-ai/dsh-tools": "0.1.5-rc.
|
|
141
|
+
"@deepseek-ai/dsh-agent": "0.1.5-rc.2",
|
|
142
|
+
"@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
|
|
143
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.2",
|
|
144
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
|
|
145
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
|
|
146
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
|
|
147
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.5-rc.2",
|
|
148
|
+
"@deepseek-ai/dsh-llm": "0.1.5-rc.2",
|
|
149
|
+
"@deepseek-ai/dsh-session": "0.1.5-rc.2",
|
|
150
|
+
"@deepseek-ai/dsh-settings": "0.1.5-rc.2",
|
|
151
|
+
"@deepseek-ai/dsh-subagent": "0.1.5-rc.2",
|
|
152
|
+
"@deepseek-ai/dsh-tools": "0.1.5-rc.2",
|
|
156
153
|
"@huanlin/dsh-plugin-better-locale": "^0.1.0",
|
|
157
154
|
"@types/node": "^24.0.0",
|
|
158
155
|
"@types/react": "~18.3.1",
|
package/src/bundle-route.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
19
19
|
import type { Context, SidebarHttpRequest, SidebarHttpResponse } from './context-types.ts'
|
|
20
20
|
|
|
21
21
|
/** The chunk names the client may request (mirror of src/client/chunk-loader.ts). */
|
|
22
|
-
export const CHUNK_NAMES = ['terminal', 'editor'
|
|
22
|
+
export const CHUNK_NAMES = ['terminal', 'editor'] as const
|
|
23
23
|
export type ChunkName = (typeof CHUNK_NAMES)[number]
|
|
24
24
|
|
|
25
25
|
/** Directory of this host-half module (lib/ — the chunk scripts live next to it). */
|
|
@@ -24,12 +24,13 @@ import {
|
|
|
24
24
|
IconRefreshOutline14,
|
|
25
25
|
IconWarningOutline16,
|
|
26
26
|
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
27
|
-
import { VscLinkExternal } from 'react-icons/vsc'
|
|
27
|
+
import { VscLinkExternal, VscRemoteExplorer } from 'react-icons/vsc'
|
|
28
28
|
import { api } from './api.ts'
|
|
29
29
|
import { embeddabilityOf, isAllowedLoopbackUrl, normalizeBrowserUrl } from './browser.ts'
|
|
30
30
|
import { patchTab } from './state.ts'
|
|
31
31
|
import { SandboxStatusBar } from './SandboxStatusBar.tsx'
|
|
32
32
|
import { t } from './locales.ts'
|
|
33
|
+
import { LiveView } from './LiveView.tsx'
|
|
33
34
|
import type { TabComponentProps } from './service.ts'
|
|
34
35
|
import css from './sidebar.module.css'
|
|
35
36
|
|
|
@@ -101,6 +102,9 @@ export function BrowserView(props: TabComponentProps) {
|
|
|
101
102
|
const [embedBlocked, setEmbedBlocked] = useState<string | null>(null)
|
|
102
103
|
/** The user asked to load the refused site anyway (keeps the plain iframe). */
|
|
103
104
|
const [forceEmbed, setForceEmbed] = useState(false)
|
|
105
|
+
/** Agent 实况模式(CDP screencast):开启后本 tab 只显示 agent 无头
|
|
106
|
+
* 浏览器的实况画面,地址栏/iframe 暂停。会话态开关,不持久化。 */
|
|
107
|
+
const [live, setLive] = useState(false)
|
|
104
108
|
|
|
105
109
|
// Probe every navigation (address bar, history, restored path): when the
|
|
106
110
|
// target forbids embedding, show the reason + open-in-browser instead of
|
|
@@ -212,6 +216,16 @@ export function BrowserView(props: TabComponentProps) {
|
|
|
212
216
|
>
|
|
213
217
|
<IconLinkOutline14 />
|
|
214
218
|
</button>
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
className={css.iconButton}
|
|
222
|
+
aria-label={t('browserLive')}
|
|
223
|
+
title={t('browserLive')}
|
|
224
|
+
aria-pressed={live}
|
|
225
|
+
onClick={() => { setLive(value => !value) }}
|
|
226
|
+
>
|
|
227
|
+
<VscRemoteExplorer size={15} />
|
|
228
|
+
</button>
|
|
215
229
|
<button
|
|
216
230
|
type="button"
|
|
217
231
|
className={css.iconButton}
|
|
@@ -233,7 +247,9 @@ export function BrowserView(props: TabComponentProps) {
|
|
|
233
247
|
onUnlock={() => { setLocalUnlock(true) }}
|
|
234
248
|
onRestore={() => { setLocalUnlock(false) }}
|
|
235
249
|
/>
|
|
236
|
-
{
|
|
250
|
+
{live ? (
|
|
251
|
+
<LiveView />
|
|
252
|
+
) : url === undefined ? (
|
|
237
253
|
<div className={css.browserStart}>{t('browserStart')}</div>
|
|
238
254
|
) : embedBlocked !== null && !forceEmbed ? (
|
|
239
255
|
<BrowserEmbedBlocked
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The editor tab host: the single FILES WINDOW. It
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* The editor tab host: the single FILES WINDOW. It fetches a file's text
|
|
3
|
+
* through the host fs.read — text renders in the (chunk-lazy) sidebar text
|
|
4
|
+
* editor, a binary file renders the shared download pane. (v1.0.4: the
|
|
5
|
+
* file-viewer registry/matching was retired — file preview is the host's
|
|
6
|
+
* job now, the sidebar edits text only.) A tab without a path (the seeded
|
|
7
|
+
* "Files" home) renders an empty-state hint instead of the loading flow;
|
|
8
|
+
* that path-less window IS the file explorer.
|
|
8
9
|
*
|
|
9
10
|
* The chrome depends on the `editorExplorer` mode (read reactively so
|
|
10
11
|
* toggling it re-renders without a reload):
|
|
@@ -18,18 +19,14 @@
|
|
|
18
19
|
* The tree's context menu offers the explicit escapes in both modes: open
|
|
19
20
|
* in a new tab (per-path dedupe) or to the side (a fresh tab in a fresh
|
|
20
21
|
* rightward split of the current pane).
|
|
21
|
-
*
|
|
22
|
-
* The strategy dispatch is pure (planFirstMatch / planFsReadOutcome in
|
|
23
|
-
* editor-load.ts); this component only wires it to the host APIs.
|
|
24
22
|
*/
|
|
25
23
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
|
|
26
|
-
import {
|
|
24
|
+
import type { ComponentType } from 'react'
|
|
27
25
|
import clsx from 'clsx'
|
|
28
26
|
import { IconCheckOutline16, IconFolderOpen16, IconRefreshOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
29
27
|
import type { Context } from '../context-types.ts'
|
|
30
|
-
import { api,
|
|
28
|
+
import { api, type SessionScope } from './api.ts'
|
|
31
29
|
import { BinaryDownload } from './binary-download.tsx'
|
|
32
|
-
import { planFirstMatch, planFsReadOutcome, type EditorLoadAction } from './editor-load.ts'
|
|
33
30
|
import { baseName } from './FileTree.tsx'
|
|
34
31
|
import { createFrameBatcher } from './frame-batcher.ts'
|
|
35
32
|
import { openSidebarFile } from './intercept.tsx'
|
|
@@ -39,16 +36,27 @@ import { TreePanel } from './TreePanel.tsx'
|
|
|
39
36
|
import { t } from './locales.ts'
|
|
40
37
|
import { relativeTo } from './paths.ts'
|
|
41
38
|
import { resolveSidebarPath } from './produced-files.ts'
|
|
42
|
-
import type { EditorToolbarControls, EditorToolbarState
|
|
39
|
+
import type { EditorToolbarControls, EditorToolbarState } from './service.ts'
|
|
40
|
+
import { lazyChunkComponent } from './lazy-chunk.tsx'
|
|
41
|
+
import type { TextEditorProps } from './TextEditor.tsx'
|
|
43
42
|
import { firstLeaf, insertLeafAt, leafWithTab, mintTabId, treeOf, type SidebarStore, type SidebarTab } from './state.ts'
|
|
44
43
|
import css from './sidebar.module.css'
|
|
45
44
|
|
|
46
45
|
type EditorLoad =
|
|
47
46
|
| { status: 'loading' }
|
|
48
47
|
| { status: 'error'; message: string }
|
|
49
|
-
| { status: 'ready';
|
|
48
|
+
| { status: 'ready'; content: string; truncated: boolean }
|
|
50
49
|
| { status: 'binary' }
|
|
51
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Lazy wrapper over the chunk-resident text editor: CodeMirror and the
|
|
53
|
+
* language packs are fetched only when a file tab first renders (see
|
|
54
|
+
* chunk-loader.ts). The `pick` function is module-level (stable identity —
|
|
55
|
+
* the wrapper effect depends on it); the cast bridges the chunk exports
|
|
56
|
+
* record to the prop shape.
|
|
57
|
+
*/
|
|
58
|
+
const LazyTextEditor = lazyChunkComponent<TextEditorProps>('editor', (mod) => mod.TextEditor as ComponentType<TextEditorProps> | undefined)
|
|
59
|
+
|
|
52
60
|
/** The docked tree panel's width bounds (drag-resize clamps into them). */
|
|
53
61
|
const TREE_WIDTH_DEFAULT = 240
|
|
54
62
|
const TREE_WIDTH_MIN = 160
|
|
@@ -103,11 +111,10 @@ export function EditorHost(props: {
|
|
|
103
111
|
}) {
|
|
104
112
|
const { ctx, store, scope, tab, expanded, revealed, onToggleDir, onReferenceFile } = props
|
|
105
113
|
const path = tab.path ?? ''
|
|
106
|
-
const title = tab.title
|
|
107
114
|
// A folder window: the model's `sidebar_open` (or any caller) opens a
|
|
108
115
|
// directory as an editor tab carrying `meta.dir: true` with the directory
|
|
109
116
|
// as its path. It renders the file tree rooted at that folder instead of
|
|
110
|
-
// the
|
|
117
|
+
// the file loading flow (a directory is not a file).
|
|
111
118
|
const isDir = metaOf(tab).dir === true
|
|
112
119
|
const [load, setLoad] = useState<EditorLoad>({ status: 'loading' })
|
|
113
120
|
// Manual refresh (issue #167): bumping the sequence re-runs the load effect
|
|
@@ -223,9 +230,8 @@ export function EditorHost(props: {
|
|
|
223
230
|
})
|
|
224
231
|
}
|
|
225
232
|
|
|
226
|
-
// The
|
|
227
|
-
// its state and registers its commands (
|
|
228
|
-
// without a toolbar — image, pdf, binary download).
|
|
233
|
+
// The editor's toolbar, hoisted into THIS header: the text editor reports
|
|
234
|
+
// its state and registers its commands (null while nothing is loaded).
|
|
229
235
|
const [toolbar, setToolbar] = useState<EditorToolbarState | null>(null)
|
|
230
236
|
const controlsRef = useRef<EditorToolbarControls | null>(null)
|
|
231
237
|
const onToolbarState = useCallback((next: EditorToolbarState) => {
|
|
@@ -279,77 +285,27 @@ export function EditorHost(props: {
|
|
|
279
285
|
|
|
280
286
|
useEffect(() => {
|
|
281
287
|
// A (re)load or a path-less tab clears any hoisted toolbar state — the
|
|
282
|
-
// fresh
|
|
288
|
+
// fresh editor re-registers its own.
|
|
283
289
|
setToolbar(null)
|
|
284
|
-
// The seeded home tab (no path) never loads a
|
|
290
|
+
// The seeded home tab (no path) never loads a file — the empty-state
|
|
285
291
|
// hint renders until the user picks a file. A folder tab never loads a
|
|
286
|
-
//
|
|
292
|
+
// file either — its tree is rooted at the folder.
|
|
287
293
|
if (showEmpty || isDir) return
|
|
288
294
|
let cancelled = false
|
|
289
|
-
// Aborts the matched viewer's `load` when the editor tears down (tab
|
|
290
|
-
// closed, path changed, session switched) or re-matches the viewer.
|
|
291
|
-
const controller = new AbortController()
|
|
292
295
|
setLoad({ status: 'loading' })
|
|
293
|
-
|
|
294
|
-
const apply = (action: EditorLoadAction): void => {
|
|
296
|
+
api.fsRead(scope, path).then((result) => {
|
|
295
297
|
if (cancelled) return
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return
|
|
300
|
-
case 'render':
|
|
301
|
-
setLoad({
|
|
302
|
-
status: 'ready',
|
|
303
|
-
viewer: action.viewer,
|
|
304
|
-
content: action.content,
|
|
305
|
-
truncated: action.truncated,
|
|
306
|
-
mediaUrl: action.mediaUrl,
|
|
307
|
-
customData: action.customData,
|
|
308
|
-
})
|
|
309
|
-
return
|
|
310
|
-
case 'customLoad':
|
|
311
|
-
void action.viewer.load?.(path, scope, controller.signal).then((data) => {
|
|
312
|
-
if (cancelled) return
|
|
313
|
-
setLoad({ status: 'ready', viewer: action.viewer, customData: data })
|
|
314
|
-
}).catch((error: unknown) => {
|
|
315
|
-
if (cancelled) return
|
|
316
|
-
setLoad({ status: 'error', message: error instanceof Error ? error.message : String(error) })
|
|
317
|
-
})
|
|
318
|
-
return
|
|
319
|
-
case 'fetchFsRead':
|
|
320
|
-
api.fsRead(scope, path).then((result) => {
|
|
321
|
-
if (cancelled) return
|
|
322
|
-
// Binary reads carry the head bytes for the detect re-match.
|
|
323
|
-
const outcome = planFsReadOutcome(action.viewer, {
|
|
324
|
-
binary: result.kind === 'binary',
|
|
325
|
-
content: result.kind === 'text' ? result.content : '',
|
|
326
|
-
truncated: result.truncated,
|
|
327
|
-
head: result.kind === 'binary' ? result.head : undefined,
|
|
328
|
-
}, (head) => ctx.get('betterSidebar')?.matchFileViewer(path, head), mediaUrlOf)
|
|
329
|
-
apply(outcome)
|
|
330
|
-
}).catch((error: unknown) => {
|
|
331
|
-
if (cancelled) return
|
|
332
|
-
setLoad({ status: 'error', message: error instanceof Error ? error.message : String(error) })
|
|
333
|
-
})
|
|
334
|
-
return
|
|
298
|
+
if (result.kind === 'binary') {
|
|
299
|
+
setLoad({ status: 'binary' })
|
|
300
|
+
return
|
|
335
301
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
// a preview-mode Ctrl+S shows the fresh content immediately. Edit mode is
|
|
344
|
-
// left alone — reloading would remount the editor and drop the caret.
|
|
345
|
-
const prevSaveState = useRef<EditorToolbarState['saveState'] | undefined>(undefined)
|
|
346
|
-
useEffect(() => {
|
|
347
|
-
const current = toolbar?.saveState
|
|
348
|
-
if (prevSaveState.current !== 'saved' && current === 'saved' && toolbar?.mode === 'preview') {
|
|
349
|
-
setReloadSeq(sequence => sequence + 1)
|
|
350
|
-
}
|
|
351
|
-
prevSaveState.current = current
|
|
352
|
-
}, [toolbar?.saveState, toolbar?.mode])
|
|
302
|
+
setLoad({ status: 'ready', content: result.content, truncated: result.truncated })
|
|
303
|
+
}).catch((error: unknown) => {
|
|
304
|
+
if (cancelled) return
|
|
305
|
+
setLoad({ status: 'error', message: error instanceof Error ? error.message : String(error) })
|
|
306
|
+
})
|
|
307
|
+
return () => { cancelled = true }
|
|
308
|
+
}, [scope.sessionId, scope.cwd, path, showEmpty, isDir, reloadSeq])
|
|
353
309
|
|
|
354
310
|
const treeOpen = treeOpenOf(tab)
|
|
355
311
|
/** Persist the panel flag on the tab (survives reloads with the layout). */
|
|
@@ -392,33 +348,6 @@ export function EditorHost(props: {
|
|
|
392
348
|
<div className={css.editor}>
|
|
393
349
|
<div className={css.editorHeader}>
|
|
394
350
|
<EditorPathInput key={path} path={path} cwd={scope.cwd} onOpen={openFile} />
|
|
395
|
-
{toolbar?.modes === true && (
|
|
396
|
-
<div className={css.editorModeToggle}>
|
|
397
|
-
<button
|
|
398
|
-
type="button"
|
|
399
|
-
className={clsx(css.editorModeButton, toolbar.mode === 'preview' && css.editorModeActive)}
|
|
400
|
-
onClick={() => {
|
|
401
|
-
// Issue #167 part B: returning from edit to preview reloads so
|
|
402
|
-
// the preview renders the just-saved content. A dirty draft
|
|
403
|
-
// (or a failed save) suppresses the reload — the draft only
|
|
404
|
-
// lives in the editor instance and a remount would drop it.
|
|
405
|
-
if (toolbar.mode === 'edit' && toolbar.dirty !== true && toolbar.saveState !== 'failed') {
|
|
406
|
-
setReloadSeq(sequence => sequence + 1)
|
|
407
|
-
}
|
|
408
|
-
controlsRef.current?.setMode('preview')
|
|
409
|
-
}}
|
|
410
|
-
>
|
|
411
|
-
{t('preview')}
|
|
412
|
-
</button>
|
|
413
|
-
<button
|
|
414
|
-
type="button"
|
|
415
|
-
className={clsx(css.editorModeButton, toolbar.mode === 'edit' && css.editorModeActive)}
|
|
416
|
-
onClick={() => { controlsRef.current?.setMode('edit') }}
|
|
417
|
-
>
|
|
418
|
-
{t('edit')}
|
|
419
|
-
</button>
|
|
420
|
-
</div>
|
|
421
|
-
)}
|
|
422
351
|
{toolbar?.dirty === true && <span className={css.dirtyDot} title={t('unsaved')} />}
|
|
423
352
|
{toolbar?.editable === true && (
|
|
424
353
|
<button
|
|
@@ -462,18 +391,19 @@ export function EditorHost(props: {
|
|
|
462
391
|
{!showEmpty && load.status === 'loading' && <div className={css.editorPlaceholder}>{t('loading')}</div>}
|
|
463
392
|
{!showEmpty && load.status === 'error' && <div className={css.editorError}>{load.message}</div>}
|
|
464
393
|
{!showEmpty && load.status === 'binary' && <BinaryDownload scope={scope} path={path} />}
|
|
465
|
-
{!showEmpty && load.status === 'ready' &&
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
394
|
+
{!showEmpty && load.status === 'ready' && (
|
|
395
|
+
<LazyTextEditor
|
|
396
|
+
ctx={ctx}
|
|
397
|
+
store={store}
|
|
398
|
+
scope={scope}
|
|
399
|
+
path={path}
|
|
400
|
+
content={load.content}
|
|
401
|
+
truncated={load.truncated}
|
|
402
|
+
toolbar="host"
|
|
403
|
+
onToolbarState={onToolbarState}
|
|
404
|
+
onToolbarControls={onToolbarControls}
|
|
405
|
+
/>
|
|
406
|
+
)}
|
|
477
407
|
</div>
|
|
478
408
|
{treeOpen && (
|
|
479
409
|
<div className={css.editorTreeDock} style={{ width: treeWidth }}>
|