@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,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat file-reference click handling (issue #314): the transcript renders
|
|
3
|
+
* workspace file paths as inline code, and clicking one should locate the
|
|
4
|
+
* file in the Explorer (switch to the Files tab, expand the ancestor chain,
|
|
5
|
+
* select the node) and open it in the Preview panel — directories are
|
|
6
|
+
* reveal-only. A document-level click listener (wired in the client apply)
|
|
7
|
+
* recognizes a conservative subset: a single-line `code` element whose text
|
|
8
|
+
* is a workspace-relative path or an absolute path under the project root.
|
|
9
|
+
* Links (`a`), multi-line fences, URLs, escaped (`..`) paths and anything
|
|
10
|
+
* inside the panels' own subtrees keep their existing behavior.
|
|
11
|
+
* @module dsh-filemgr/client/chat/file-ref
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { PanelApi } from '../api.ts'
|
|
15
|
+
import type { PanelStores } from '../store.ts'
|
|
16
|
+
import { parentRel } from '../fileType.ts'
|
|
17
|
+
|
|
18
|
+
/** Longest candidate text (arbitrary safety cap against huge code spans). */
|
|
19
|
+
const MAX_REF_LENGTH = 512
|
|
20
|
+
|
|
21
|
+
/** Normalize both separators to '/'. */
|
|
22
|
+
function normalizeSlashes(value: string): string {
|
|
23
|
+
return value.replace(/\\/g, '/')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Interpret a code span's text as a workspace path. Returns the workspace
|
|
28
|
+
* RELATIVE path ('' = the root itself), or null when the text is not a
|
|
29
|
+
* recognizable in-workspace path. Never resolves above the root: `..`
|
|
30
|
+
* segments, absolute outside paths, URLs and whitespace/multi-line text all
|
|
31
|
+
* fall back to null so the transcript keeps its normal behavior.
|
|
32
|
+
*/
|
|
33
|
+
export function pathFromText(text: string, root: string): string | null {
|
|
34
|
+
const raw = text.trim()
|
|
35
|
+
if (raw === '' || raw.length > MAX_REF_LENGTH) return null
|
|
36
|
+
if (/[\r\n]/.test(raw)) return null
|
|
37
|
+
if (raw.includes('://')) return null
|
|
38
|
+
const normalized = normalizeSlashes(raw)
|
|
39
|
+
const rootNorm = normalizeSlashes(root).replace(/\/+$/, '')
|
|
40
|
+
if (rootNorm === '') return null
|
|
41
|
+
|
|
42
|
+
// Absolute path inside the workspace. Posix paths start with '/'
|
|
43
|
+
// (case-sensitive prefix match); Windows drive paths carry a drive
|
|
44
|
+
// letter (case-insensitive prefix match with a separator boundary).
|
|
45
|
+
if (normalized.startsWith('/')) {
|
|
46
|
+
if (normalized === rootNorm) return ''
|
|
47
|
+
if (!normalized.startsWith(rootNorm + '/')) return null
|
|
48
|
+
const rel = normalized.slice(rootNorm.length).replace(/^\/+/, '')
|
|
49
|
+
return validRelative(rel) ? rel : null
|
|
50
|
+
}
|
|
51
|
+
if (/^[a-zA-Z]:/.test(normalized)) {
|
|
52
|
+
const lower = normalized.toLowerCase()
|
|
53
|
+
const lowerRoot = rootNorm.toLowerCase()
|
|
54
|
+
if (lower === lowerRoot) return ''
|
|
55
|
+
if (!lower.startsWith(lowerRoot + '/')) return null
|
|
56
|
+
const rel = normalized.slice(rootNorm.length).replace(/^\/+/, '')
|
|
57
|
+
return validRelative(rel) ? rel : null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Workspace-relative path ('.' prefix stripped).
|
|
61
|
+
const rel = normalized.startsWith('./') ? normalized.slice(2) : normalized
|
|
62
|
+
return validRelative(rel) ? rel : null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Validate a relative path: no escapes, no whitespace, looks path-like. */
|
|
66
|
+
function validRelative(rel: string): boolean {
|
|
67
|
+
if (rel === '' || rel.length > MAX_REF_LENGTH) return false
|
|
68
|
+
if (/\s/.test(rel)) return false
|
|
69
|
+
const segments = rel.split('/')
|
|
70
|
+
if (segments.length < 2) {
|
|
71
|
+
// A bare name is accepted only when it looks like a file (dotted), so
|
|
72
|
+
// ordinary words in code spans never trigger a locate.
|
|
73
|
+
return /^[^/]*\.[^/]+$/.test(rel)
|
|
74
|
+
}
|
|
75
|
+
for (const segment of segments) {
|
|
76
|
+
if (segment === '' || segment === '.' || segment === '..') return false
|
|
77
|
+
}
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The `code` element a click targets, when it is a candidate file ref. */
|
|
82
|
+
export function fileRefElement(target: EventTarget | null): HTMLElement | null {
|
|
83
|
+
if (!(target instanceof Element)) return null
|
|
84
|
+
const code = target.closest('code')
|
|
85
|
+
if (code === null) return null
|
|
86
|
+
// Links keep their own navigation; the panels' own code blocks (preview
|
|
87
|
+
// editors, tree rows) never trigger the transcript locate.
|
|
88
|
+
if (code.closest('a') !== null) return null
|
|
89
|
+
if (code.closest('.filemgr-root') !== null) return null
|
|
90
|
+
if (code.closest('[data-filemgr-preview-col], [data-filemgr-explorer-col]') !== null) return null
|
|
91
|
+
return code
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Locate a workspace-relative path from the transcript: Files tab + expand
|
|
96
|
+
* ancestors + select; directories stay reveal-only, files also open in the
|
|
97
|
+
* Preview panel (dedup focuses the existing tab). The parent listing is
|
|
98
|
+
* consulted to classify the node; unknown paths keep the reveal and never
|
|
99
|
+
* issue a preview request.
|
|
100
|
+
*/
|
|
101
|
+
export async function locateFileRef(stores: PanelStores, api: PanelApi, rel: string): Promise<void> {
|
|
102
|
+
const explorer = stores.explorer
|
|
103
|
+
explorer.setActiveTab('files')
|
|
104
|
+
if (rel !== '') explorer.reveal(rel)
|
|
105
|
+
|
|
106
|
+
const root = stores.layout.getSnapshot().root
|
|
107
|
+
if (root === '' || rel === '') return
|
|
108
|
+
|
|
109
|
+
const name = rel.split('/').pop() ?? ''
|
|
110
|
+
const parent = parentRel(rel)
|
|
111
|
+
let entry = explorer.getSnapshot().dirs[parent]?.find((item) => item.name === name)
|
|
112
|
+
if (entry === undefined) {
|
|
113
|
+
const result = await api.list(root, parent)
|
|
114
|
+
if (result.ok) entry = result.value.entries.find((item) => item.name === name)
|
|
115
|
+
}
|
|
116
|
+
// Known directories: reveal-only (their children list is the tree's job).
|
|
117
|
+
// Known files: open in the preview panel. Unknown: no preview request.
|
|
118
|
+
if (entry !== undefined && !entry.isDir) {
|
|
119
|
+
stores.preview.openFile(root, rel)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Document-level click handler: locate recognized chat file references. */
|
|
124
|
+
export function handleFileRefClick(stores: PanelStores, api: PanelApi, event: MouseEvent): void {
|
|
125
|
+
if (event.defaultPrevented || event.button !== 0) return
|
|
126
|
+
const code = fileRefElement(event.target)
|
|
127
|
+
if (code === null) return
|
|
128
|
+
const root = stores.layout.getSnapshot().root
|
|
129
|
+
if (root === '') return
|
|
130
|
+
const rel = pathFromText(code.textContent ?? '', root)
|
|
131
|
+
if (rel === null) return
|
|
132
|
+
// The explorer must be visible for the reveal to be seen: un-collapse the
|
|
133
|
+
// column (persisted like the chevron toggle does).
|
|
134
|
+
const layout = stores.layout.getSnapshot()
|
|
135
|
+
if (layout.explorerCollapsed) {
|
|
136
|
+
stores.layout.update((prev) => ({ ...prev, explorerCollapsed: false }))
|
|
137
|
+
try {
|
|
138
|
+
localStorage.setItem(`project-panel-collapse:${root}`, 'expanded')
|
|
139
|
+
} catch {
|
|
140
|
+
// best-effort
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
void locateFileRef(stores, api, rel)
|
|
144
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat-transcript mermaid enhancement: the core conversation renderer emits
|
|
3
|
+
* fenced code as `pre > code.language-mermaid`, and the shell has no slot
|
|
4
|
+
* for message-body post-processing — so this component rides the
|
|
5
|
+
* conversation input dock as a zero-render sentinel and observes the
|
|
6
|
+
* document for mermaid blocks the transcript mounts. Blocks inside the
|
|
7
|
+
* preview panel's own subtree are excluded (each surface owns its blocks).
|
|
8
|
+
*
|
|
9
|
+
* Streaming awareness: an assistant message re-renders continuously, so a
|
|
10
|
+
* diagram fence is often incomplete mid-stream. Renders that fail restore
|
|
11
|
+
* the block and the next mutation retries it — once the fence closes the
|
|
12
|
+
* diagram lands. Mutations are debounced to one rAF so long transcripts do
|
|
13
|
+
* not re-scan the whole document: each batch is mapped to the minimal
|
|
14
|
+
* mutated subtrees and scoped per-frame while the first scheduled pass scans
|
|
15
|
+
* the body once. The observer is disconnected on unmount.
|
|
16
|
+
* @module dsh-filemgr/client/chat/mermaid-chat
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useEffect } from 'react'
|
|
20
|
+
import type { JSX } from 'react'
|
|
21
|
+
import { DATA_MD_SCOPE, enhanceMermaidBlocks, mermaidTheme, rethemeMermaidBlocks, shellIsDark, watchShellTheme } from '../preview/mermaid.ts'
|
|
22
|
+
import previewCss from '../styles/preview.module.css'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Map a mutation batch to the minimal scan scopes that may contain new
|
|
26
|
+
* mermaid fences. Each record contributes its target and its added nodes
|
|
27
|
+
* (an added element directly; otherwise that node's parentElement), deduped
|
|
28
|
+
* by identity. Disconnected nodes and removed-only records yield nothing —
|
|
29
|
+
* removal never introduces a fence. Pure (DOM-read only) so tests can drive
|
|
30
|
+
* it in jsdom.
|
|
31
|
+
*/
|
|
32
|
+
export function enhanceScopesFor(records: MutationRecord[]): Element[] {
|
|
33
|
+
const scopes = new Set<Element>()
|
|
34
|
+
for (const record of records) {
|
|
35
|
+
// Removal-only batches do not introduce a fence: without additions the
|
|
36
|
+
// target scan is wasted, so only records carrying added nodes contribute.
|
|
37
|
+
if (record.addedNodes.length === 0) continue
|
|
38
|
+
if (record.target instanceof Element && record.target.isConnected) {
|
|
39
|
+
scopes.add(record.target)
|
|
40
|
+
}
|
|
41
|
+
for (const node of record.addedNodes) {
|
|
42
|
+
const element = node instanceof Element ? node : node.parentElement
|
|
43
|
+
if (element !== null && element.isConnected) scopes.add(element)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return Array.from(scopes)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Hidden sentinel: renders nothing, owns the transcript observer. */
|
|
50
|
+
export function MermaidChatEnhancer(): JSX.Element | null {
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
let scheduled = false
|
|
53
|
+
let pendingFrame = 0
|
|
54
|
+
let firstPass = true
|
|
55
|
+
let pendingRecords: MutationRecord[] = []
|
|
56
|
+
const run = (): void => {
|
|
57
|
+
scheduled = false
|
|
58
|
+
const records = pendingRecords
|
|
59
|
+
pendingRecords = []
|
|
60
|
+
const scopes = enhanceScopesFor(records)
|
|
61
|
+
if (firstPass) {
|
|
62
|
+
// First scheduled pass only: scan the whole document exactly once.
|
|
63
|
+
// Later batches (even removal-only ones that yield no scopes) never
|
|
64
|
+
// fall back to a full-body scan.
|
|
65
|
+
firstPass = false
|
|
66
|
+
void enhanceMermaidBlocks(document.body, {
|
|
67
|
+
className: previewCss.mermaidBlock,
|
|
68
|
+
theme: mermaidTheme(shellIsDark()),
|
|
69
|
+
skip: (pre) => pre.closest(`[${DATA_MD_SCOPE}]`) !== null,
|
|
70
|
+
})
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
for (const scope of scopes) {
|
|
74
|
+
void enhanceMermaidBlocks(scope, {
|
|
75
|
+
className: previewCss.mermaidBlock,
|
|
76
|
+
theme: mermaidTheme(shellIsDark()),
|
|
77
|
+
skip: (pre) => pre.closest(`[${DATA_MD_SCOPE}]`) !== null,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const schedule = (): void => {
|
|
82
|
+
if (scheduled) return
|
|
83
|
+
scheduled = true
|
|
84
|
+
pendingFrame = requestAnimationFrame(run)
|
|
85
|
+
}
|
|
86
|
+
const observer = new MutationObserver((records) => {
|
|
87
|
+
pendingRecords = pendingRecords.concat(records)
|
|
88
|
+
schedule()
|
|
89
|
+
})
|
|
90
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
91
|
+
schedule()
|
|
92
|
+
const disposeTheme = watchShellTheme((isDark) => {
|
|
93
|
+
void rethemeMermaidBlocks(document.body, { theme: mermaidTheme(isDark) })
|
|
94
|
+
})
|
|
95
|
+
return () => {
|
|
96
|
+
observer.disconnect()
|
|
97
|
+
disposeTheme()
|
|
98
|
+
cancelAnimationFrame(pendingFrame)
|
|
99
|
+
}
|
|
100
|
+
}, [])
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer placeholder hint: the shell owns the input-bar placeholder
|
|
3
|
+
* ("给智能体发消息") and exposes no override seam (the composer bar is a
|
|
4
|
+
* single slot owned by ui-conversation, and its locale namespace rejects
|
|
5
|
+
* duplicate registration), so this zero-render sentinel — riding the same
|
|
6
|
+
* `conversation.input.dock` band as the mermaid enhancer — rewrites the
|
|
7
|
+
* placeholder attribute in the DOM. It re-applies on any placeholder
|
|
8
|
+
* attribute change or node insert, which covers React re-renders that
|
|
9
|
+
* reset the attribute to the shell default. English UI is left untouched
|
|
10
|
+
* (exact-match on the Chinese string only).
|
|
11
|
+
* @module dsh-filemgr/client/chat/placeholder-hint
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect } from 'react'
|
|
15
|
+
import type { JSX } from 'react'
|
|
16
|
+
|
|
17
|
+
const OLD_PLACEHOLDER = '给智能体发消息'
|
|
18
|
+
const NEW_PLACEHOLDER = '给智能体发消息。@引用项目文件,/ 调用技能与指令'
|
|
19
|
+
|
|
20
|
+
/** Rewrite every composer input whose placeholder matches the shell default. */
|
|
21
|
+
function apply(): void {
|
|
22
|
+
for (const el of document.querySelectorAll<HTMLTextAreaElement | HTMLInputElement>('textarea, input[type="text"]')) {
|
|
23
|
+
if (el.placeholder === OLD_PLACEHOLDER) el.placeholder = NEW_PLACEHOLDER
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Hidden sentinel: renders nothing, owns the placeholder rewriting observer. */
|
|
28
|
+
export function PlaceholderHint(): JSX.Element | null {
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
apply()
|
|
31
|
+
const observer = new MutationObserver((records) => {
|
|
32
|
+
const relevant = records.some(
|
|
33
|
+
(record) => record.type === 'attributes' || record.addedNodes.length > 0,
|
|
34
|
+
)
|
|
35
|
+
if (relevant) apply()
|
|
36
|
+
})
|
|
37
|
+
observer.observe(document.body, {
|
|
38
|
+
attributes: true,
|
|
39
|
+
attributeFilter: ['placeholder'],
|
|
40
|
+
childList: true,
|
|
41
|
+
subtree: true,
|
|
42
|
+
})
|
|
43
|
+
return () => { observer.disconnect() }
|
|
44
|
+
}, [])
|
|
45
|
+
return null
|
|
46
|
+
}
|