dsh-links-panel 0.78.1 → 0.79.1

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/lib/client.js CHANGED
@@ -1172,7 +1172,7 @@ window.__ModuleLoader__.load({
1172
1172
  * only from successful web_fetch results; nothing is fetched by the plugin itself.
1173
1173
  */
1174
1174
  const FAVICON_LIBRARY_KEY = 'dsh-links.favicons.v1'
1175
- const FAVICON_LIBRARY_LIMIT = 800
1175
+ const FAVICON_LIBRARY_LIMIT = 150
1176
1176
 
1177
1177
  /** @returns {Record<string, string>} favicon URL by host or domain. */
1178
1178
  function loadFaviconLibrary() {
@@ -2464,6 +2464,21 @@ window.__ModuleLoader__.load({
2464
2464
  */
2465
2465
  /** Cached luminance analysis of one icon data URL. */
2466
2466
  const ICON_TONE_CACHE = new Map()
2467
+ const ICON_TONE_CACHE_LIMIT = 300
2468
+
2469
+ /**
2470
+ * Remember one analysis without letting the cache grow unbounded.
2471
+ * @param {string} src - data URL key.
2472
+ * @param {object | null} tone - analysis result.
2473
+ * @returns {void}
2474
+ */
2475
+ function rememberIconTone(src, tone) {
2476
+ if (ICON_TONE_CACHE.size >= ICON_TONE_CACHE_LIMIT) {
2477
+ const oldest = ICON_TONE_CACHE.keys().next()
2478
+ if (!oldest.done) ICON_TONE_CACHE.delete(oldest.value)
2479
+ }
2480
+ ICON_TONE_CACHE.set(src, tone)
2481
+ }
2467
2482
 
2468
2483
  /**
2469
2484
  * Average luminance and saturation of an icon, so a monochrome mark that would
@@ -2506,7 +2521,7 @@ window.__ModuleLoader__.load({
2506
2521
  count += 1
2507
2522
  }
2508
2523
  const result = count === 0 ? null : { luma: luma / count, saturation: saturation / count }
2509
- ICON_TONE_CACHE.set(src, result)
2524
+ rememberIconTone(src, result)
2510
2525
  resolve(result)
2511
2526
  } catch (error) {
2512
2527
  resolve(null)
@@ -2608,6 +2623,38 @@ window.__ModuleLoader__.load({
2608
2623
  return trf('time.daysAgo', { count: Math.floor(delta / 86400000) })
2609
2624
  }
2610
2625
 
2626
+ /**
2627
+ * Throttle an event-stream snapshot so a streaming answer does not re-aggregate the
2628
+ * whole conversation on every token. Leading call is immediate; later ones land at
2629
+ * most once per `wait` milliseconds, and the last value always arrives.
2630
+ * @param {{entries: Array, hasMore: boolean}} snapshot - live session snapshot.
2631
+ * @param {number} wait - minimum interval in milliseconds.
2632
+ * @returns {{entries: Array, hasMore: boolean}} throttled snapshot.
2633
+ */
2634
+ function useThrottledSnapshot(snapshot, wait) {
2635
+ const [value, setValue] = React.useState(snapshot)
2636
+ const stateRef = React.useRef({ last: 0, timer: null, pending: snapshot })
2637
+ React.useEffect(() => {
2638
+ const state = stateRef.current
2639
+ state.pending = snapshot
2640
+ const flush = () => {
2641
+ state.timer = null
2642
+ state.last = Date.now()
2643
+ setValue(state.pending)
2644
+ }
2645
+ const elapsed = Date.now() - state.last
2646
+ if (elapsed >= wait) flush()
2647
+ else if (state.timer === null) state.timer = setTimeout(flush, wait - elapsed)
2648
+ }, [snapshot, wait])
2649
+ React.useEffect(
2650
+ () => () => {
2651
+ if (stateRef.current.timer !== null) clearTimeout(stateRef.current.timer)
2652
+ },
2653
+ [],
2654
+ )
2655
+ return value
2656
+ }
2657
+
2611
2658
  /**
2612
2659
  * One mention row inside the “mentioned in N turns” dialog.
2613
2660
  * @param {{item: object, active: boolean, onHover: Function, onSelect: Function}} props - row props.
@@ -3045,12 +3092,15 @@ window.__ModuleLoader__.load({
3045
3092
  scanStoreRef.current = { cache: new Map(), callNames: new Map() }
3046
3093
  }
3047
3094
  // Rows = conversation items table (cached rows ∪ current window scan).
3095
+ // The stream is throttled: a long answer emits many events, and re-aggregating
3096
+ // on each of them is what made the panel feel heavy while the agent typed.
3097
+ const liveSnapshot = useThrottledSnapshot(snapshot, 300)
3048
3098
  const all = React.useMemo(() => {
3049
3099
  const store = scanStoreRef.current
3050
- const scanned = aggregate(snapshot.entries, store.cache, store.callNames, faviconHintsRef.current)
3100
+ const scanned = aggregate(liveSnapshot.entries, store.cache, store.callNames, faviconHintsRef.current)
3051
3101
  let newest = scannedSeqRef.current
3052
3102
  let lowest = scannedMinSeqRef.current
3053
- for (const entry of snapshot.entries || []) {
3103
+ for (const entry of liveSnapshot.entries || []) {
3054
3104
  const seq = entry && entry.event ? entry.event.seq : null
3055
3105
  if (typeof seq !== 'number') continue
3056
3106
  if (seq > newest) newest = seq
@@ -3061,7 +3111,7 @@ window.__ModuleLoader__.load({
3061
3111
  if (!snapshot.hasMore) return scanned
3062
3112
  if (linksCache.rows.length === 0) return scanned
3063
3113
  return mergeRows(linksCache.rows, scanned)
3064
- }, [snapshot, linksCache])
3114
+ }, [liveSnapshot, linksCache])
3065
3115
 
3066
3116
  // Persist the favicon cache (debounced) and flush it when the panel unmounts, so
3067
3117
  // switching conversations never loses icons that were just fetched.
@@ -3145,15 +3195,11 @@ window.__ModuleLoader__.load({
3145
3195
  const candidates = [
3146
3196
  `https://${target.host}/favicon.ico`,
3147
3197
  `https://${target.host}/apple-touch-icon.png`,
3148
- `https://${target.host}/favicon.png`,
3149
- `https://${target.host}/favicon.svg`,
3150
3198
  ]
3151
3199
  if (target.domain !== target.host) {
3152
3200
  candidates.push(
3153
3201
  `https://${target.domain}/favicon.ico`,
3154
3202
  `https://${target.domain}/apple-touch-icon.png`,
3155
- `https://${target.domain}/favicon.png`,
3156
- `https://${target.domain}/favicon.svg`,
3157
3203
  )
3158
3204
  }
3159
3205
  let found = ''
@@ -3290,34 +3336,14 @@ window.__ModuleLoader__.load({
3290
3336
  }
3291
3337
  const run = async () => {
3292
3338
  try {
3293
- const base = new URL('/dsh-links/meta', document.baseURI)
3294
- base.searchParams.set('urls', JSON.stringify([reference.url]))
3295
- const response = await fetch(base)
3296
- let iconUrl = ''
3297
- if (response && response.ok) {
3298
- const payload = await response.json()
3299
- const page = payload && payload.pages && payload.pages[reference.url]
3300
- iconUrl = page && typeof page.icon === 'string' ? page.icon : ''
3301
- }
3302
- if (iconUrl !== '') {
3303
- const dataUrl = await requestFavicon(iconUrl)
3304
- if (dataUrl !== '') {
3305
- setFaviconLibrary((current) => (current[host] ? current : { ...current, [host]: dataUrl }))
3306
- return
3307
- }
3308
- }
3309
3339
  const candidates = [
3310
3340
  `https://${host}/favicon.ico`,
3311
3341
  `https://${host}/apple-touch-icon.png`,
3312
- `https://${host}/favicon.png`,
3313
- `https://${host}/favicon.svg`,
3314
3342
  ]
3315
3343
  if (domain !== host) {
3316
3344
  candidates.push(
3317
3345
  `https://${domain}/favicon.ico`,
3318
3346
  `https://${domain}/apple-touch-icon.png`,
3319
- `https://${domain}/favicon.png`,
3320
- `https://${domain}/favicon.svg`,
3321
3347
  )
3322
3348
  }
3323
3349
  for (const candidate of candidates) {
@@ -4115,7 +4141,7 @@ window.__ModuleLoader__.load({
4115
4141
  const inject = ['slots', 'sidebarRightTabs', 'sessions', 'locale']
4116
4142
 
4117
4143
  /** Build stamp shown in the footer, so a stale bundle is visible at a glance. */
4118
- const BUILD = 'v0.78.1'
4144
+ const BUILD = 'v0.79.1'
4119
4145
  /**
4120
4146
  * Translate one key and fill `{name}` placeholders.
4121
4147
  * @param {string} key - dictionary key.
@@ -4427,7 +4453,7 @@ window.__ModuleLoader__.load({
4427
4453
  }
4428
4454
  }
4429
4455
  visit(snapshot, 0)
4430
- return [...ids].slice(0, 5)
4456
+ return [...ids].slice(0, 3)
4431
4457
  }
4432
4458
 
4433
4459
  const attach = (sessionId) => {
@@ -4467,7 +4493,10 @@ window.__ModuleLoader__.load({
4467
4493
  }
4468
4494
  const schedule = () => {
4469
4495
  if (tracker.timer !== null) return
4470
- tracker.timer = setTimeout(flush, 400)
4496
+ // Long debounce: the tracker only needs to be current when the panel
4497
+ // opens, not on every streamed token, and synchronous merges+storage
4498
+ // writes on the UI thread are what make streaming feel janky.
4499
+ tracker.timer = setTimeout(flush, 1500)
4471
4500
  }
4472
4501
  schedule()
4473
4502
  tracker.dispose = source.subscribe(schedule)
package/lib/index.js CHANGED
@@ -37,7 +37,7 @@ function withOutboundLock(task) {
37
37
  }
38
38
 
39
39
  /** Host build stamp, echoed to the panel so a stale bundle is detectable. */
40
- export const BUILD = 'v0.78.1'
40
+ export const BUILD = 'v0.79.1'
41
41
  /**
42
42
  * The desktop browser reaches the network through the system proxy; the Host's own
43
43
  * Node requests bypass it. Detect that proxy so probes can use the same route.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-links-panel",
3
- "version": "0.78.1",
3
+ "version": "0.79.1",
4
4
  "description": "Reference links panel for DeepSeek Harness: collects every URL of a conversation into the right sidebar with pins, turn counts, metadata and favicon caching. DSH 右栏「应用链接」面板。",
5
5
  "license": "MIT",
6
6
  "type": "module",