dsh-file-activity 0.5.7 → 0.5.9

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.
@@ -1,3 +1,4 @@
1
+ 'use strict'
1
2
  // ── data access (host routes) ─────────────────────────────────────────
2
3
  async function fetchStats(sessionId) {
3
4
  const response = await fetch(`/file-activity/api/stats?sessionId=${encodeURIComponent(sessionId)}`)
@@ -5,7 +6,6 @@ async function fetchStats(sessionId) {
5
6
  if (json === null || typeof json !== 'object' || json.ok !== true) return null
6
7
  return json.value
7
8
  }
8
-
9
9
  /** Resolve the session working directory through the sidebar's native API. */
10
10
  async function fetchSessionCwd(sessionId) {
11
11
  try {
@@ -21,7 +21,6 @@ async function fetchSessionCwd(sessionId) {
21
21
  return ''
22
22
  }
23
23
  }
24
-
25
24
  function postRecord(sessionId, path, op) {
26
25
  if (typeof sessionId !== 'string' || sessionId === '' || typeof path !== 'string' || path === '') return
27
26
  void fetch('/file-activity/api/record', {
@@ -30,7 +29,6 @@ function postRecord(sessionId, path, op) {
30
29
  body: JSON.stringify({ sessionId, path, op }),
31
30
  }).catch(() => {})
32
31
  }
33
-
34
32
  function postClear(sessionId) {
35
33
  void fetch('/file-activity/api/clear', {
36
34
  method: 'POST',
@@ -38,12 +36,10 @@ function postClear(sessionId) {
38
36
  body: JSON.stringify({ sessionId }),
39
37
  }).catch(() => {})
40
38
  }
41
-
42
39
  /** Plugin media route URL for a recorded path (authorized per session). */
43
40
  function mediaUrlOf(sessionId, path) {
44
41
  return `/file-activity/file?${new URLSearchParams({ sessionId, path })}`
45
42
  }
46
-
47
43
  /** Plugin text route URL (`as=text`): fs.read-shaped JSON for recorded text. */
48
44
  function textUrlOf(sessionId, path) {
49
45
  return `/file-activity/file?${new URLSearchParams({ sessionId, path, as: 'text' })}`
@@ -1,99 +1,105 @@
1
- // ── plugin body ───────────────────────────────────────────────────────
2
- /**
3
- * The stylesheet is pure static CSS and must NOT depend on the
4
- * betterSidebar service: inject it first, unconditionally. If it lived
5
- * behind the `service === undefined` early return, an HMR rebuild or
6
- * service reload could leave the already-rendered tab WITHOUT its
7
- * stylesheet — the raw white-text list you see when the CSS is gone.
8
- * Each fiber owns its own <style> element and the disposer removes
9
- * only that element, so a rebuild always keeps at least one copy.
10
- */
11
- function injectStyles(ctx) {
12
- ctx.effect(() => {
13
- if (typeof document === 'undefined' || document === null || typeof document.head === 'undefined') return () => {}
14
- const style = document.createElement('style')
15
- style.setAttribute('data-dsh-file-activity', 'styles')
16
- style.textContent = STYLES
17
- document.head.appendChild(style)
18
- return () => {
19
- if (style.parentNode) style.parentNode.removeChild(style)
20
- }
21
- }, 'dsh-file-activity: styles')
22
- }
23
-
24
- /** Mount probe: report client activation to the host state (synthetic
25
- * session id, invisible in the UI — confirms the client half actually
26
- * loaded after a page refresh). */
27
- function mountProbe() {
28
- void fetch('/file-activity/api/record', {
29
- method: 'POST',
30
- headers: { 'content-type': 'application/json' },
31
- body: JSON.stringify({ sessionId: '__probe__', path: 'mounted', op: 'read' }),
32
- }).catch(() => {})
33
- }
34
-
35
- /** Register the tab (enabled by default in the Side card settings). */
36
- function registerTab(ctx, dataStore) {
37
- const service = ctx.betterSidebar
1
+ 'use strict'
2
+ // ── plugin body (native sidebar extension points) ─────────────────────
3
+ //
4
+ // Everything this half needs comes from Cordis services the HOST provides
5
+ // (issue #187 batch 2): the tab registry, the slot registry, the document
6
+ // preview registry and the right-Sidebar navigation controller. There is no
7
+ // third-party sidebar package in the picture anymore, so the page keeps
8
+ // working on any host that ships those four services.
9
+ //
10
+ // The stylesheet is pure static CSS and must NOT depend on any of them:
11
+ // inject it first, unconditionally. If it lived behind an early return, an HMR
12
+ // rebuild or service reload could leave the already-rendered tab WITHOUT its
13
+ // stylesheet — the raw white-text list you see when the CSS is gone. Each
14
+ // fiber owns its own <style> element and the disposer removes only that
15
+ // element, so a rebuild always keeps at least one copy.
16
+ /** registerTab step one: the tab TYPE (what a file-activity page is). */
17
+ function registerTabType(ctx) {
38
18
  ctx.effect(
39
19
  () =>
40
- service.registerTab({
20
+ ctx.sidebarRightTabs.register({
41
21
  id: TAB_ID,
22
+ kind: TAB_KIND,
23
+ // No patterns: this is a page type, opened by kind (ctx.sidebarRight.openTab).
42
24
  title: () => strings.title(),
43
- icon: (size) => icon.clock(size),
44
- order: 15,
45
- single: true,
46
- settings: {
47
- pluginToggles: [
48
- {
49
- key: 'autoOpen',
50
- title: () => (isZh() ? '会话开始时自动打开' : 'Auto-open on session start'),
51
- desc: () =>
52
- isZh()
53
- ? '每个会话首次打开时自动显示本页(可在侧边栏设置中关闭)'
54
- : 'Opens this tab once per session by default (turn off here)',
55
- type: 'switch',
56
- },
57
- ],
58
- },
59
- component: (props) => createElement(FileActivityView, { ...props, dataStore }),
25
+ // Guide entry (issue #266 B): the sidebar's "new tab" page is the guide
26
+ // tab, and its doors come from `sidebarRightTabs.guide()` — the entries
27
+ // every registered type contributes. Without one the page was reachable
28
+ // ONLY through auto-open, so closing its chip made it impossible to
29
+ // reopen from the UI. Picking the capsule opens this kind in the guide's
30
+ // place (host GuideBody → actions.openTab(kind, { replaceTab: true })).
31
+ guide: [
32
+ {
33
+ order: 20,
34
+ title: () => strings.title(),
35
+ description: () => strings.guideDescription(),
36
+ },
37
+ ],
60
38
  }),
61
- 'dsh-file-activity: tab registration',
39
+ 'dsh-file-activity: tab type',
62
40
  )
63
41
  }
64
-
65
- exports.inject = ['betterSidebar']
66
-
42
+ /** registerTab step two: the body seat (keyed by the definition id). */
43
+ function registerTabBody(ctx, dataStore) {
44
+ ctx.effect(
45
+ () =>
46
+ ctx.slots.inject('sidebar.right.pane.tab', () =>
47
+ ctx.slots.register({ name: 'sidebar.right.pane.tab', key: TAB_ID }, function (props) {
48
+ const injected = Object.assign({}, props, { dataStore })
49
+ return createElement(FileActivityView, injected)
50
+ }),
51
+ ),
52
+ 'dsh-file-activity: tab body',
53
+ )
54
+ }
55
+ /** registerTab step three: the chip title seat (a live, localized label). */
56
+ function registerTabTitle(ctx) {
57
+ ctx.effect(
58
+ () =>
59
+ ctx.slots.inject('sidebar.right.pane.tab.title', () =>
60
+ ctx.slots.register({ name: 'sidebar.right.pane.tab.title', key: TAB_ID }, () =>
61
+ createElement('span', { className: 'dfa-chip' }, strings.title()),
62
+ ),
63
+ ),
64
+ 'dsh-file-activity: tab title',
65
+ )
66
+ }
67
+ /**
68
+ * Run one registration, converting a throw into an observable failure.
69
+ *
70
+ * Swallowing is deliberate: the host throws for a taken id or a taken kind, and
71
+ * letting it escape would fail the WHOLE activation (the stylesheet, the
72
+ * floating preview and the settings tab would go with it). What must never
73
+ * happen is a SILENT failure, so the cause is logged, marked on <html> and
74
+ * rendered as a notice inside the tab body.
75
+ */
76
+ function guarded(kind, register) {
77
+ try {
78
+ register()
79
+ } catch (error) {
80
+ markDegraded(kind, error)
81
+ }
82
+ }
83
+ exports.inject = ['slots', 'sidebarRightTabs', 'documentPreviews', 'sidebarRight']
67
84
  exports.apply = function apply(ctx) {
68
- // Stylesheet first, unconditionally (HMR pitfall — see injectStyles).
85
+ // Stylesheet first, unconditionally (HMR pitfall — see the header note).
69
86
  injectStyles(ctx)
70
- const service = ctx.betterSidebar
71
- if (service === undefined) {
72
- // 依赖缺失提示(issue #72 同类问题):dsh-file-activity 的 client 端
73
- // 依赖 dsh-better-sidebar 提供侧边栏扩展点,未安装时静默返回会让用户
74
- // 以为插件坏了——明确提示安装方式。
75
- if (typeof console !== 'undefined' && typeof console.warn === 'function') {
76
- console.warn(
77
- '[dsh-file-activity] dsh-better-sidebar 未安装:文件活动页签无法挂载。请安装宿主插件:dsh plugin --profile web add dsh-better-sidebar dsh-file-activity',
78
- )
79
- }
80
- return
81
- }
82
-
83
- // Per-session data store: { bySession: { [sessionId]: { recent, counts, loading } }, preview }
84
- // Each conversation reads/writes only its own bucket, so switching
85
- // sessions never leaks another session's file activity into the view.
87
+ // Failing loudly beats failing silently: a rejected registration (id taken,
88
+ // kind taken, seat missing) is recorded so the user and the e2e suite can
89
+ // see WHY the tab never appeared.
90
+ guarded('tab', () => registerTabType(ctx))
86
91
  const dataStore = createStore({ bySession: {}, preview: null })
87
- mountProbe()
88
-
92
+ registerSharedStore(dataStore)
93
+ registerTabBody(ctx, dataStore)
94
+ registerTabTitle(ctx)
95
+ registerPreviewOverlay(ctx, dataStore)
96
+ registerSettingsTab(ctx)
97
+ guarded('previews', () => registerDocumentPreviews(ctx))
89
98
  // sidebar operations → host record route
90
99
  ctx.effect(() => installFetchInterceptor(), 'dsh-file-activity: sidebar fetch observation')
91
- registerTab(ctx, dataStore)
92
-
93
100
  // auto-open once per session (default on)
94
- ctx.effect(() => installAutoOpen(ctx, TAB_ID), 'dsh-file-activity: auto-open')
101
+ ctx.effect(() => installAutoOpen(ctx), 'dsh-file-activity: auto-open')
95
102
  }
96
-
97
103
  // Internal functions exposed for the render-path test suite only; inert in
98
104
  // the browser bundle (plain properties on the exports object).
99
105
  exports.__test = {
@@ -102,10 +108,13 @@ exports.__test = {
102
108
  fetchTextContent,
103
109
  textUrlOf,
104
110
  strings,
111
+ viewerOf,
105
112
  renderPreviewBody,
106
113
  previewClickAction,
107
114
  isInsideFloating,
108
115
  closePreviewOnHidden,
116
+ autoOpenEnabled,
117
+ PREVIEW_EXTENSIONS,
109
118
  AUTO_CLOSE_MS,
110
119
  // Static stylesheet text, so the render-path suite can assert the floating
111
120
  // preview body keeps its flex-fill container (issue #111).
@@ -1,76 +1,99 @@
1
+ 'use strict'
1
2
  // ── auto-open (enabled by default) ────────────────────────────────────
2
- function findTabIn(state, tabId) {
3
- const leaves = (node) => (node.kind === 'leaf' ? [node] : (node.children ?? []).flatMap(leaves))
4
- for (const node of [state?.splits, state?.bottomSplits]) {
5
- if (node === undefined || node === null) continue
6
- for (const leaf of leaves(node)) {
7
- if ((leaf.tabs ?? []).some((tab) => tab.type === tabId)) return true
8
- }
9
- }
10
- return false
11
- }
12
-
13
- /** Current sidebar snapshot, or null when the service is not ready. */
14
- function sidebarSnapshot(service) {
3
+ //
4
+ // Native flow: the tab is a PAGE type, so opening it means
5
+ // `ctx.sidebarRight.openTab(kind)` — there is no layout snapshot to inspect
6
+ // and no session id to read from a third-party sidebar service anymore.
7
+ //
8
+ // Timing is the whole problem. The navigation controller throws
9
+ // "sidebarRight: no session surface is mounted" when the right column has not
10
+ // mounted its session surface yet — which is exactly the state during the
11
+ // first paint of a fresh page. The previous third-party sidebar used to answer with a snapshot
12
+ // and the old code simply gave up when it was missing, which is how the tab
13
+ // could silently never auto-open. Here the attempt is RETRIED with the timers
14
+ // until the surface is up, and a permanent failure is recorded through
15
+ // markDegraded() instead of disappearing.
16
+ /** Retry cadence while the right column has not mounted its session surface. */
17
+ const AUTO_OPEN_RETRY_MS = 1000
18
+ /** Give up after this many attempts (~30s) and degrade observably. */
19
+ const AUTO_OPEN_MAX_TRIES = 30
20
+ /**
21
+ * Pending retry timer id (0 = none).
22
+ *
23
+ * There is deliberately NO "already attempted" flag guarding this module: a
24
+ * rejected registration or an HMR rebuild re-runs apply(), and the second run
25
+ * must still be able to bring the tab back. `openTab` is idempotent — it
26
+ * focuses the existing page instead of duplicating it.
27
+ */
28
+ let autoOpenTimer = 0
29
+ /**
30
+ * Whether this BROWSER TAB already had its auto-open attempt.
31
+ *
32
+ * The native API exposes no "current session id" to a plugin (`openTab`
33
+ * targets the mounted session, and the layout store is the host's own), so a
34
+ * per-session marker cannot be written from here. The marker is therefore
35
+ * per-browser-tab: one attempt per page load, which is exactly the visible
36
+ * behaviour — the session's tab record itself is persisted host-side, so
37
+ * revisiting a session that already shows the tab merely focuses it.
38
+ */
39
+ function isAutoOpenAttempted() {
40
+ const key = AUTO_OPEN_KEY + 'loaded'
15
41
  try {
16
- return service.getSnapshot?.()
42
+ return window.sessionStorage.getItem(key) === '1'
17
43
  } catch {
18
- return null
19
- }
20
- }
21
-
22
- /** The user disabled auto-open for this tab in the sidebar settings. */
23
- function isAutoOpenDisabled(snapshot, tabId) {
24
- const settings = snapshot.prefs?.pluginSettings?.[tabId]
25
- return settings !== undefined && settings.autoOpen === false
26
- }
27
-
28
- /** Whether this session was already auto-opened (localStorage marker). */
29
- function isAutoOpenMarked(sessionId) {
30
- try {
31
- return Boolean(window.localStorage.getItem(AUTO_OPEN_KEY + sessionId))
32
- } catch {
33
- return true
44
+ return false
34
45
  }
35
46
  }
36
-
37
- /** Persist the auto-opened marker for this session. */
38
- function markAutoOpened(sessionId) {
47
+ /** Mark this browser tab as already attempted. */
48
+ function markAutoOpenAttempted() {
39
49
  try {
40
- window.localStorage.setItem(AUTO_OPEN_KEY + sessionId, '1')
50
+ window.sessionStorage.setItem(AUTO_OPEN_KEY + 'loaded', '1')
41
51
  } catch {
42
52
  // ignore
43
53
  }
44
54
  }
45
-
46
- /** Open the tab once per session unless disabled in the plugin settings. */
47
- function tryAutoOpen(service, tabId) {
48
- const snapshot = sidebarSnapshot(service)
49
- if (snapshot === undefined || snapshot === null || snapshot.sessionId === undefined || snapshot.state === undefined)
50
- return
51
- const sessionId = snapshot.sessionId
52
- if (isAutoOpenDisabled(snapshot, tabId)) return
53
- if (isAutoOpenMarked(sessionId)) return
54
- if (findTabIn(snapshot.state, tabId)) {
55
- markAutoOpened(sessionId)
56
- return
57
- }
55
+ /** The most recent controller refusal, surfaced when retries run out. */
56
+ let lastAutoOpenError = null
57
+ /** One open attempt: true when the controller accepted it. */
58
+ function tryOpenTab(ctx, tries) {
58
59
  try {
59
- service.openTab({ type: tabId, title: strings.title(), path: '' })
60
- markAutoOpened(sessionId)
60
+ ctx.sidebarRight.openTab(TAB_KIND, { revealIfOpened: false })
61
+ return true
61
62
  } catch (error) {
62
- console.error('[dsh-file-activity] auto-open failed:', error)
63
+ if (tries === 0) lastAutoOpenError = error
64
+ return false
63
65
  }
64
66
  }
65
-
66
- function installAutoOpen(ctx, tabId) {
67
- const service = ctx.betterSidebar
68
- tryAutoOpen(service, tabId)
69
- let off = () => {}
70
- try {
71
- off = service.subscribeState?.(() => tryAutoOpen(service, tabId)) ?? off
72
- } catch {
73
- // service may lack subscribeState on older versions
67
+ /**
68
+ * Open the tab once the session surface exists, then stop. The tab record
69
+ * itself is persisted host-side, so a session that already shows it is not
70
+ * reopened: `openTab` focuses the existing page instead of duplicating it.
71
+ * @param ctx - client context carrying the navigation controller.
72
+ * @returns disposer cancelling a pending retry.
73
+ */
74
+ function installAutoOpen(ctx) {
75
+ if (!autoOpenEnabled()) return () => {}
76
+ if (isAutoOpenAttempted()) return () => {}
77
+ let tries = 0
78
+ const attempt = () => {
79
+ tries += 1
80
+ if (tryOpenTab(ctx, tries - 1)) {
81
+ autoOpenTimer = 0
82
+ markAutoOpenAttempted()
83
+ return
84
+ }
85
+ if (tries >= AUTO_OPEN_MAX_TRIES) {
86
+ autoOpenTimer = 0
87
+ markDegraded('auto-open', lastAutoOpenError ?? 'sidebarRight: session surface never mounted')
88
+ return
89
+ }
90
+ autoOpenTimer = window.setTimeout(attempt, AUTO_OPEN_RETRY_MS)
91
+ }
92
+ attempt()
93
+ return () => {
94
+ if (autoOpenTimer !== 0) {
95
+ window.clearTimeout(autoOpenTimer)
96
+ autoOpenTimer = 0
97
+ }
74
98
  }
75
- return off
76
99
  }
@@ -0,0 +1,62 @@
1
+ 'use strict'
2
+ // ── native document previews (metadata + plain-text renderer) ──────────
3
+ //
4
+ // The HOST owns file content on the native side: a registered preview only
5
+ // declares WHICH suffixes this implementation recognizes and HOW the document
6
+ // owner should deliver the bytes ('text-pages' = paged text window,
7
+ // 'bytes-complete' = whole file). The owner then injects the prepared
8
+ // DocumentContent into the 'sidebar.right.tab.document' seat, keyed by this
9
+ // implementation's id — so the renderer never fetches anything itself
10
+ // (the responsibility split that made 第三方查看器注册表的 matchFileViewer
11
+ // obsolete; its fetchStrategy belonged to the third-party viewer).
12
+ //
13
+ // Scope of the registration: filenames whose suffixes no shipped renderer
14
+ // claims. Anything the host already renders (code / markdown / image / pdf /
15
+ // html) keeps winning: those implementations outrank this one by longest
16
+ // suffix, and a 'fallback' band type (the shipped text preview) still beats
17
+ // nothing-else-matches. This descriptor exists so a recorded log/diff/ndjson
18
+ // file opens with a real renderer instead of the "nothing can view this"
19
+ // notice.
20
+ //
21
+ // Degradation is explicit and observable (issue #187 batch 2): a rejected
22
+ // registration is logged with its cause and marked on <html> so it can never
23
+ // fail silently.
24
+ /** Suffixes this implementation claims (no leading dot; compound is allowed). */
25
+ const PREVIEW_EXTENSIONS = ['log', 'jsonl', 'ndjson', 'diff', 'patch']
26
+ /**
27
+ * Record an extension-point failure so it is visible to users, tests and e2e.
28
+ * The <html> markers are `data-dfa-degraded` (which stage failed) plus
29
+ * `data-dfa-degraded-cause` (why), which makes a missing tab diagnosable from
30
+ * the page itself instead of only from the browser console.
31
+ */
32
+ function markDegraded(kind, error) {
33
+ const message = error instanceof Error ? error.message : String(error)
34
+ if (typeof console !== 'undefined' && typeof console.error === 'function') {
35
+ console.error('[dsh-file-activity] ' + kind + ' 注册失败: ' + message)
36
+ }
37
+ try {
38
+ const root = typeof document === 'undefined' ? undefined : document.documentElement
39
+ if (root !== undefined && root !== null && root.dataset !== undefined) {
40
+ root.dataset.dfaDegraded = kind
41
+ root.dataset.dfaDegradedCause = message.slice(0, 200)
42
+ }
43
+ } catch {
44
+ // observation only: a missing document must not break activation
45
+ }
46
+ }
47
+ /** Register the native document-preview descriptor (metadata only). */
48
+ function registerDocumentPreviews(ctx) {
49
+ ctx.effect(
50
+ () =>
51
+ ctx.documentPreviews.register({
52
+ id: TAB_ID,
53
+ extensions: PREVIEW_EXTENSIONS,
54
+ // 'extension' lets this implementation win over a shipped one on the
55
+ // same suffix; distinct suffixes keep the shipped renderers in charge.
56
+ priority: 'extension',
57
+ title: () => strings.title(),
58
+ loading: 'text-pages',
59
+ }),
60
+ 'dsh-file-activity: document previews',
61
+ )
62
+ }
@@ -1,10 +1,10 @@
1
+ 'use strict'
1
2
  // ── path / time formatting helpers ────────────────────────────────────
2
3
  function basenameOf(path) {
3
4
  const norm = path.split('\\').join('/')
4
5
  const idx = norm.lastIndexOf('/')
5
6
  return idx === -1 ? norm : norm.slice(idx + 1)
6
7
  }
7
-
8
8
  /** Compact relative time: 刚刚 / N 分钟前 / N 小时前 / N 天前 / MM/DD. */
9
9
  function formatRelative(time) {
10
10
  if (typeof time !== 'number' || !Number.isFinite(time)) return ''
@@ -19,7 +19,6 @@ function formatRelative(time) {
19
19
  const date = new Date(time)
20
20
  return `${date.getMonth() + 1}/${date.getDate()}`
21
21
  }
22
-
23
22
  /** Local wall-clock HH:MM:SS (used in tooltips; full precision). */
24
23
  function formatTime(time) {
25
24
  const date = new Date(time)
@@ -1,3 +1,4 @@
1
+ 'use strict'
1
2
  // ── i18n ──────────────────────────────────────────────────────────────
2
3
  function isZh() {
3
4
  try {
@@ -7,7 +8,6 @@ function isZh() {
7
8
  return false
8
9
  }
9
10
  }
10
-
11
11
  const strings = {
12
12
  title: () => (isZh() ? '文件活动' : 'File Activity'),
13
13
  recent: () => (isZh() ? '最近访问' : 'Recent'),
@@ -43,5 +43,19 @@ const strings = {
43
43
  isZh() ? '文件位于工作区外,暂无法读取内容' : 'The file is outside the workspace and cannot be read',
44
44
  downloadToView: () => (isZh() ? '下载查看' : 'download to view'),
45
45
  clickOutsideToClose: () => (isZh() ? '点击外部关闭' : 'Click outside to close'),
46
+ guideDescription: () =>
47
+ isZh()
48
+ ? '查看 agent 与侧边栏读写过的文件(最近访问 + 目录统计)'
49
+ : 'Files the agent and the sidebar touched (recent + tree)',
50
+ autoOpenLabel: () => (isZh() ? '会话开始时自动打开' : 'Auto-open on session start'),
51
+ autoOpenHint: () =>
52
+ isZh()
53
+ ? '每个会话首次打开时自动显示本页;关闭后仍可从侧边栏右上角的「新标签页」手动打开。'
54
+ : 'Shows this page once per session. When off, open it from the sidebar new-tab control.',
55
+ tabUnavailable: () => (isZh() ? '侧边栏扩展点不可用' : 'Sidebar extension point unavailable'),
56
+ tabUnavailableHint: () =>
57
+ isZh()
58
+ ? '本插件未能注册页签(宿主原生 API 可能已变更)。原因见浏览器控制台。'
59
+ : 'This plugin could not register its tab (the host native API may have changed). See the browser console.',
46
60
  autoCloseHint: () => (isZh() ? '预览失败,即将自动关闭' : 'Preview failed — closing automatically'),
47
61
  }
@@ -1,40 +1,29 @@
1
- // ── fetch interception: sidebar file operations ───────────────────────
2
- function methodOf(init) {
3
- return (init?.method ?? 'GET').toUpperCase()
4
- }
5
-
6
- /** POST body as a plain object (non-string bodies are ignored). */
7
- function parseBody(init) {
8
- return typeof init?.body === 'string' ? JSON.parse(init.body) : {}
9
- }
10
-
11
- /** Record fs.read / fs.write POSTs observed on the sidebar API. */
12
- function recordSidebarFs(url, init) {
13
- if (url.pathname !== '/sidebar/api/fs.read' && url.pathname !== '/sidebar/api/fs.write') return
14
- if (methodOf(init) !== 'POST') return
15
- const body = parseBody(init)
16
- if (typeof body.sessionId !== 'string' || typeof body.path !== 'string') return
17
- postRecord(body.sessionId, body.path, url.pathname === '/sidebar/api/fs.write' ? 'write' : 'read')
18
- }
19
-
20
- /** Record sidebar media opens (/sidebar/file?sessionId=...&path=...). */
21
- function recordMediaOpen(url, init) {
22
- if (url.pathname !== '/sidebar/file' || methodOf(init) !== 'GET') return
1
+ 'use strict'
2
+ // ── fetch interception: the plugin's own file routes ───────────────────
3
+ //
4
+ // Before the native migration this module also watched the THIRD-PARTY
5
+ // sidebar's routes (/sidebar/api/fs.read|fs.write, /sidebar/file). Those are
6
+ // the host's internal surface now, fed by the host's own file provider and
7
+ // document owner — the plugin neither opens nor owns them, so observing them
8
+ // would record host traffic that is already recorded through the plugin's
9
+ // fs/observed host half. What remains is the plugin's own media/text route,
10
+ // whose reads ARE the user's previews.
11
+ /** Record requests for the plugin's own file route (media + text previews). */
12
+ function recordOwnMediaOpen(url, init) {
13
+ if (url.pathname !== '/file-activity/file') return
14
+ if ((init?.method ?? 'GET').toUpperCase() !== 'GET') return
23
15
  const sessionId = url.searchParams.get('sessionId')
24
16
  const path = url.searchParams.get('path')
25
17
  if (sessionId !== null && path !== null) postRecord(sessionId, path, 'read')
26
18
  }
27
-
28
- /** Observe a resolved fetch URL and record sidebar file operations. */
19
+ /** Observe a resolved fetch URL and record plugin file operations. */
29
20
  function observeSidebarFetch(url, init) {
30
21
  try {
31
- recordSidebarFs(url, init)
32
- recordMediaOpen(url, init)
22
+ recordOwnMediaOpen(url, init)
33
23
  } catch {
34
24
  // observation must never break the underlying call
35
25
  }
36
26
  }
37
-
38
27
  function installFetchInterceptor() {
39
28
  const original = window.fetch.bind(window)
40
29
  window.fetch = (input, init) => {