@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.
Files changed (96) hide show
  1. package/LICENSE +38 -38
  2. package/README.i18n.yaml +6 -6
  3. package/README.md +66 -66
  4. package/README.zh.md +66 -66
  5. package/cordis.patch.yml +13 -13
  6. package/lib/client.js +27305 -270
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +474 -42
  9. package/lib/tsconfig.client.tsbuildinfo +1 -1
  10. package/lib/tsconfig.host.tsbuildinfo +1 -1
  11. package/lib/types/client/file-source.d.ts +10 -0
  12. package/lib/types/client/file-source.d.ts.map +1 -1
  13. package/lib/types/client/fileType.d.ts +19 -4
  14. package/lib/types/client/fileType.d.ts.map +1 -1
  15. package/lib/types/client/index.d.ts.map +1 -1
  16. package/lib/types/client/locales.d.ts +3 -0
  17. package/lib/types/client/locales.d.ts.map +1 -1
  18. package/lib/types/client/mount.d.ts +1 -1
  19. package/lib/types/client/mount.d.ts.map +1 -1
  20. package/lib/types/client/preview/PreviewPanel.d.ts +2 -1
  21. package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -1
  22. package/lib/types/client/preview/PreviewToolbar.d.ts +3 -1
  23. package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -1
  24. package/lib/types/client/preview/content.d.ts +3 -1
  25. package/lib/types/client/preview/content.d.ts.map +1 -1
  26. package/lib/types/client/preview/office.d.ts +23 -0
  27. package/lib/types/client/preview/office.d.ts.map +1 -0
  28. package/lib/types/client/reference.d.ts +17 -0
  29. package/lib/types/client/reference.d.ts.map +1 -1
  30. package/lib/types/client/store.d.ts.map +1 -1
  31. package/lib/types/host/fs-service.d.ts.map +1 -1
  32. package/lib/types/host/office-preview.d.ts +13 -0
  33. package/lib/types/host/office-preview.d.ts.map +1 -0
  34. package/lib/types/host/routes.d.ts.map +1 -1
  35. package/lib/types/index.d.ts.map +1 -1
  36. package/package.json +19 -4
  37. package/src/client/FileManagerSettingsCard.tsx +117 -117
  38. package/src/client/PluginSettingsCard.tsx +337 -337
  39. package/src/client/chat/file-ref.ts +144 -144
  40. package/src/client/chat/mermaid-chat.tsx +102 -102
  41. package/src/client/chat/placeholder-hint.tsx +46 -46
  42. package/src/client/components/ExplorerPanel.tsx +534 -534
  43. package/src/client/components/FileIcon.tsx +46 -46
  44. package/src/client/components/ScmPanel.tsx +480 -480
  45. package/src/client/components/a11y.ts +20 -20
  46. package/src/client/components/icons.tsx +274 -274
  47. package/src/client/components/overlay.tsx +225 -225
  48. package/src/client/drag/DragFileInlay.tsx +89 -89
  49. package/src/client/drag/file-drag.ts +67 -67
  50. package/src/client/drag.ts +154 -154
  51. package/src/client/file-source.ts +34 -0
  52. package/src/client/fileType.ts +32 -5
  53. package/src/client/hooks/useResizableSplit.ts +91 -91
  54. package/src/client/hooks/useStore.ts +15 -15
  55. package/src/client/index.ts +42 -3
  56. package/src/client/layout.ts +523 -523
  57. package/src/client/locales.ts +6 -0
  58. package/src/client/maximize.ts +34 -34
  59. package/src/client/mount.tsx +2 -1
  60. package/src/client/persist.ts +194 -194
  61. package/src/client/picker/FilePickerModal.tsx +526 -526
  62. package/src/client/picker/file-picker.ts +86 -86
  63. package/src/client/preview/PreviewPanel.tsx +7 -1
  64. package/src/client/preview/PreviewTabs.tsx +159 -159
  65. package/src/client/preview/PreviewToolbar.tsx +3 -2
  66. package/src/client/preview/content.tsx +7 -2
  67. package/src/client/preview/markdown.ts +351 -351
  68. package/src/client/preview/mermaid.ts +274 -274
  69. package/src/client/preview/office.tsx +321 -0
  70. package/src/client/reference.ts +49 -0
  71. package/src/client/settings-card.module.css +316 -316
  72. package/src/client/settings-form.ts +451 -451
  73. package/src/client/store.ts +17 -12
  74. package/src/client/styles/drag.module.css +30 -30
  75. package/src/client/styles/explorer.module.css +371 -371
  76. package/src/client/styles/picker.module.css +344 -344
  77. package/src/client/styles/preview.module.css +3 -1
  78. package/src/client/styles/scm.module.css +380 -380
  79. package/src/client/styles/tokens.module.css +376 -371
  80. package/src/core/types.ts +162 -162
  81. package/src/host/fs-service.ts +7 -1
  82. package/src/host/gate.ts +73 -73
  83. package/src/host/git-runner.ts +116 -116
  84. package/src/host/git-service.ts +395 -395
  85. package/src/host/loopback.ts +63 -63
  86. package/src/host/office-preview.ts +399 -0
  87. package/src/host/poll-guard.ts +109 -109
  88. package/src/host/routes.ts +72 -1
  89. package/src/index.ts +3 -1
  90. package/src/mount-once.ts +48 -48
  91. package/lib/types/client/chat/AttachedFilesDock.d.ts +0 -51
  92. package/lib/types/client/chat/AttachedFilesDock.d.ts.map +0 -1
  93. package/lib/types/client/floating.d.ts +0 -27
  94. package/lib/types/client/floating.d.ts.map +0 -1
  95. package/lib/types/client/mention.d.ts +0 -44
  96. package/lib/types/client/mention.d.ts.map +0 -1
@@ -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
+ }
@@ -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
- return () => disposeRoutes()
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"}