dsh-claude-style 0.6.1 → 0.6.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 (3) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/lib/client.js +182 -58
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable changes to `dsh-claude-style` are documented here, newest first.
4
4
 
5
+ ## [0.6.4] - 2026-09-24
6
+
7
+ [中文](#cn-0.6.4) | [English](#en-0.6.4)
8
+
9
+ <h3 id="cn-0.6.4">问题修复</h3>
10
+
11
+ - 修复 **刚启动、账号抽屉还没展开过时,页脚里的插件条目一直留在账户行旁边**:隐藏页脚条目与把条目镜像进抽屉原本挤在同一次调用里,而抽屉没打开时这次调用会提前返回,于是宿主重新渲染出来的插件图标继续画在侧栏底部、紧挨账户行,直到抽屉被展开过一次才被标记隐藏——这正是该现象只在首次启动后出现的原因。现在隐藏条目每次同步都跑,只有镜像需要抽屉容器。
12
+ - 修复 **底栏统计弹层偶尔只剩一段(只显示会话统计,或只显示 Token 用量)**:皮肤按标签文字区分宿主的两颗药丸,读取窗口又只有 300 毫秒,而宿主的两个面板按它自己的提交节奏挂载,晚到一步就丢掉那一段。现在按面板自身的标记判定类型,等待拉长到 800 毫秒、读失败时重试一次;药丸按钮每一步都从 DOM 重新取,窗口内还会再按一次——宿主重渲染换掉节点时,按旧节点等于没按;首次只读到一段时安排一次补读把缺的那段补上,卡片只增不减,已经打开的卡片不会被一次失败的读取改小。会话本身没有计时数据时宿主也不渲染计时面板,此时只有 Token 用量是正确的。
13
+ - 修复 **0.1.7-rc.2 里工作区会话行的状态圈消失**:新宿主把会话行的前导座位交给槽位出口渲染,座位里因此始终有一个 `div[data-slot]` 包裹层(`display: contents`),座位不再是空元素,画在 `:empty` 上的圆圈就不再出现。圆圈现在也挂在空的槽位出口上;座位里带运行状态点时仍由状态点自己绘制。
14
+ - 修复 **账户抽屉收起时它的行与图标停在账户行上方**:自建抽屉的内容要在收起状态下预先对账(镜像行只在收起时同步、展开时冻结),所以收起时面板连同齿轮图标一直挂在账户行正上方,此前只用透明度隐藏,内容仍留在绘制与命中树里。收起状态改用 `visibility` 隐藏(保留布局,关闭的淡出照常),展开时恢复可见。
15
+
16
+ <h3 id="en-0.6.4">Bug Fixes</h3>
17
+
18
+ - Fix **the footer's plugin entries staying beside the account row after a fresh start until the drawer has been opened once**: hiding the entries and mirroring them into the drawer shared one call that returned early while no drawer was up, so the plugin icons the host re-rendered stayed painted at the sidebar's bottom next to the account row until the drawer had been opened once — which is why the symptom only appeared after a fresh start. The hiding now runs on every sync; only the mirroring needs the drawer's container.
19
+ - Fix **the stats card occasionally showing only one section (session statistics alone, or Token usage alone)**: the skin told the host's two pills apart by their label text and read with a 300 ms window, while the host mounts its panels on its own commit — one late panel lost that section. The read now identifies each panel by its own marker, waits up to 800 ms and retries once; it re-resolves the pill from the DOM at every step and presses the live node again while the window lasts, because a press on a node the host has since re-rendered goes nowhere; and a card that did come up short schedules one late re-read that fills the missing section in, so the card only ever grows and a failed read never shrinks a card already on screen. A session with no timing data renders no timing panel in the host either, where Token usage alone is correct.
20
+ - Fix **the workspace session rows' status circle disappearing on 0.1.7-rc.2**: the newer host renders the row's leading seat through a slot outlet, so the seat always carries a `div[data-slot]` anchor (`display: contents`) and is never empty — the circle drawn on `:empty` stopped appearing. The circle now hangs on the empty outlet anchor as well; a seat carrying the running status dot still keeps the dot's own paint.
21
+ - Fix **the account drawer's rows and icons parked above the account row while it is closed**: the self-built drawer's content is reconciled while closed by design (the mirror sync only runs then and freezes while open), so the panel and its gear icon sit right above the account row; opacity alone left that content in the paint and hit-test tree. The closed state now hides it with `visibility` (layout kept, closing fade intact) and the open state restores it.
22
+
23
+ **Full Changelog**: [v0.6.1...v0.6.4](https://github.com/Nwflower/dsh-claude-style/compare/v0.6.1...v0.6.4)
24
+
5
25
  ## [0.6.1] - 2026-09-24
6
26
 
7
27
  [中文](#cn-0.6.1) | [English](#en-0.6.1)
package/lib/client.js CHANGED
@@ -2920,7 +2920,12 @@ window.__ModuleLoader__.load({
2920
2920
  "body[data-dsh-claude-style] [class*=\"projectRow\"][aria-expanded=\"false\"] [class*=\"chevron\"]::after {",
2921
2921
  " transform: rotate(-90deg);",
2922
2922
  "}",
2923
- "/* Session status circle indicator (matching Claude style) */",
2923
+ "/* Session status circle indicator (matching Claude style). The host's newer",
2924
+ " sidebar renders the leading seat through a slot outlet, whose anchor",
2925
+ " (`div[data-slot]`, display:contents) is a child of the seat even when nothing",
2926
+ " is registered — an idle row's seat is therefore no longer :empty, so the",
2927
+ " circle hangs on the empty anchor as well. A seat carrying status dots or",
2928
+ " another plugin's content keeps its own paint and gets no circle. */",
2924
2929
  "body[data-dsh-claude-style] [class*=\"sessionRow\"] [class*=\"slot\"] {",
2925
2930
  " width: 14px !important;",
2926
2931
  " height: 16px !important;",
@@ -2928,7 +2933,8 @@ window.__ModuleLoader__.load({
2928
2933
  " align-items: center !important;",
2929
2934
  " justify-content: center !important;",
2930
2935
  "}",
2931
- "body[data-dsh-claude-style] [class*=\"sessionRow\"] [class*=\"slot\"]:empty::after {",
2936
+ "body[data-dsh-claude-style] [class*=\"sessionRow\"] [class*=\"slot\"]:empty::after,",
2937
+ "body[data-dsh-claude-style] [class*=\"sessionRow\"] [class*=\"slot\"] > [data-slot]:empty::after {",
2932
2938
  " content: \"\";",
2933
2939
  " display: inline-block;",
2934
2940
  " width: 5px;",
@@ -3255,15 +3261,24 @@ window.__ModuleLoader__.load({
3255
3261
  " flex-direction: column !important;",
3256
3262
  " gap: 6px !important;",
3257
3263
  " opacity: 0 !important;",
3264
+ " /* The drawer's rows are built and reconciled while it is closed (the mirror",
3265
+ " sync only runs then), so the panel sits right above the account row with",
3266
+ " its icons in it. opacity alone leaves that content in the paint and",
3267
+ " hit-test tree; visibility takes it out of both, and the delayed transition",
3268
+ " keeps the closing fade. Layout is untouched, so the closed drawer's box is",
3269
+ " still measurable. */",
3270
+ " visibility: hidden !important;",
3258
3271
  " pointer-events: none !important;",
3259
3272
  " transform: translateY(6px) scale(0.98) !important;",
3260
3273
  " transform-origin: bottom left !important;",
3261
- " transition: opacity 0.15s ease, transform 0.15s ease !important;",
3274
+ " transition: opacity 0.15s ease, transform 0.15s ease, visibility 0s linear 0.15s !important;",
3262
3275
  "}",
3263
3276
  "body[data-dsh-claude-style] .dsh-claude-account-popover[data-open=\"true\"] {",
3264
3277
  " opacity: 1 !important;",
3278
+ " visibility: visible !important;",
3265
3279
  " pointer-events: auto !important;",
3266
3280
  " transform: translateY(0) scale(1) !important;",
3281
+ " transition: opacity 0.15s ease, transform 0.15s ease, visibility 0s !important;",
3267
3282
  "}",
3268
3283
  "body[data-dsh-claude-style][data-ds-dark-theme] .dsh-claude-account-popover {",
3269
3284
  " background: #1e1e1d !important;",
@@ -6731,49 +6746,114 @@ window.__ModuleLoader__.load({
6731
6746
  return rows
6732
6747
  }
6733
6748
 
6734
- function statsPanelData(selector) {
6735
- var panel = document.querySelector(selector)
6736
- return {
6737
- title: panel === null ? '' : (panel.getAttribute('aria-label') || ''),
6738
- rows: statsRowsFrom(panel),
6749
+ /** How long one pill's panel may take to mount (the host commits on its own schedule). */
6750
+ var STATS_PANEL_POLL_MS = 25
6751
+ var STATS_PANEL_ATTEMPTS = 32
6752
+ /** When a click is repeated inside that window, and how often. */
6753
+ var STATS_PANEL_REPRESS_AT = 12
6754
+ var STATS_PANEL_REPRESS_MAX = 2
6755
+ /** How often a read that could not open a panel is retried, and after what delay. */
6756
+ var STATS_READ_RETRIES = 1
6757
+ var STATS_RETRY_MS = 120
6758
+ /** How long after a short card the one late re-read runs. */
6759
+ var STATS_FILL_MS = 700
6760
+
6761
+ /** The stats panel that is open right now, with the kind its own marker names. */
6762
+ function openStatsPanel() {
6763
+ var details = document.querySelector('[data-session-stats-details]')
6764
+ if (details !== null) {
6765
+ var detailsDialog = details.closest('[role="dialog"]')
6766
+ if (detailsDialog !== null) return { kind: 'details', panel: detailsDialog }
6739
6767
  }
6768
+ var usage = document.querySelector('[data-session-stats-usage]')
6769
+ if (usage !== null) {
6770
+ var usageDialog = usage.closest('[role="dialog"]')
6771
+ if (usageDialog !== null) return { kind: 'usage', panel: usageDialog }
6772
+ }
6773
+ return null
6740
6774
  }
6741
6775
 
6742
- function withStatsPanel(button, selector, done) {
6743
- if (button === null) { done({ title: '', rows: [] }); return }
6744
- if (button.getAttribute('aria-expanded') !== 'true') button.click()
6776
+ /** The row's pill buttons as they are right now (the host replaces them on re-render). */
6777
+ function statsPillAt(index) {
6778
+ var root = statsRoot()
6779
+ if (root === null) return null
6780
+ var buttons = root.querySelectorAll('button')
6781
+ return index < buttons.length ? buttons[index] : null
6782
+ }
6783
+
6784
+ /**
6785
+ * Open one pill, read whichever stats panel it shows, and close it again.
6786
+ *
6787
+ * The read is kind-agnostic on purpose: which panel appears is the host's
6788
+ * own answer, so a pill is never mislabelled by its label text (the timing
6789
+ * pill's label carries a tok/s reading as well, and the timing pill is a
6790
+ * plain span — no trigger at all — while the session has no timing yet).
6791
+ * The panel counts only while that pill reports itself expanded, so a
6792
+ * sibling's panel that has not unmounted yet is never read as this one's.
6793
+ * The pill is re-resolved from the DOM at every step and pressed again
6794
+ * while the window lasts: the host re-renders the row on its own schedule,
6795
+ * and a click on a node it has since replaced goes nowhere. `null` means
6796
+ * the row has no such pill; a pill that shows no panel inside the window
6797
+ * reports a failure, which is not an absent section.
6798
+ */
6799
+ function readStatsPill(index, done) {
6800
+ var presses = 0
6801
+ function press() {
6802
+ var pill = statsPillAt(index)
6803
+ if (pill === null) return false
6804
+ if (pill.getAttribute('aria-expanded') !== 'true') pill.click()
6805
+ return true
6806
+ }
6807
+ if (!press()) { done(null); return }
6745
6808
  var attempts = 0
6746
6809
  function read() {
6747
- var panel = document.querySelector(selector)
6748
- if (panel === null && attempts < 15) {
6810
+ var pill = statsPillAt(index)
6811
+ if (pill === null) { done({ failed: true, rows: [] }); return }
6812
+ var open = pill.getAttribute('aria-expanded') === 'true' ? openStatsPanel() : null
6813
+ if (open === null && attempts < STATS_PANEL_ATTEMPTS) {
6749
6814
  attempts += 1
6750
- setTimeout(read, 20)
6815
+ if (attempts === STATS_PANEL_REPRESS_AT && presses < STATS_PANEL_REPRESS_MAX) {
6816
+ presses += 1
6817
+ press()
6818
+ }
6819
+ setTimeout(read, STATS_PANEL_POLL_MS)
6751
6820
  return
6752
6821
  }
6753
- var data = statsPanelData(selector)
6754
- if (button.getAttribute('aria-expanded') === 'true') button.click()
6755
- done(data)
6822
+ if (pill.getAttribute('aria-expanded') === 'true') pill.click()
6823
+ if (open === null) { done({ failed: true, rows: [] }); return }
6824
+ done({ kind: open.kind, title: open.panel.getAttribute('aria-label') || '', rows: statsRowsFrom(open.panel) })
6756
6825
  }
6757
- setTimeout(read, 20)
6826
+ setTimeout(read, STATS_PANEL_POLL_MS)
6758
6827
  }
6759
6828
 
6829
+ /**
6830
+ * Read both of the host's pills.
6831
+ *
6832
+ * `done(sections, incomplete)`: the sections in the host's own order
6833
+ * (session statistics, then token usage), and `incomplete` true when a
6834
+ * pill the row carries could not be read — the caller must not mistake
6835
+ * that for a section the host does not have.
6836
+ */
6760
6837
  function collectStatsData(done) {
6761
- var root = statsRoot()
6762
- if (root === null) { done([]); return }
6763
- var buttons = root.querySelectorAll('button')
6764
- var timeBtn = null
6765
- var usageBtn = null
6766
- for (var i = 0; i < buttons.length; i++) {
6767
- var aria = buttons[i].getAttribute('aria-label') || ''
6768
- if (/轮|步|turns?|steps?/i.test(aria)) timeBtn = buttons[i]
6769
- else usageBtn = buttons[i]
6770
- }
6771
- withStatsPanel(timeBtn, '[role="dialog"]:has([data-session-stats-details])', function (timeData) {
6772
- withStatsPanel(usageBtn, '[role="dialog"]:has([data-session-stats-usage])', function (usageData) {
6838
+ if (statsRoot() === null) { done([], false); return }
6839
+ readStatsPill(0, function (first) {
6840
+ readStatsPill(1, function (second) {
6841
+ var reads = [first, second]
6773
6842
  var sections = []
6774
- if (timeData.rows.length > 0) sections.push(timeData)
6775
- if (usageData.rows.length > 0) sections.push(usageData)
6776
- done(sections)
6843
+ var incomplete = false
6844
+ for (var i = 0; i < reads.length; i++) {
6845
+ var read = reads[i]
6846
+ if (read === null) continue
6847
+ if (read.failed === true || read.rows.length === 0) {
6848
+ incomplete = true
6849
+ continue
6850
+ }
6851
+ sections.push(read)
6852
+ }
6853
+ sections.sort(function (a, b) {
6854
+ return (a.kind === 'details' ? 0 : 1) - (b.kind === 'details' ? 0 : 1)
6855
+ })
6856
+ done(sections, incomplete)
6777
6857
  })
6778
6858
  })
6779
6859
  }
@@ -6841,12 +6921,34 @@ window.__ModuleLoader__.load({
6841
6921
  pop.innerHTML = html
6842
6922
  }
6843
6923
 
6844
- /** The last full read; a thinner one never replaces it while the card is up. */
6924
+ /** Whether the card is up, so a failed re-read can leave its content alone. */
6925
+ function isStatsPopoverOpen() {
6926
+ return statsPopover !== null && statsPopover.getAttribute('data-open') === 'true'
6927
+ }
6845
6928
 
6846
- var statsSections = null
6929
+ /** How many sections the card currently shows. */
6930
+ var renderedSections = 0
6847
6931
 
6848
- var statsSectionsAt = 0
6932
+ /** Place the card above the stats row, clamped to the viewport. */
6933
+ function placeStatsPopover(anchor) {
6934
+ var pop = ensureStatsPopover()
6935
+ var live = statsRoot() || anchor
6936
+ if (live === null) return
6937
+ var rect = live.getBoundingClientRect()
6938
+ var width = pop.offsetWidth
6939
+ var height = pop.offsetHeight
6940
+ var left = Math.max(8, Math.min(rect.left + rect.width / 2 - width / 2, window.innerWidth - width - 8))
6941
+ var top = Math.max(8, rect.top - height - 8)
6942
+ pop.style.left = left + 'px'
6943
+ pop.style.top = top + 'px'
6944
+ }
6849
6945
 
6946
+ function showStatsSections(anchor, sections) {
6947
+ renderStatsPopover(sections)
6948
+ renderedSections = sections.length
6949
+ ensureStatsPopover().setAttribute('data-open', 'true')
6950
+ placeStatsPopover(anchor)
6951
+ }
6850
6952
 
6851
6953
  function showStatsPopover(anchor) {
6852
6954
  // The card reads the host's two dialogs, which only the detailed row
@@ -6858,29 +6960,40 @@ window.__ModuleLoader__.load({
6858
6960
  clearTimeout(statsHideTimer)
6859
6961
  statsHideTimer = null
6860
6962
  }
6861
- collectStatsData(function (sections) {
6963
+ readStatsCard(anchor, 0)
6964
+ }
6965
+
6966
+ /**
6967
+ * Collect the host's panels and render the card.
6968
+ *
6969
+ * A read that could not open one of the panels is retried, and one that
6970
+ * still comes back short while the card is already up leaves the card as
6971
+ * it is: the host's panels mount on its own commit, so a read that lost
6972
+ * the race must never read as a section the host does not have — that was
6973
+ * the 'click and it drops to Token usage only' card. A card that did come
6974
+ * up short schedules one late re-read, which fills it in and never
6975
+ * shrinks it.
6976
+ */
6977
+ function readStatsCard(anchor, attempt) {
6978
+ collectStatsData(function (sections, incomplete) {
6862
6979
  if (sections.length === 0) return
6863
- // A click re-collects, and the host's two pills can be mid-flight then: a read
6864
- // that comes back with fewer sections than the card already shows must not
6865
- // shrink it — that is the 'click and it drops to Token usage only' bug.
6866
- var fresh = Date.now() - statsSectionsAt < 180000
6867
- if (statsSections !== null && fresh && sections.length < statsSections.length) {
6868
- sections = statsSections
6869
- } else {
6870
- statsSections = sections
6871
- statsSectionsAt = Date.now()
6980
+ if (incomplete && attempt < STATS_READ_RETRIES) {
6981
+ setTimeout(function () { readStatsCard(anchor, attempt + 1) }, STATS_RETRY_MS)
6982
+ return
6872
6983
  }
6873
- renderStatsPopover(sections)
6874
- var pop = ensureStatsPopover()
6875
- pop.setAttribute('data-open', 'true')
6876
- var live = statsRoot() || anchor
6877
- var rect = live.getBoundingClientRect()
6878
- var width = pop.offsetWidth
6879
- var height = pop.offsetHeight
6880
- var left = Math.max(8, Math.min(rect.left + rect.width / 2 - width / 2, window.innerWidth - width - 8))
6881
- var top = Math.max(8, rect.top - height - 8)
6882
- pop.style.left = left + 'px'
6883
- pop.style.top = top + 'px'
6984
+ if (incomplete && isStatsPopoverOpen()) return
6985
+ showStatsSections(anchor, sections)
6986
+ if (incomplete) setTimeout(function () { fillStatsCard(anchor) }, STATS_FILL_MS)
6987
+ })
6988
+ }
6989
+
6990
+ /** One late re-read for a card that came up short; it only ever adds. */
6991
+ function fillStatsCard(anchor) {
6992
+ collectStatsData(function (sections, incomplete) {
6993
+ if (incomplete || sections.length === 0) return
6994
+ if (!isStatsPopoverOpen()) return
6995
+ if (sections.length <= renderedSections) return
6996
+ showStatsSections(anchor, sections)
6884
6997
  })
6885
6998
  }
6886
6999
 
@@ -11056,6 +11169,12 @@ window.__ModuleLoader__.load({
11056
11169
  var footerEntries = syncFooterActionVisibility(footerActions)
11057
11170
  var body = options.body()
11058
11171
 
11172
+ // A closed host drawer has no container to mirror into, but the entries
11173
+ // above are hidden in place all the same: the host re-renders them, and a
11174
+ // pass that skips this leaves their icons painted in the sidebar until the
11175
+ // drawer is opened once.
11176
+ if (body === null) return
11177
+
11059
11178
  // While the popover is open, its mirrors must stay completely static:
11060
11179
  // a content rewrite, reorder, or embedded-clone replacement under the
11061
11180
  // pointer cancels the browser's :hover state and can swallow the click
@@ -11746,10 +11865,15 @@ window.__ModuleLoader__.load({
11746
11865
  syncAccountRowBox(entryRow, footArea)
11747
11866
  if (hostTrigger !== null) bindHostRowHover(hostTrigger)
11748
11867
 
11868
+ // The host's footer entries are hidden in place on every pass, drawer or
11869
+ // no drawer: the host re-renders them, and a pass that skipped this left
11870
+ // their icons painted beside the account row until the drawer had been
11871
+ // opened once. Only the mirroring needs a container to write into.
11872
+ mirror.sync(footArea)
11873
+
11749
11874
  var root = surface.mode() === 'host' ? surface.container() : accountPopover
11750
11875
  if (root === null) return
11751
11876
  rows.syncHeader(root, hostTrigger)
11752
- mirror.sync(footArea)
11753
11877
  // The rail toggle (and any reflow) moves the anchor without a window
11754
11878
  // resize or a page scroll, so an open drawer re-resolves its position at
11755
11879
  // the end of its own pass.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-claude-style",
3
3
  "description": "Claude Code Desktop theme for DeepSeek Harness (dsh) web GUI — visual & interaction overhaul",
4
- "version": "0.6.1",
4
+ "version": "0.6.4",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "icon": "lib/claude-mark.svg",