@lijian-ui/dsh-file-manager 0.1.20
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 +38 -0
- package/README.i18n.yaml +6 -0
- package/README.md +66 -0
- package/README.zh.md +66 -0
- package/cordis.patch.yml +13 -0
- package/lib/assets/mermaid.min.js +3587 -0
- package/lib/client.js +7621 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +3207 -0
- package/lib/tsconfig.client.tsbuildinfo +1 -0
- package/lib/tsconfig.host.tsbuildinfo +1 -0
- package/lib/types/client/FileManagerSettingsCard.d.ts +56 -0
- package/lib/types/client/FileManagerSettingsCard.d.ts.map +1 -0
- package/lib/types/client/PluginSettingsCard.d.ts +104 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
- package/lib/types/client/api.d.ts +75 -0
- package/lib/types/client/api.d.ts.map +1 -0
- package/lib/types/client/chat/AttachedFilesDock.d.ts +51 -0
- package/lib/types/client/chat/AttachedFilesDock.d.ts.map +1 -0
- package/lib/types/client/chat/file-ref.d.ts +35 -0
- package/lib/types/client/chat/file-ref.d.ts.map +1 -0
- package/lib/types/client/chat/mermaid-chat.d.ts +30 -0
- package/lib/types/client/chat/mermaid-chat.d.ts.map +1 -0
- package/lib/types/client/chat/placeholder-hint.d.ts +16 -0
- package/lib/types/client/chat/placeholder-hint.d.ts.map +1 -0
- package/lib/types/client/components/ExplorerPanel.d.ts +26 -0
- package/lib/types/client/components/ExplorerPanel.d.ts.map +1 -0
- package/lib/types/client/components/FileIcon.d.ts +15 -0
- package/lib/types/client/components/FileIcon.d.ts.map +1 -0
- package/lib/types/client/components/ScmPanel.d.ts +23 -0
- package/lib/types/client/components/ScmPanel.d.ts.map +1 -0
- package/lib/types/client/components/a11y.d.ts +12 -0
- package/lib/types/client/components/a11y.d.ts.map +1 -0
- package/lib/types/client/components/icons.d.ts +41 -0
- package/lib/types/client/components/icons.d.ts.map +1 -0
- package/lib/types/client/components/overlay.d.ts +45 -0
- package/lib/types/client/components/overlay.d.ts.map +1 -0
- package/lib/types/client/drag/DragFileInlay.d.ts +31 -0
- package/lib/types/client/drag/DragFileInlay.d.ts.map +1 -0
- package/lib/types/client/drag/file-drag.d.ts +47 -0
- package/lib/types/client/drag/file-drag.d.ts.map +1 -0
- package/lib/types/client/drag.d.ts +31 -0
- package/lib/types/client/drag.d.ts.map +1 -0
- package/lib/types/client/file-source.d.ts +29 -0
- package/lib/types/client/file-source.d.ts.map +1 -0
- package/lib/types/client/fileType.d.ts +31 -0
- package/lib/types/client/fileType.d.ts.map +1 -0
- package/lib/types/client/floating.d.ts +27 -0
- package/lib/types/client/floating.d.ts.map +1 -0
- package/lib/types/client/hooks/useResizableSplit.d.ts +37 -0
- package/lib/types/client/hooks/useResizableSplit.d.ts.map +1 -0
- package/lib/types/client/hooks/useStore.d.ts +10 -0
- package/lib/types/client/hooks/useStore.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +57 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/layout.d.ts +104 -0
- package/lib/types/client/layout.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +125 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/maximize.d.ts +24 -0
- package/lib/types/client/maximize.d.ts.map +1 -0
- package/lib/types/client/mention.d.ts +44 -0
- package/lib/types/client/mention.d.ts.map +1 -0
- package/lib/types/client/mount.d.ts +22 -0
- package/lib/types/client/mount.d.ts.map +1 -0
- package/lib/types/client/persist.d.ts +55 -0
- package/lib/types/client/persist.d.ts.map +1 -0
- package/lib/types/client/picker/FilePickerModal.d.ts +47 -0
- package/lib/types/client/picker/FilePickerModal.d.ts.map +1 -0
- package/lib/types/client/picker/file-picker.d.ts +62 -0
- package/lib/types/client/picker/file-picker.d.ts.map +1 -0
- package/lib/types/client/preview/PreviewPanel.d.ts +17 -0
- package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -0
- package/lib/types/client/preview/PreviewTabs.d.ts +30 -0
- package/lib/types/client/preview/PreviewTabs.d.ts.map +1 -0
- package/lib/types/client/preview/PreviewToolbar.d.ts +37 -0
- package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -0
- package/lib/types/client/preview/content.d.ts +43 -0
- package/lib/types/client/preview/content.d.ts.map +1 -0
- package/lib/types/client/preview/markdown.d.ts +60 -0
- package/lib/types/client/preview/markdown.d.ts.map +1 -0
- package/lib/types/client/preview/mermaid.d.ts +78 -0
- package/lib/types/client/preview/mermaid.d.ts.map +1 -0
- package/lib/types/client/preview/selection.d.ts +26 -0
- package/lib/types/client/preview/selection.d.ts.map +1 -0
- package/lib/types/client/reference.d.ts +75 -0
- package/lib/types/client/reference.d.ts.map +1 -0
- package/lib/types/client/settings-form.d.ts +206 -0
- package/lib/types/client/settings-form.d.ts.map +1 -0
- package/lib/types/client/store.d.ts +279 -0
- package/lib/types/client/store.d.ts.map +1 -0
- package/lib/types/core/types.d.ts +132 -0
- package/lib/types/core/types.d.ts.map +1 -0
- package/lib/types/host/fs-service.d.ts +96 -0
- package/lib/types/host/fs-service.d.ts.map +1 -0
- package/lib/types/host/gate.d.ts +45 -0
- package/lib/types/host/gate.d.ts.map +1 -0
- package/lib/types/host/git-runner.d.ts +80 -0
- package/lib/types/host/git-runner.d.ts.map +1 -0
- package/lib/types/host/git-service.d.ts +130 -0
- package/lib/types/host/git-service.d.ts.map +1 -0
- package/lib/types/host/loopback.d.ts +25 -0
- package/lib/types/host/loopback.d.ts.map +1 -0
- package/lib/types/host/poll-guard.d.ts +55 -0
- package/lib/types/host/poll-guard.d.ts.map +1 -0
- package/lib/types/host/routes.d.ts +54 -0
- package/lib/types/host/routes.d.ts.map +1 -0
- package/lib/types/index.d.ts +35 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/mount-once.d.ts +25 -0
- package/lib/types/mount-once.d.ts.map +1 -0
- package/package.json +85 -0
- package/src/client/FileManagerSettingsCard.tsx +117 -0
- package/src/client/PluginSettingsCard.tsx +337 -0
- package/src/client/api.ts +141 -0
- package/src/client/chat/file-ref.ts +144 -0
- package/src/client/chat/mermaid-chat.tsx +102 -0
- package/src/client/chat/placeholder-hint.tsx +46 -0
- package/src/client/components/ExplorerPanel.tsx +534 -0
- package/src/client/components/FileIcon.tsx +46 -0
- package/src/client/components/ScmPanel.tsx +480 -0
- package/src/client/components/a11y.ts +20 -0
- package/src/client/components/icons.tsx +274 -0
- package/src/client/components/overlay.tsx +225 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/drag/DragFileInlay.tsx +89 -0
- package/src/client/drag/file-drag.ts +67 -0
- package/src/client/drag.ts +154 -0
- package/src/client/file-source.ts +58 -0
- package/src/client/fileType.ts +129 -0
- package/src/client/floating.ts +63 -0
- package/src/client/hooks/useResizableSplit.ts +91 -0
- package/src/client/hooks/useStore.ts +15 -0
- package/src/client/index.ts +389 -0
- package/src/client/layout.ts +546 -0
- package/src/client/locales.ts +247 -0
- package/src/client/maximize.ts +34 -0
- package/src/client/mount.tsx +74 -0
- package/src/client/persist.ts +194 -0
- package/src/client/picker/FilePickerModal.tsx +506 -0
- package/src/client/picker/file-picker.ts +86 -0
- package/src/client/preview/PreviewPanel.tsx +233 -0
- package/src/client/preview/PreviewTabs.tsx +159 -0
- package/src/client/preview/PreviewToolbar.tsx +160 -0
- package/src/client/preview/content.tsx +617 -0
- package/src/client/preview/markdown.ts +351 -0
- package/src/client/preview/mermaid.ts +274 -0
- package/src/client/preview/selection.ts +82 -0
- package/src/client/reference.ts +161 -0
- package/src/client/settings-card.module.css +316 -0
- package/src/client/settings-form.ts +451 -0
- package/src/client/store.ts +1290 -0
- package/src/client/styles/chip.module.css +38 -0
- package/src/client/styles/drag.module.css +30 -0
- package/src/client/styles/explorer.module.css +371 -0
- package/src/client/styles/picker.module.css +344 -0
- package/src/client/styles/preview.module.css +904 -0
- package/src/client/styles/scm.module.css +380 -0
- package/src/client/styles/tokens.module.css +480 -0
- package/src/core/types.ts +162 -0
- package/src/host/fs-service.ts +513 -0
- package/src/host/gate.ts +74 -0
- package/src/host/git-runner.ts +116 -0
- package/src/host/git-service.ts +395 -0
- package/src/host/loopback.ts +63 -0
- package/src/host/poll-guard.ts +109 -0
- package/src/host/routes.ts +695 -0
- package/src/index.ts +97 -0
- package/src/mount-once.ts +48 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileManager right-panel system — browser half: mounts the explorer and preview
|
|
3
|
+
* columns into the web shell's frame grid (through the layout controller),
|
|
4
|
+
* binds the four stores to the live client runtime (the active session's cwd
|
|
5
|
+
* is the project root), subscribes to the host change stream (fs + git), and
|
|
6
|
+
* follows the shell's dark marker (body[data-ds-dark-theme]) via CSS only.
|
|
7
|
+
*
|
|
8
|
+
* Failure policy: every DOM/runtime wiring failure is logged, never thrown —
|
|
9
|
+
* the web shell fails the whole boot when a plugin apply throws.
|
|
10
|
+
*
|
|
11
|
+
* FileManager right-panel design (Apache-2.0, iOfficeAI/FileManager) — re-implemented
|
|
12
|
+
* from measured behavior and architecture, not copied code.
|
|
13
|
+
* @module dsh-filemgr/client
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createElement } from 'react'
|
|
17
|
+
import { createRoot } from 'react-dom/client'
|
|
18
|
+
import type { ClientContext, SessionId, SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-runtime/client'
|
|
19
|
+
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
20
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
21
|
+
// Type-only: pulls the official settings-scope service onto the client Context.
|
|
22
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
23
|
+
// Type-only: pulls the ui-conversation SlotMap merge (the input dock entry).
|
|
24
|
+
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
25
|
+
import { PanelApi, subscribePanelEvents } from './api.ts'
|
|
26
|
+
import { FileManagerSettingsCard, FileManagerSettingsCardController, type FileManagerPanelSettings } from './FileManagerSettingsCard.tsx'
|
|
27
|
+
import { PanelLayoutController } from './layout.ts'
|
|
28
|
+
import { createPanelStores, layoutSetRoot } from './store.ts'
|
|
29
|
+
import { mountPanels } from './mount.tsx'
|
|
30
|
+
import { NS, dictionaries, setLanguage, type FileManagerPanelKey } from './locales.ts'
|
|
31
|
+
import { DragFileInlay, type DragFileInjected } from './drag/DragFileInlay.tsx'
|
|
32
|
+
import { insertPathIntoDraft } from './drag/file-drag.ts'
|
|
33
|
+
import { MermaidChatEnhancer } from './chat/mermaid-chat.tsx'
|
|
34
|
+
import { PlaceholderHint } from './chat/placeholder-hint.tsx'
|
|
35
|
+
import { FilePickerHost } from './picker/FilePickerModal.tsx'
|
|
36
|
+
import { handleFileRefClick } from './chat/file-ref.ts'
|
|
37
|
+
import { appendToDraft, appendFileReferenceChip } from './reference.ts'
|
|
38
|
+
import { registerFileReferenceSource } from './file-source.ts'
|
|
39
|
+
import type { FileReferenceSelection } from './preview/selection.ts'
|
|
40
|
+
import './styles/chip.module.css'
|
|
41
|
+
|
|
42
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
43
|
+
interface LocaleNamespaceMap {
|
|
44
|
+
/** Panel surface copy. */
|
|
45
|
+
'filemgr': FileManagerPanelKey
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface SlotMap {
|
|
49
|
+
/**
|
|
50
|
+
* One family plugin card inside the Web UI Plugins group. Spelled here
|
|
51
|
+
* with the same shape so this package can register without depending on
|
|
52
|
+
* the sibling web-ui-settings package.
|
|
53
|
+
*/
|
|
54
|
+
'web-ui.plugin.item': { kind: 'list'; scope: 'root'; owner: SettingsPluginItemOwnerProps }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Owner share of a plugin card (the section supplies nothing). */
|
|
59
|
+
export interface SettingsPluginItemOwnerProps {
|
|
60
|
+
/** Marker field: card owner props are intentionally empty. */
|
|
61
|
+
children?: never
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
declare module '@deepseek-ai/cordis' {
|
|
65
|
+
interface Context {
|
|
66
|
+
/**
|
|
67
|
+
* Optional rc.6 compatibility binder provided by dsh-web-ui-settings;
|
|
68
|
+
* absent when that group plugin is not installed, so callers fall back to
|
|
69
|
+
* the official settings scope.
|
|
70
|
+
*/
|
|
71
|
+
webUiSettings?: { bind<S>(spec: SettingsScopeSpec<S>): SettingsScope<S> }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Required services: sessions for the project root, locale for the copy, the settings scope for the master switch, and conversation for the composer draft edits (picker insert / @ button). */
|
|
76
|
+
export const inject = ['sessions', 'locale', 'settingsScope', 'conversation']
|
|
77
|
+
|
|
78
|
+
/** Shared, stateless fs client (the PanelApi only wraps fetch calls). */
|
|
79
|
+
const panelApi = new PanelApi()
|
|
80
|
+
|
|
81
|
+
/** Apply the browser half. */
|
|
82
|
+
export function apply(ctx: ClientContext): void {
|
|
83
|
+
ctx.effect(() => ctx.locale.register(NS, dictionaries), 'dsh-filemgr: dictionaries')
|
|
84
|
+
|
|
85
|
+
// The `file` reference source: registers the codec that serializes file
|
|
86
|
+
// chips on submit (`@path:lines`), which the shell requires before any
|
|
87
|
+
// `slash/input-insert-reference` chip can send. Inert as a menu
|
|
88
|
+
// participant — the '@' tree modal is our own picker, not the shell menu.
|
|
89
|
+
ctx.effect(() => registerFileReferenceSource(ctx), 'dsh-filemgr: file reference source')
|
|
90
|
+
|
|
91
|
+
// The '@' picker is our own tree modal (pi-desktop style), NOT the shell's
|
|
92
|
+
// flat trigger menu: a bare `@` in the composer opens FilePickerHost
|
|
93
|
+
// directly. The one inputTriggers registration above is codec-only and
|
|
94
|
+
// inert in the menu; the modal covers any residual menu the moment it
|
|
95
|
+
// opens.
|
|
96
|
+
|
|
97
|
+
// The composer drop target for explorer file drags: mounted in the
|
|
98
|
+
// official `conversation.input.dock` band (declared by the shipped
|
|
99
|
+
// ui-conversation rc.6 shell), session-routed through the conversation
|
|
100
|
+
// input facade. A missing session scope or conversation service degrades
|
|
101
|
+
// to no-op — the panels themselves never depend on the dock entry.
|
|
102
|
+
ctx.inject(['slots', 'conversation', 'sessions'], (scope: ClientContext) => {
|
|
103
|
+
const sessions = scope.sessions
|
|
104
|
+
const conversation = scope.conversation
|
|
105
|
+
scope.slots.inject('conversation.input.dock', () =>
|
|
106
|
+
scope.slots.register({
|
|
107
|
+
name: 'conversation.input.dock',
|
|
108
|
+
id: 'filemgr-drag-file',
|
|
109
|
+
order: 90,
|
|
110
|
+
locale: NS,
|
|
111
|
+
inject: (sessionId: SessionId | undefined): DragFileInjected => ({
|
|
112
|
+
insertPath: (path: string): boolean => {
|
|
113
|
+
if (sessionId === undefined) return false
|
|
114
|
+
const actx = sessions.scope(sessionId)
|
|
115
|
+
if (actx === undefined) return false
|
|
116
|
+
const input = conversation.input
|
|
117
|
+
if (input === undefined) return false
|
|
118
|
+
const shell = input.for(actx)
|
|
119
|
+
const draft = shell.state.getSnapshot().draft
|
|
120
|
+
shell.setDraft(insertPathIntoDraft(draft, path))
|
|
121
|
+
return true
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
}, DragFileInlay))
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Transcript mermaid enhancement rides the same dock as a zero-render
|
|
128
|
+
// sentinel: the shell has no message-body slot, so the sentinel observes
|
|
129
|
+
// the document for the chat renderer's `code.language-mermaid` blocks.
|
|
130
|
+
ctx.inject(['slots'], (scope: ClientContext) => {
|
|
131
|
+
scope.slots.inject('conversation.input.dock', () =>
|
|
132
|
+
scope.slots.register({
|
|
133
|
+
name: 'conversation.input.dock',
|
|
134
|
+
id: 'filemgr-mermaid-chat',
|
|
135
|
+
order: 91,
|
|
136
|
+
}, MermaidChatEnhancer))
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Composer placeholder hint rides the same band: the shell's input bar
|
|
140
|
+
// placeholder ("给智能体发消息") has no plugin override seam, so a sentinel
|
|
141
|
+
// rewrites the DOM attribute to advertise the '@' file reference and the
|
|
142
|
+
// '/' command trigger (see placeholder-hint.tsx).
|
|
143
|
+
ctx.inject(['slots'], (scope: ClientContext) => {
|
|
144
|
+
scope.slots.inject('conversation.input.dock', () =>
|
|
145
|
+
scope.slots.register({
|
|
146
|
+
name: 'conversation.input.dock',
|
|
147
|
+
id: 'filemgr-placeholder-hint',
|
|
148
|
+
order: 92,
|
|
149
|
+
}, PlaceholderHint))
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// The settings card: one master switch (issue #307) in the Web UI Plugins
|
|
153
|
+
// group, bound to the 'filemgr' namespace through the family bridge
|
|
154
|
+
// (or the official settings scope when the deployment exposes it).
|
|
155
|
+
ctx.inject(['slots', 'settingsScope'], (settingsCtx: ClientContext) => {
|
|
156
|
+
const binder = settingsCtx.get('webUiSettings') ?? settingsCtx.settingsScope
|
|
157
|
+
const panelScope = binder.bind<FileManagerPanelSettings>({ namespace: NS })
|
|
158
|
+
const settingsCard = new FileManagerSettingsCardController(panelScope)
|
|
159
|
+
settingsCtx.slots.inject('web-ui.plugin.item', () => {
|
|
160
|
+
const unregister = settingsCtx.slots.register({
|
|
161
|
+
name: 'web-ui.plugin.item',
|
|
162
|
+
id: 'filemgr',
|
|
163
|
+
order: 110,
|
|
164
|
+
locale: NS,
|
|
165
|
+
inject: () => settingsCard.inject(),
|
|
166
|
+
}, FileManagerSettingsCard)
|
|
167
|
+
return () => {
|
|
168
|
+
settingsCard.dispose()
|
|
169
|
+
unregister()
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
ctx.effect(() => {
|
|
175
|
+
// Master switch (issue #307): the settings card edits the 'filemgr'
|
|
176
|
+
// namespace through the family settings bridge (or the official scope).
|
|
177
|
+
// While off, the panels, the floating button and the change stream stay
|
|
178
|
+
// unmounted; toggling the switch re-mounts them live (the pet's model).
|
|
179
|
+
let panelScope: SettingsScope<FileManagerPanelSettings> | undefined
|
|
180
|
+
try {
|
|
181
|
+
const binder = ctx.get('webUiSettings') ?? ctx.settingsScope
|
|
182
|
+
if (binder !== undefined) panelScope = binder.bind<FileManagerPanelSettings>({ namespace: NS })
|
|
183
|
+
} catch (error) {
|
|
184
|
+
// A missing settings seam must not break the panel boot: default on.
|
|
185
|
+
panelScope = undefined
|
|
186
|
+
}
|
|
187
|
+
const enabled = (): boolean => panelScope?.getSnapshot().value?.enabled ?? true
|
|
188
|
+
let disposeUi: (() => void) | undefined
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Mount the whole panel UI (columns, handles, floating button, change
|
|
192
|
+
* stream, persists) and return its teardown. A fresh lifecycle per
|
|
193
|
+
* enable keeps toggling idempotent (the layout controller cannot be
|
|
194
|
+
* reused after dispose).
|
|
195
|
+
*/
|
|
196
|
+
const mountUi = (): (() => void) => {
|
|
197
|
+
const stores = createPanelStores(panelApi)
|
|
198
|
+
const layout = new PanelLayoutController(stores.layout)
|
|
199
|
+
const disposers: Array<() => void> = []
|
|
200
|
+
let disposeGitEvents: (() => void) | undefined
|
|
201
|
+
let currentRoot = ''
|
|
202
|
+
let lastPreviewOpen = false
|
|
203
|
+
|
|
204
|
+
// The Explorer row '@' button: insert a file-reference chip for files
|
|
205
|
+
// (path + 1..N) or plain `@<rel-path>/` text for folders. File lines
|
|
206
|
+
// are fetched from the host so the chip can carry an accurate line
|
|
207
|
+
// range; failures fall back to plain text.
|
|
208
|
+
const onReference = (path: string, isDir: boolean): void => {
|
|
209
|
+
const sessionId = ctx.sessions.list.getSnapshot().current as SessionId | undefined
|
|
210
|
+
if (sessionId === undefined) {
|
|
211
|
+
console.warn('[dsh-filemgr] @ 引用插入失败:没有活动会话')
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
if (isDir) {
|
|
215
|
+
appendToDraft(ctx, sessionId, `@${path}/ `)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
void panelApi.read(currentRoot, path, false).then((result) => {
|
|
219
|
+
if (!result.ok) {
|
|
220
|
+
appendToDraft(ctx, sessionId, `@${path} `)
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
const text = result.value.content
|
|
224
|
+
const trimmed = text.endsWith('\n') ? text.slice(0, -1) : text
|
|
225
|
+
const lines = trimmed === '' ? 1 : trimmed.split('\n').length
|
|
226
|
+
appendFileReferenceChip(ctx, sessionId, { path, startLine: 1, endLine: lines })
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The preview "添加到对话" action: insert a file-reference chip
|
|
231
|
+
// carrying the selected line range, appended at the draft end.
|
|
232
|
+
const onAddFileReference = (selection: FileReferenceSelection): boolean => {
|
|
233
|
+
const sessionId = ctx.sessions.list.getSnapshot().current as SessionId | undefined
|
|
234
|
+
return appendFileReferenceChip(ctx, sessionId, selection)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// The project root follows the active session's cwd; switching sessions
|
|
238
|
+
// re-binds every store (widths, collapse, tree, tabs persist per root).
|
|
239
|
+
const bindRoot = (): void => {
|
|
240
|
+
const snapshot = ctx.sessions.list.getSnapshot()
|
|
241
|
+
const sessionId = snapshot.current as SessionId | undefined
|
|
242
|
+
const cwd = sessionId === undefined ? undefined : snapshot.byId[sessionId]?.cwd
|
|
243
|
+
const root = typeof cwd === 'string' && cwd !== '' ? cwd : ''
|
|
244
|
+
if (root === currentRoot) return
|
|
245
|
+
currentRoot = root
|
|
246
|
+
|
|
247
|
+
// The change stream is git-only and lives exactly as long as the SCM
|
|
248
|
+
// tab is visible (no fs watch — the explorer/preview refresh on user
|
|
249
|
+
// actions, see the host route layer). Drop the old stream on root
|
|
250
|
+
// switches; syncGitSubscription re-evaluates it below.
|
|
251
|
+
disposeGitEvents?.()
|
|
252
|
+
disposeGitEvents = undefined
|
|
253
|
+
const previewOpen = stores.preview.getSnapshot().open
|
|
254
|
+
lastPreviewOpen = previewOpen
|
|
255
|
+
layoutSetRoot(stores.layout, root, previewOpen)
|
|
256
|
+
stores.explorer.setRoot(root)
|
|
257
|
+
stores.scm.setRoot(root)
|
|
258
|
+
stores.preview.setRoot(root)
|
|
259
|
+
|
|
260
|
+
syncGitSubscription()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Keep the git change stream in lockstep with SCM-tab visibility:
|
|
265
|
+
* opening the changes tab starts the subscription (and fetches status
|
|
266
|
+
* once immediately — the host poll only ticks on its own cadence);
|
|
267
|
+
* switching away closes it, so git polling happens only while the SCM
|
|
268
|
+
* panel is actually on screen. Idempotent — explorer state changes
|
|
269
|
+
* (expansion, selection) call it and it no-ops unless the tab flipped.
|
|
270
|
+
*/
|
|
271
|
+
const syncGitSubscription = (): void => {
|
|
272
|
+
const root = currentRoot
|
|
273
|
+
const want = root !== '' && stores.explorer.getSnapshot().activeTab === 'changes'
|
|
274
|
+
if (want && disposeGitEvents === undefined) {
|
|
275
|
+
void stores.scm.refresh()
|
|
276
|
+
disposeGitEvents = subscribePanelEvents(root, (event) => {
|
|
277
|
+
if (event.kind === 'git') {
|
|
278
|
+
// The host status is the only truth; land it directly.
|
|
279
|
+
stores.scm.update((prev) => (prev.root !== root ? prev : { ...prev, status: event.status, loading: false }))
|
|
280
|
+
// The index/worktree moved: every open diff tab is stale by now.
|
|
281
|
+
void stores.preview.handleGitChange(root)
|
|
282
|
+
}
|
|
283
|
+
if (event.kind === 'gitUnavailable') {
|
|
284
|
+
// The host could not run git at all: land the friendly
|
|
285
|
+
// unavailable state once instead of a "not a repository" view.
|
|
286
|
+
stores.scm.update((prev) => (prev.root !== root ? prev : { ...prev, status: null, loading: false, gitMissing: true }))
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
} else if (!want && disposeGitEvents !== undefined) {
|
|
290
|
+
disposeGitEvents()
|
|
291
|
+
disposeGitEvents = undefined
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
disposers.push(ctx.sessions.list.subscribe(bindRoot))
|
|
295
|
+
disposers.push(stores.explorer.subscribe(syncGitSubscription))
|
|
296
|
+
bindRoot()
|
|
297
|
+
|
|
298
|
+
// Mirror the preview open state into the layout store (single source: the
|
|
299
|
+
// preview store), and play the enter animation when the region opens.
|
|
300
|
+
const mirrorPreviewOpen = (): void => {
|
|
301
|
+
const open = stores.preview.getSnapshot().open
|
|
302
|
+
if (open === lastPreviewOpen) return
|
|
303
|
+
lastPreviewOpen = open
|
|
304
|
+
stores.layout.update((prev) => ({ ...prev, previewOpen: open }))
|
|
305
|
+
if (open) {
|
|
306
|
+
const col = document.querySelector<HTMLElement>('[data-filemgr-preview-col]')
|
|
307
|
+
col?.classList.add('filemgr-preview-enter')
|
|
308
|
+
setTimeout(() => col?.classList.remove('filemgr-preview-enter'), 300)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
disposers.push(stores.preview.subscribe(mirrorPreviewOpen))
|
|
312
|
+
|
|
313
|
+
// Language mirroring (the shell owns <html lang>; the dictionary follows).
|
|
314
|
+
let langObserver: MutationObserver | undefined
|
|
315
|
+
const syncLanguage = (): void => {
|
|
316
|
+
setLanguage(document.documentElement.lang?.startsWith('zh') ? 'zh' : 'en')
|
|
317
|
+
}
|
|
318
|
+
langObserver = new MutationObserver(syncLanguage)
|
|
319
|
+
langObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] })
|
|
320
|
+
syncLanguage()
|
|
321
|
+
|
|
322
|
+
// Mount everything. DOM failures degrade the panels, never the GUI.
|
|
323
|
+
try {
|
|
324
|
+
layout.mount()
|
|
325
|
+
mountPanels(stores, () => layout.toggleExplorer(), onReference, onAddFileReference)
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error('[dsh-filemgr] mount failed:', error)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// The '@' tree-browser modal host: a dedicated root appended to body so
|
|
331
|
+
// the modal portals above the shell chrome regardless of panel state.
|
|
332
|
+
const pickerHostEl = document.createElement('div')
|
|
333
|
+
document.body.appendChild(pickerHostEl)
|
|
334
|
+
const pickerRoot = createRoot(pickerHostEl)
|
|
335
|
+
pickerRoot.render(createElement(FilePickerHost, { ctx, api: panelApi }))
|
|
336
|
+
|
|
337
|
+
// Chat file-reference clicks (issue #314): recognize workspace paths in
|
|
338
|
+
// transcript code spans and locate them in the Explorer / Preview.
|
|
339
|
+
const onFileRefClick = (event: MouseEvent): void => {
|
|
340
|
+
try {
|
|
341
|
+
handleFileRefClick(stores, panelApi, event)
|
|
342
|
+
} catch (error) {
|
|
343
|
+
// A broken locate must never break the transcript's own clicks.
|
|
344
|
+
console.error('[dsh-filemgr] file ref locate failed:', error)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
document.addEventListener('click', onFileRefClick)
|
|
348
|
+
|
|
349
|
+
// Debounced persists (explorer/scm/preview) may be pending when the page
|
|
350
|
+
// hides; flush them so a close/background never drops the last 150ms.
|
|
351
|
+
const flushOnHide = (): void => stores.flushNow()
|
|
352
|
+
const onVisibilityChange = (): void => {
|
|
353
|
+
if (document.visibilityState === 'hidden') flushOnHide()
|
|
354
|
+
}
|
|
355
|
+
window.addEventListener('pagehide', flushOnHide)
|
|
356
|
+
document.addEventListener('visibilitychange', onVisibilityChange)
|
|
357
|
+
|
|
358
|
+
return () => {
|
|
359
|
+
flushOnHide()
|
|
360
|
+
window.removeEventListener('pagehide', flushOnHide)
|
|
361
|
+
document.removeEventListener('visibilitychange', onVisibilityChange)
|
|
362
|
+
document.removeEventListener('click', onFileRefClick)
|
|
363
|
+
disposeGitEvents?.()
|
|
364
|
+
langObserver?.disconnect()
|
|
365
|
+
pickerRoot.unmount()
|
|
366
|
+
pickerHostEl.remove()
|
|
367
|
+
for (const dispose of disposers) dispose()
|
|
368
|
+
layout.dispose()
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const syncUi = (): void => {
|
|
372
|
+
if (enabled() && disposeUi === undefined) {
|
|
373
|
+
disposeUi = mountUi()
|
|
374
|
+
} else if (!enabled() && disposeUi !== undefined) {
|
|
375
|
+
disposeUi()
|
|
376
|
+
disposeUi = undefined
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
syncUi()
|
|
380
|
+
const unsubscribeSettings = panelScope?.subscribe(syncUi)
|
|
381
|
+
return () => {
|
|
382
|
+
unsubscribeSettings?.()
|
|
383
|
+
if (disposeUi !== undefined) {
|
|
384
|
+
disposeUi()
|
|
385
|
+
disposeUi = undefined
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}, 'dsh-filemgr: wiring')
|
|
389
|
+
}
|