@lijian-ui/dsh-file-manager 0.2.3 → 0.2.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 +38 -38
- package/README.i18n.yaml +6 -6
- package/README.md +66 -66
- package/README.zh.md +66 -66
- package/cordis.patch.yml +13 -13
- package/lib/client.js +27305 -270
- package/lib/client.js.map +1 -1
- package/lib/index.js +474 -42
- package/lib/tsconfig.client.tsbuildinfo +1 -1
- package/lib/tsconfig.host.tsbuildinfo +1 -1
- package/lib/types/client/file-source.d.ts +10 -0
- package/lib/types/client/file-source.d.ts.map +1 -1
- package/lib/types/client/fileType.d.ts +19 -4
- package/lib/types/client/fileType.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +3 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/mount.d.ts +1 -1
- package/lib/types/client/mount.d.ts.map +1 -1
- package/lib/types/client/preview/PreviewPanel.d.ts +2 -1
- package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -1
- package/lib/types/client/preview/PreviewToolbar.d.ts +3 -1
- package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -1
- package/lib/types/client/preview/content.d.ts +3 -1
- package/lib/types/client/preview/content.d.ts.map +1 -1
- package/lib/types/client/preview/office.d.ts +23 -0
- package/lib/types/client/preview/office.d.ts.map +1 -0
- package/lib/types/client/reference.d.ts +17 -0
- package/lib/types/client/reference.d.ts.map +1 -1
- package/lib/types/client/store.d.ts.map +1 -1
- package/lib/types/host/fs-service.d.ts.map +1 -1
- package/lib/types/host/office-preview.d.ts +13 -0
- package/lib/types/host/office-preview.d.ts.map +1 -0
- package/lib/types/host/routes.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +19 -4
- package/src/client/FileManagerSettingsCard.tsx +117 -117
- package/src/client/PluginSettingsCard.tsx +337 -337
- package/src/client/chat/file-ref.ts +144 -144
- package/src/client/chat/mermaid-chat.tsx +102 -102
- package/src/client/chat/placeholder-hint.tsx +46 -46
- package/src/client/components/ExplorerPanel.tsx +534 -534
- package/src/client/components/FileIcon.tsx +46 -46
- package/src/client/components/ScmPanel.tsx +480 -480
- package/src/client/components/a11y.ts +20 -20
- package/src/client/components/icons.tsx +274 -274
- package/src/client/components/overlay.tsx +225 -225
- package/src/client/drag/DragFileInlay.tsx +89 -89
- package/src/client/drag/file-drag.ts +67 -67
- package/src/client/drag.ts +154 -154
- package/src/client/file-source.ts +34 -0
- package/src/client/fileType.ts +32 -5
- package/src/client/hooks/useResizableSplit.ts +91 -91
- package/src/client/hooks/useStore.ts +15 -15
- package/src/client/index.ts +42 -3
- package/src/client/layout.ts +523 -523
- package/src/client/locales.ts +6 -0
- package/src/client/maximize.ts +34 -34
- package/src/client/mount.tsx +2 -1
- package/src/client/persist.ts +194 -194
- package/src/client/picker/FilePickerModal.tsx +526 -526
- package/src/client/picker/file-picker.ts +86 -86
- package/src/client/preview/PreviewPanel.tsx +7 -1
- package/src/client/preview/PreviewTabs.tsx +159 -159
- package/src/client/preview/PreviewToolbar.tsx +3 -2
- package/src/client/preview/content.tsx +7 -2
- package/src/client/preview/markdown.ts +351 -351
- package/src/client/preview/mermaid.ts +274 -274
- package/src/client/preview/office.tsx +321 -0
- package/src/client/reference.ts +49 -0
- package/src/client/settings-card.module.css +316 -316
- package/src/client/settings-form.ts +451 -451
- package/src/client/store.ts +17 -12
- package/src/client/styles/drag.module.css +30 -30
- package/src/client/styles/explorer.module.css +371 -371
- package/src/client/styles/picker.module.css +344 -344
- package/src/client/styles/preview.module.css +3 -1
- package/src/client/styles/scm.module.css +380 -380
- package/src/client/styles/tokens.module.css +376 -371
- package/src/core/types.ts +162 -162
- package/src/host/fs-service.ts +7 -1
- package/src/host/gate.ts +73 -73
- package/src/host/git-runner.ts +116 -116
- package/src/host/git-service.ts +395 -395
- package/src/host/loopback.ts +63 -63
- package/src/host/office-preview.ts +399 -0
- package/src/host/poll-guard.ts +109 -109
- package/src/host/routes.ts +72 -1
- package/src/index.ts +3 -1
- package/src/mount-once.ts +48 -48
- package/lib/types/client/chat/AttachedFilesDock.d.ts +0 -51
- package/lib/types/client/chat/AttachedFilesDock.d.ts.map +0 -1
- package/lib/types/client/floating.d.ts +0 -27
- package/lib/types/client/floating.d.ts.map +0 -1
- package/lib/types/client/mention.d.ts +0 -44
- package/lib/types/client/mention.d.ts.map +0 -1
package/src/host/poll-guard.ts
CHANGED
|
@@ -1,109 +1,109 @@
|
|
|
1
|
-
// Generated by scripts/sync-shared.mjs from shared/host/poll-guard.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
-
/**
|
|
3
|
-
* Poll-loop guard shared by the plugin family's Host halves (git-graph,
|
|
4
|
-
* filemgr): one bounded refresh loop that never overlaps itself,
|
|
5
|
-
* backs off on consecutive failures, and stops at a global deadline. Pure
|
|
6
|
-
* logic with injectable timers, so consumers test it without wall clocks.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/** Timer injection seam; defaults to the global setTimeout/clearTimeout pair. */
|
|
10
|
-
export interface PollTimers {
|
|
11
|
-
/** Schedule fn(ms). */
|
|
12
|
-
set: (fn: () => void, ms: number) => unknown
|
|
13
|
-
/** Cancel a scheduled handle. */
|
|
14
|
-
clear: (handle: unknown) => void
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const DEFAULT_TIMERS: PollTimers = {
|
|
18
|
-
set: (fn, ms) => setTimeout(fn, ms),
|
|
19
|
-
clear: (handle) => { clearTimeout(handle as ReturnType<typeof setTimeout>) },
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface PollGuardOptions {
|
|
23
|
-
/** Base interval between runs. */
|
|
24
|
-
intervalMs: number
|
|
25
|
-
/** Runs stop entirely once this much wall time passed since start(). */
|
|
26
|
-
deadlineMs: number
|
|
27
|
-
/** Cap for the failure backoff; each consecutive failure doubles the delay up to this. */
|
|
28
|
-
maxBackoffMs: number
|
|
29
|
-
/** Optional timer seam for tests. */
|
|
30
|
-
timers?: PollTimers
|
|
31
|
-
/** Invoked once when the deadline fires; no run starts afterwards. */
|
|
32
|
-
onDeadline?: () => void
|
|
33
|
-
/** Invoked after a run settles with the consecutive-failure count. */
|
|
34
|
-
onSettled?: (consecutiveFailures: number) => void
|
|
35
|
-
/** The task each tick runs; a rejection counts as a failure. */
|
|
36
|
-
onRun: () => Promise<void>
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Owns one bounded poll loop.
|
|
41
|
-
*
|
|
42
|
-
* Guarantees: at most one task runs at a time (a scheduled tick whose turn
|
|
43
|
-
* arrives while a run is in flight is dropped); consecutive failures double
|
|
44
|
-
* the delay up to maxBackoffMs and reset on the first success; the loop
|
|
45
|
-
* stops forever at deadlineMs and cancels its timer.
|
|
46
|
-
*/
|
|
47
|
-
export class PollGuard {
|
|
48
|
-
private readonly options: Required<PollGuardOptions>
|
|
49
|
-
private handle: unknown
|
|
50
|
-
private running = false
|
|
51
|
-
private startedAt = 0
|
|
52
|
-
private stopped = false
|
|
53
|
-
private failures = 0
|
|
54
|
-
|
|
55
|
-
/** @param options - loop bounds; interval/deadline/backoff/onRun are required, the rest optional. */
|
|
56
|
-
constructor(options: PollGuardOptions) {
|
|
57
|
-
this.options = {
|
|
58
|
-
timers: DEFAULT_TIMERS,
|
|
59
|
-
onDeadline: () => {},
|
|
60
|
-
onSettled: () => {},
|
|
61
|
-
...options,
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Start the loop. Safe to call once; later calls are ignored. */
|
|
66
|
-
start(): void {
|
|
67
|
-
if (this.startedAt !== 0) return
|
|
68
|
-
this.startedAt = Date.now()
|
|
69
|
-
this.schedule(this.options.intervalMs)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Stop the loop permanently and drop any pending tick. */
|
|
73
|
-
stop(): void {
|
|
74
|
-
this.stopped = true
|
|
75
|
-
this.options.timers.clear(this.handle)
|
|
76
|
-
this.handle = undefined
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private schedule(delayMs: number): void {
|
|
80
|
-
if (this.stopped) return
|
|
81
|
-
this.handle = this.options.timers.set(() => { void this.tick() }, delayMs)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private delay(): number {
|
|
85
|
-
const backoff = this.options.intervalMs * 2 ** Math.min(this.failures, 8)
|
|
86
|
-
return Math.min(backoff, this.options.maxBackoffMs)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private async tick(): Promise<void> {
|
|
90
|
-
if (this.stopped) return
|
|
91
|
-
if (this.running) return // Anti-overlap: drop ticks that arrive mid-run.
|
|
92
|
-
if (Date.now() - this.startedAt >= this.options.deadlineMs) {
|
|
93
|
-
this.stopped = true
|
|
94
|
-
this.options.onDeadline()
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
this.running = true
|
|
98
|
-
try {
|
|
99
|
-
await this.options.onRun()
|
|
100
|
-
this.failures = 0
|
|
101
|
-
} catch {
|
|
102
|
-
this.failures += 1
|
|
103
|
-
} finally {
|
|
104
|
-
this.running = false
|
|
105
|
-
this.options.onSettled(this.failures)
|
|
106
|
-
this.schedule(this.delay())
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/poll-guard.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Poll-loop guard shared by the plugin family's Host halves (git-graph,
|
|
4
|
+
* filemgr): one bounded refresh loop that never overlaps itself,
|
|
5
|
+
* backs off on consecutive failures, and stops at a global deadline. Pure
|
|
6
|
+
* logic with injectable timers, so consumers test it without wall clocks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Timer injection seam; defaults to the global setTimeout/clearTimeout pair. */
|
|
10
|
+
export interface PollTimers {
|
|
11
|
+
/** Schedule fn(ms). */
|
|
12
|
+
set: (fn: () => void, ms: number) => unknown
|
|
13
|
+
/** Cancel a scheduled handle. */
|
|
14
|
+
clear: (handle: unknown) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_TIMERS: PollTimers = {
|
|
18
|
+
set: (fn, ms) => setTimeout(fn, ms),
|
|
19
|
+
clear: (handle) => { clearTimeout(handle as ReturnType<typeof setTimeout>) },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PollGuardOptions {
|
|
23
|
+
/** Base interval between runs. */
|
|
24
|
+
intervalMs: number
|
|
25
|
+
/** Runs stop entirely once this much wall time passed since start(). */
|
|
26
|
+
deadlineMs: number
|
|
27
|
+
/** Cap for the failure backoff; each consecutive failure doubles the delay up to this. */
|
|
28
|
+
maxBackoffMs: number
|
|
29
|
+
/** Optional timer seam for tests. */
|
|
30
|
+
timers?: PollTimers
|
|
31
|
+
/** Invoked once when the deadline fires; no run starts afterwards. */
|
|
32
|
+
onDeadline?: () => void
|
|
33
|
+
/** Invoked after a run settles with the consecutive-failure count. */
|
|
34
|
+
onSettled?: (consecutiveFailures: number) => void
|
|
35
|
+
/** The task each tick runs; a rejection counts as a failure. */
|
|
36
|
+
onRun: () => Promise<void>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Owns one bounded poll loop.
|
|
41
|
+
*
|
|
42
|
+
* Guarantees: at most one task runs at a time (a scheduled tick whose turn
|
|
43
|
+
* arrives while a run is in flight is dropped); consecutive failures double
|
|
44
|
+
* the delay up to maxBackoffMs and reset on the first success; the loop
|
|
45
|
+
* stops forever at deadlineMs and cancels its timer.
|
|
46
|
+
*/
|
|
47
|
+
export class PollGuard {
|
|
48
|
+
private readonly options: Required<PollGuardOptions>
|
|
49
|
+
private handle: unknown
|
|
50
|
+
private running = false
|
|
51
|
+
private startedAt = 0
|
|
52
|
+
private stopped = false
|
|
53
|
+
private failures = 0
|
|
54
|
+
|
|
55
|
+
/** @param options - loop bounds; interval/deadline/backoff/onRun are required, the rest optional. */
|
|
56
|
+
constructor(options: PollGuardOptions) {
|
|
57
|
+
this.options = {
|
|
58
|
+
timers: DEFAULT_TIMERS,
|
|
59
|
+
onDeadline: () => {},
|
|
60
|
+
onSettled: () => {},
|
|
61
|
+
...options,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Start the loop. Safe to call once; later calls are ignored. */
|
|
66
|
+
start(): void {
|
|
67
|
+
if (this.startedAt !== 0) return
|
|
68
|
+
this.startedAt = Date.now()
|
|
69
|
+
this.schedule(this.options.intervalMs)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Stop the loop permanently and drop any pending tick. */
|
|
73
|
+
stop(): void {
|
|
74
|
+
this.stopped = true
|
|
75
|
+
this.options.timers.clear(this.handle)
|
|
76
|
+
this.handle = undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private schedule(delayMs: number): void {
|
|
80
|
+
if (this.stopped) return
|
|
81
|
+
this.handle = this.options.timers.set(() => { void this.tick() }, delayMs)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private delay(): number {
|
|
85
|
+
const backoff = this.options.intervalMs * 2 ** Math.min(this.failures, 8)
|
|
86
|
+
return Math.min(backoff, this.options.maxBackoffMs)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async tick(): Promise<void> {
|
|
90
|
+
if (this.stopped) return
|
|
91
|
+
if (this.running) return // Anti-overlap: drop ticks that arrive mid-run.
|
|
92
|
+
if (Date.now() - this.startedAt >= this.options.deadlineMs) {
|
|
93
|
+
this.stopped = true
|
|
94
|
+
this.options.onDeadline()
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
this.running = true
|
|
98
|
+
try {
|
|
99
|
+
await this.options.onRun()
|
|
100
|
+
this.failures = 0
|
|
101
|
+
} catch {
|
|
102
|
+
this.failures += 1
|
|
103
|
+
} finally {
|
|
104
|
+
this.running = false
|
|
105
|
+
this.options.onSettled(this.failures)
|
|
106
|
+
this.schedule(this.delay())
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/host/routes.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
16
|
-
import { createReadStream } from 'node:fs'
|
|
16
|
+
import { createReadStream, watch as watchDir } from 'node:fs'
|
|
17
17
|
import { dirname } from 'node:path'
|
|
18
18
|
import { pipeline } from 'node:stream/promises'
|
|
19
19
|
import { readFile, stat } from 'node:fs/promises'
|
|
@@ -94,6 +94,14 @@ interface Subscriber {
|
|
|
94
94
|
const GIT_POLL_MS = 30_000
|
|
95
95
|
/** SSE keep-alive comment interval (proxies drop idle connections). */
|
|
96
96
|
const HEARTBEAT_MS = 15_000
|
|
97
|
+
/**
|
|
98
|
+
* Debounce window for coalescing fs.watch events into one `{kind:'fs'}`
|
|
99
|
+
* push. LLM/ad-hoc file writes (e.g. an agent creating work products with a
|
|
100
|
+
* subprocess) often land several events in quick succession; a single trailing
|
|
101
|
+
* push per burst keeps the client's refresh amortized (client also coalesces
|
|
102
|
+
* with its own FS_COALESCE_MS).
|
|
103
|
+
*/
|
|
104
|
+
const FS_WATCH_DEBOUNCE_MS = 200
|
|
97
105
|
|
|
98
106
|
/**
|
|
99
107
|
* Parse a `Range: bytes=start-end` header against the file size. RFC 7233
|
|
@@ -303,6 +311,64 @@ export function registerPanelRoutes(ctx: Context, fs: FsService, git: GitService
|
|
|
303
311
|
gitPoll = undefined
|
|
304
312
|
}
|
|
305
313
|
|
|
314
|
+
// ─── fs watch (bound to the SSE subscriber lifecycle) ────────────────────
|
|
315
|
+
// The former design deliberately used no recursive fs.watch for cost, so the
|
|
316
|
+
// explorer only refreshed on user actions. That misses out-of-band writes
|
|
317
|
+
// (an agent/LLM subprocess creating a work product directly on disk), which
|
|
318
|
+
// is exactly the case the user wants to surface immediately. We restore a
|
|
319
|
+
// bounded watch: one fs.watch per connected SSE stream (panel visible), a
|
|
320
|
+
// trailing debounce that coalesces bursts into a single `{kind:'fs'}` push,
|
|
321
|
+
// and a graceful fallback when recursive watch is unavailable or errors.
|
|
322
|
+
let fsWatcher: import('node:fs').FSWatcher | undefined
|
|
323
|
+
let fsWatchTimer: ReturnType<typeof setTimeout> | undefined
|
|
324
|
+
|
|
325
|
+
const pushFsChange = (): void => {
|
|
326
|
+
if (fsWatchTimer !== undefined) return
|
|
327
|
+
fsWatchTimer = setTimeout(() => {
|
|
328
|
+
fsWatchTimer = undefined
|
|
329
|
+
for (const subscriber of subscribers) push(subscriber, { kind: 'fs' })
|
|
330
|
+
}, FS_WATCH_DEBOUNCE_MS)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Start watching one canonical root; recursive watch falls back to non-recursive when unsupported. */
|
|
334
|
+
const startFsWatch = (): void => {
|
|
335
|
+
if (fsWatcher !== undefined) return
|
|
336
|
+
let watcher: import('node:fs').FSWatcher | undefined
|
|
337
|
+
try {
|
|
338
|
+
watcher = watchDir(subscribersRoot(), { recursive: true })
|
|
339
|
+
} catch (error) {
|
|
340
|
+
ctx.logger.warn(`dsh-filemgr: recursive fs.watch unavailable (${String(error)}), falling back to shallow watch`)
|
|
341
|
+
try {
|
|
342
|
+
watcher = watchDir(subscribersRoot())
|
|
343
|
+
} catch (fallbackError) {
|
|
344
|
+
ctx.logger.warn(`dsh-filemgr: fs.watch failed, live directory changes will not auto-refresh (${String(fallbackError)})`)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
fsWatcher = watcher
|
|
349
|
+
// Ignore our own .git churn and debounce everything; the client re-lists
|
|
350
|
+
// expanded dirs only, so extra events cost a relist at most.
|
|
351
|
+
const onEvent = (): void => pushFsChange()
|
|
352
|
+
watcher.on('change', (_eventType, _filename) => onEvent())
|
|
353
|
+
watcher.on('error', (error) => {
|
|
354
|
+
ctx.logger.warn(`dsh-filemgr: fs.watch error (${String(error)}), live refresh degraded`)
|
|
355
|
+
})
|
|
356
|
+
ctx.logger.debug(`dsh-filemgr: watching ${subscribersRoot()} for live directory changes`)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const stopFsWatch = (): void => {
|
|
360
|
+
if (fsWatchTimer !== undefined) {
|
|
361
|
+
clearTimeout(fsWatchTimer)
|
|
362
|
+
fsWatchTimer = undefined
|
|
363
|
+
}
|
|
364
|
+
if (fsWatcher === undefined) return
|
|
365
|
+
fsWatcher.close()
|
|
366
|
+
fsWatcher = undefined
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** The canonical root of the single connected subscriber; '' when none. */
|
|
370
|
+
const subscribersRoot = (): string => (subscribers.size > 0 ? [...subscribers][0].root : '')
|
|
371
|
+
|
|
306
372
|
/**
|
|
307
373
|
* GET /filemgr/raw: stream one workspace file (markdown image srcs,
|
|
308
374
|
* pdf preview). Gated like every other operation; FsService.readRaw only
|
|
@@ -666,6 +732,9 @@ export function registerPanelRoutes(ctx: Context, fs: FsService, git: GitService
|
|
|
666
732
|
// from the probe's broadcast above.
|
|
667
733
|
if (gitUnavailable) push(subscriber, { kind: 'gitUnavailable' })
|
|
668
734
|
startGitPoll()
|
|
735
|
+
// Watch the workspace root while any SSE stream is open (panel visible);
|
|
736
|
+
// stops when the last subscriber disconnects (see below).
|
|
737
|
+
startFsWatch()
|
|
669
738
|
if (heartbeatTimer === undefined) {
|
|
670
739
|
heartbeatTimer = setInterval(() => {
|
|
671
740
|
for (const current of subscribers) current.res.write(': ping\n\n')
|
|
@@ -675,6 +744,7 @@ export function registerPanelRoutes(ctx: Context, fs: FsService, git: GitService
|
|
|
675
744
|
subscribers.delete(subscriber)
|
|
676
745
|
if (subscribers.size === 0) {
|
|
677
746
|
stopGitPoll()
|
|
747
|
+
stopFsWatch()
|
|
678
748
|
if (heartbeatTimer !== undefined) clearInterval(heartbeatTimer)
|
|
679
749
|
heartbeatTimer = undefined
|
|
680
750
|
}
|
|
@@ -688,6 +758,7 @@ export function registerPanelRoutes(ctx: Context, fs: FsService, git: GitService
|
|
|
688
758
|
return () => {
|
|
689
759
|
for (const dispose of disposers) dispose()
|
|
690
760
|
stopGitPoll()
|
|
761
|
+
stopFsWatch()
|
|
691
762
|
if (heartbeatTimer !== undefined) clearInterval(heartbeatTimer)
|
|
692
763
|
for (const subscriber of subscribers) subscriber.res.end()
|
|
693
764
|
subscribers.clear()
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { FsService } from './host/fs-service.ts'
|
|
|
25
25
|
import { GitService, subprocessRunner } from './host/git-service.ts'
|
|
26
26
|
import { createWorkspaceGate } from './host/gate.ts'
|
|
27
27
|
import { registerPanelRoutes } from './host/routes.ts'
|
|
28
|
+
import { registerOfficePreviewRoute } from './host/office-preview.ts'
|
|
28
29
|
import { mountOnce } from './mount-once.ts'
|
|
29
30
|
|
|
30
31
|
/** Required services: the route registry, the managed subprocess seam, the workspace registry, and the prompt band. */
|
|
@@ -69,7 +70,8 @@ function applyImpl(ctx: Context, config: FileManagerPanelSettings = {}): void {
|
|
|
69
70
|
if (disposePanel === undefined && enabled) {
|
|
70
71
|
disposePanel = ctx.effect(() => {
|
|
71
72
|
const disposeRoutes = registerPanelRoutes(ctx, fs, git)
|
|
72
|
-
|
|
73
|
+
const disposeOffice = registerOfficePreviewRoute(ctx)
|
|
74
|
+
return () => { disposeRoutes(); disposeOffice() }
|
|
73
75
|
}, 'dsh-filemgr: /filemgr routes')
|
|
74
76
|
disposePrompt = ctx.effect(() => ctx.systemPrompt.section({
|
|
75
77
|
name: 'plugin:filemgr',
|
package/src/mount-once.ts
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
// Generated by scripts/sync-shared.mjs from shared/host/mount-once.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
-
/**
|
|
3
|
-
* Host single-instance guard shared by the plugin family. The family bundle
|
|
4
|
-
* (dsh-web-ui-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
5
|
-
* the loader accepts a standalone install of the same package side by side;
|
|
6
|
-
* without this guard the second instance would still re-register the same
|
|
7
|
-
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
8
|
-
* and fail the boot. mountOnce makes the second host apply a no-op for the
|
|
9
|
-
* lifetime of the first instance (the browser half is already deduped by
|
|
10
|
-
* package name in the client module host).
|
|
11
|
-
*
|
|
12
|
-
* The registry rides a global symbol so two module instances of the same
|
|
13
|
-
* package (npm copy vs repository link) still share one verdict. cordis
|
|
14
|
-
* `ctx.effect` runs its callback immediately and treats the callback's
|
|
15
|
-
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const MOUNTED = Symbol.for('dsh-web-ui.mounted-plugins')
|
|
19
|
-
|
|
20
|
-
interface MountRegistry {
|
|
21
|
-
[MOUNTED]?: Set<string>
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function mountedSet(): Set<string> {
|
|
25
|
-
const registry = globalThis as MountRegistry
|
|
26
|
-
return (registry[MOUNTED] ??= new Set())
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Wrap a cordis plugin apply so the package runs at most once per process.
|
|
31
|
-
* The first mount registers normally and unmarks when its fiber disposes;
|
|
32
|
-
* any later mount of the same package name is a no-op.
|
|
33
|
-
* @param packageName - npm package identity shared by every install source.
|
|
34
|
-
* @param fn - the original plugin apply.
|
|
35
|
-
* @returns an apply of the same shape.
|
|
36
|
-
*/
|
|
37
|
-
export function mountOnce<T extends (...args: any[]) => unknown>(packageName: string, fn: T): T {
|
|
38
|
-
return ((...args: unknown[]) => {
|
|
39
|
-
const mounted = mountedSet()
|
|
40
|
-
if (mounted.has(packageName)) return
|
|
41
|
-
mounted.add(packageName)
|
|
42
|
-
const ctx = args[0] as { effect?: (effect: () => unknown) => unknown } | undefined
|
|
43
|
-
ctx?.effect?.(() => () => {
|
|
44
|
-
mounted.delete(packageName)
|
|
45
|
-
})
|
|
46
|
-
return fn(...args)
|
|
47
|
-
}) as T
|
|
48
|
-
}
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/mount-once.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Host single-instance guard shared by the plugin family. The family bundle
|
|
4
|
+
* (dsh-web-ui-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
5
|
+
* the loader accepts a standalone install of the same package side by side;
|
|
6
|
+
* without this guard the second instance would still re-register the same
|
|
7
|
+
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
8
|
+
* and fail the boot. mountOnce makes the second host apply a no-op for the
|
|
9
|
+
* lifetime of the first instance (the browser half is already deduped by
|
|
10
|
+
* package name in the client module host).
|
|
11
|
+
*
|
|
12
|
+
* The registry rides a global symbol so two module instances of the same
|
|
13
|
+
* package (npm copy vs repository link) still share one verdict. cordis
|
|
14
|
+
* `ctx.effect` runs its callback immediately and treats the callback's
|
|
15
|
+
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const MOUNTED = Symbol.for('dsh-web-ui.mounted-plugins')
|
|
19
|
+
|
|
20
|
+
interface MountRegistry {
|
|
21
|
+
[MOUNTED]?: Set<string>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function mountedSet(): Set<string> {
|
|
25
|
+
const registry = globalThis as MountRegistry
|
|
26
|
+
return (registry[MOUNTED] ??= new Set())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Wrap a cordis plugin apply so the package runs at most once per process.
|
|
31
|
+
* The first mount registers normally and unmarks when its fiber disposes;
|
|
32
|
+
* any later mount of the same package name is a no-op.
|
|
33
|
+
* @param packageName - npm package identity shared by every install source.
|
|
34
|
+
* @param fn - the original plugin apply.
|
|
35
|
+
* @returns an apply of the same shape.
|
|
36
|
+
*/
|
|
37
|
+
export function mountOnce<T extends (...args: any[]) => unknown>(packageName: string, fn: T): T {
|
|
38
|
+
return ((...args: unknown[]) => {
|
|
39
|
+
const mounted = mountedSet()
|
|
40
|
+
if (mounted.has(packageName)) return
|
|
41
|
+
mounted.add(packageName)
|
|
42
|
+
const ctx = args[0] as { effect?: (effect: () => unknown) => unknown } | undefined
|
|
43
|
+
ctx?.effect?.(() => () => {
|
|
44
|
+
mounted.delete(packageName)
|
|
45
|
+
})
|
|
46
|
+
return fn(...args)
|
|
47
|
+
}) as T
|
|
48
|
+
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Attached file reference pills, rendered in the `conversation.input.dock`
|
|
3
|
-
* band directly above the composer card.
|
|
4
|
-
*
|
|
5
|
-
* Design: the composer draft is the single source of truth. The pill row is
|
|
6
|
-
* a pure visual scan of `@<path>:<line>` tokens in the live draft — no
|
|
7
|
-
* occurrence table, no `slash/input-insert-reference` bail (that path
|
|
8
|
-
* requires a registered codec and fails with
|
|
9
|
-
* `no serializer for reference source "file"`), so sending is always a
|
|
10
|
-
* plain-text sink. Because the row derives from the draft, every mutation
|
|
11
|
-
* (typing, backspace, send, session switch) updates the pills for free.
|
|
12
|
-
*
|
|
13
|
-
* Each pill shows the basename (directory suffix only when the basename is
|
|
14
|
-
* ambiguous), the line range, and a dismiss × that removes the token from
|
|
15
|
-
* the draft. The full relative path rides the title tooltip.
|
|
16
|
-
* @module dsh-filemgr/client/chat/AttachedFilesDock
|
|
17
|
-
*/
|
|
18
|
-
import type { JSX } from 'react';
|
|
19
|
-
import type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client';
|
|
20
|
-
/** Minimal structural view of the conversation input service (see reference.ts). */
|
|
21
|
-
interface DraftInput {
|
|
22
|
-
state: {
|
|
23
|
-
getSnapshot(): {
|
|
24
|
-
draft: string;
|
|
25
|
-
draftRev: number;
|
|
26
|
-
};
|
|
27
|
-
subscribe(listener: () => void): () => void;
|
|
28
|
-
};
|
|
29
|
-
setDraft(draft: string): void;
|
|
30
|
-
}
|
|
31
|
-
interface ConversationService {
|
|
32
|
-
input: {
|
|
33
|
-
for(actx: ClientContext): DraftInput;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* The dock entry: subscribes to the session's draft and renders one pill per
|
|
38
|
-
* `@path:line` token. Renders nothing while the draft carries no references.
|
|
39
|
-
* @param sessionId - the addressed session (the dock is session-scoped).
|
|
40
|
-
* @param sessions - session scope resolver (actx for the input shell).
|
|
41
|
-
* @param conversation - conversation service face.
|
|
42
|
-
*/
|
|
43
|
-
export declare function AttachedFilesDock({ sessionId, sessions, conversation, }: {
|
|
44
|
-
sessionId: SessionId;
|
|
45
|
-
sessions: {
|
|
46
|
-
scope(id: SessionId): ClientContext | undefined;
|
|
47
|
-
};
|
|
48
|
-
conversation: ConversationService;
|
|
49
|
-
}): JSX.Element | null;
|
|
50
|
-
export {};
|
|
51
|
-
//# sourceMappingURL=AttachedFilesDock.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"AttachedFilesDock.d.ts","sourceRoot":"","sources":["../../../../src/client/chat/AttachedFilesDock.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,wCAAwC,CAAA;AAMtF,oFAAoF;AACpF,UAAU,UAAU;IAClB,KAAK,EAAE;QACL,WAAW,IAAI;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAA;QAClD,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;KAC5C,CAAA;IACD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AACD,UAAU,mBAAmB;IAC3B,KAAK,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,aAAa,GAAG,UAAU,CAAA;KAAE,CAAA;CAChD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,QAAQ,EACR,YAAY,GACb,EAAE;IACD,SAAS,EAAE,SAAS,CAAA;IACpB,QAAQ,EAAE;QAAE,KAAK,CAAC,EAAE,EAAE,SAAS,GAAG,aAAa,GAAG,SAAS,CAAA;KAAE,CAAA;IAC7D,YAAY,EAAE,mBAAmB,CAAA;CAClC,GAAG,GAAG,CAAC,OAAO,GAAG,IAAI,CAoDrB"}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Floating expand button geometry (issues #374 / #292): the button is a
|
|
3
|
-
* fixed chrome element docked at the viewport's top-right corner, just
|
|
4
|
-
* below the shell conversation header — the horizontal divider under the
|
|
5
|
-
* "Session log" row — so the re-expand control sits below the header
|
|
6
|
-
* chrome instead of overlapping it. Its top stays below the Window
|
|
7
|
-
* Controls Overlay titlebar strip when dsh-desktop reports one (issue
|
|
8
|
-
* #292). Every computed position is clamped into the usable range.
|
|
9
|
-
* @module dsh-filemgr/client/floating
|
|
10
|
-
*/
|
|
11
|
-
/** Breathing room above/below the button (px). */
|
|
12
|
-
export declare const FLOATING_MARGIN_PX = 6;
|
|
13
|
-
/** Gap between the shell header's bottom divider and the button top (px). */
|
|
14
|
-
export declare const FLOATING_HEADER_GAP_PX = 8;
|
|
15
|
-
/** The button's rendered size (kept in sync with tokens.module.css). */
|
|
16
|
-
export declare const FLOATING_BUTTON_HEIGHT_PX = 24;
|
|
17
|
-
/** Top offset of the explorer's collapse chevron inside its tab bar (px).
|
|
18
|
-
* Used only as a fallback when the shell conversation header is not found
|
|
19
|
-
* (kept in sync with the chevron rule in tokens.module.css). */
|
|
20
|
-
export declare const COLLAPSE_CHEVRON_TOP_PX = 6;
|
|
21
|
-
/** The WCO titlebar height when visible; 0 in a plain browser tab. */
|
|
22
|
-
export declare function titlebarAreaHeight(): number;
|
|
23
|
-
/** Clamp a requested top px into the usable vertical range. */
|
|
24
|
-
export declare function clampFloatingTop(top: number, viewportHeight: number, buttonHeight: number, titlebar: number): number;
|
|
25
|
-
/** The default top: aligned with the collapse chevron at the top-right. */
|
|
26
|
-
export declare function topAlignedFloatingTop(viewportHeight: number, buttonHeight: number, titlebar: number): number;
|
|
27
|
-
//# sourceMappingURL=floating.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"floating.d.ts","sourceRoot":"","sources":["../../../src/client/floating.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,kDAAkD;AAClD,eAAO,MAAM,kBAAkB,IAAI,CAAA;AACnC,6EAA6E;AAC7E,eAAO,MAAM,sBAAsB,IAAI,CAAA;AACvC,wEAAwE;AACxE,eAAO,MAAM,yBAAyB,KAAK,CAAA;AAC3C;;gEAEgE;AAChE,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAQxC,sEAAsE;AACtE,wBAAgB,kBAAkB,IAAI,MAAM,CAU3C;AAED,+DAA+D;AAC/D,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GACf,MAAM,CAKR;AAED,2EAA2E;AAC3E,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO5G"}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The '@' input-trigger source: a lazy directory-tree browser rooted at the
|
|
3
|
-
* active session's cwd, mirroring the Explorer panel's tree. Each keystroke
|
|
4
|
-
* lists the addressed directory on demand (no precomputed index); picking a
|
|
5
|
-
* file lands a plain-text `@<rel-path>` token in the draft — the same readable
|
|
6
|
-
* reference the Explorer's @-reference button inserts, and the Host validates
|
|
7
|
-
* it as an existence-only workspace reference.
|
|
8
|
-
*
|
|
9
|
-
* Directory navigation: the inline trigger menu cannot expand a directory
|
|
10
|
-
* (it closes on pick, and the shell only re-runs trigger detection on
|
|
11
|
-
* keyboard input), so picking a directory opens the tree-browser modal
|
|
12
|
-
* (FilePickerModal) anchored at that directory — a real recursive tree with
|
|
13
|
-
* lazy expansion, mirroring the pi-desktop picker.
|
|
14
|
-
*
|
|
15
|
-
* Type-only imports from @deepseek-ai/dsh-client-ui-input-trigger/client keep
|
|
16
|
-
* the client bundle purity gate satisfied (the service itself is injected by
|
|
17
|
-
* the shell, never bundled here).
|
|
18
|
-
* @module dsh-filemgr/client/mention
|
|
19
|
-
*/
|
|
20
|
-
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
21
|
-
import type { InputTriggerServiceContract, InputTriggerSource } from '@deepseek-ai/dsh-client-ui-input-trigger/client';
|
|
22
|
-
import type { PanelApi } from './api.ts';
|
|
23
|
-
declare module '@deepseek-ai/dsh-client-ui-input-trigger/client' {
|
|
24
|
-
interface InputTriggerCandidate {
|
|
25
|
-
/** Workspace-relative path (the token we insert). */
|
|
26
|
-
readonly value?: string;
|
|
27
|
-
/** Whether the entry is a directory (drives the trailing-slash suffix). */
|
|
28
|
-
readonly fmIsDir?: boolean;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
/** Owner source name (lexicon + decoration routing key). Unique per trigger. */
|
|
32
|
-
export declare const SOURCE_NAME = "file-manager";
|
|
33
|
-
/**
|
|
34
|
-
* Build the '@' trigger source over the injected root context + the PanelApi.
|
|
35
|
-
* One source per plugin fiber; it reads the live session root on every key so
|
|
36
|
-
* switching sessions re-roots the browser automatically.
|
|
37
|
-
* @param ctx - root client context (captured for session projection).
|
|
38
|
-
* @param api - the panel fs client (lists directories on demand).
|
|
39
|
-
* @returns the source to register with `inputTriggers.registerSource`.
|
|
40
|
-
*/
|
|
41
|
-
export declare function createFileMentionSource(ctx: ClientContext, api: PanelApi): InputTriggerSource;
|
|
42
|
-
/** The typed service contract the shell injects as `inputTriggers`. */
|
|
43
|
-
export type { InputTriggerServiceContract };
|
|
44
|
-
//# sourceMappingURL=mention.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mention.d.ts","sourceRoot":"","sources":["../../../src/client/mention.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,wCAAwC,CAAA;AACtF,OAAO,KAAK,EAEV,2BAA2B,EAC3B,kBAAkB,EACnB,MAAM,iDAAiD,CAAA;AAExD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAKxC,OAAO,QAAQ,iDAAiD,CAAC;IAC/D,UAAU,qBAAqB;QAC7B,qDAAqD;QACrD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;QACvB,2EAA2E;QAC3E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAC3B;CACF;AAED,gFAAgF;AAChF,eAAO,MAAM,WAAW,iBAAiB,CAAA;AAyEzC;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,QAAQ,GAAG,kBAAkB,CAmD7F;AAED,uEAAuE;AACvE,YAAY,EAAE,2BAA2B,EAAE,CAAA"}
|