@aryacode/dsh-aryacode-polish 0.8.1 → 0.9.6

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/CHANGELOG.md CHANGED
@@ -1,12 +1,80 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.6] - 2026-10-28
4
+
5
+ ### Fixed
6
+
7
+ - A workspace whose Host title merely repeats its id is humanised, because that is
8
+ what the default workspace's title is.
9
+
10
+ ## [0.9.5] - 2026-10-28
11
+
12
+ ### Added
13
+
14
+ - Better workspace carries the add control the stock browser had: a header with a
15
+ "+" that opens this bundle's own source menu (On this PC or a GitHub repository)
16
+ and adds the chosen folder as a workspace.
17
+ - The default workspace no longer shows its raw id: an empty Host `title` is
18
+ humanised ("default-workspace" reads "Default workspace").
19
+
20
+ ## [0.9.4] - 2026-10-28
21
+
22
+ ### Fixed
23
+
24
+ - The seat's `inject()` is called with the standard props the runtime captured, so the
25
+ menu renders again after 0.9.3 referenced a name that only lived in the factory.
26
+
27
+ ## [0.9.3] - 2026-10-28
28
+
29
+ ### Fixed
30
+
31
+ - Each menu seat's own `inject()` runs before its occupant renders, so this bundle's
32
+ "Open workspace folder" row is back beside the shipped rows instead of silently
33
+ rendering nothing for want of the services it declared.
34
+
35
+ ## [0.9.2] - 2026-10-28
36
+
37
+ ### Fixed
38
+
39
+ - The shipped session menu rows (Pin, Rename, Fork, Archive) now receive the standard
40
+ hooks they call -- `usePinned`, `useArchived`, `useSessionAccess` and `useShortcuts` --
41
+ derived from the snapshots this seat already subscribes to, instead of throwing
42
+ `TypeError: usePinned is not a function` inside `usePinState`.
43
+
44
+ ## [0.9.1] - 2026-10-28
45
+
46
+ ### Fixed
47
+
48
+ - Better workspace reads the session list as `{ ids, byId }` and keeps the Host order;
49
+ it no longer re-sorts on a timestamp `SessionSummary` does not carry.
50
+
51
+ ## [0.9.0] - 2026-10-28
52
+
53
+ ### Added
54
+
55
+ - Better workspace (`betterWorkspace`): our own sidebar browser registered into
56
+ `sidebar.workspaces` at `priority: -1`, with the shipped session menu rendered
57
+ inside it and a right-click menu on every workspace row (open, copy path, remove).
58
+ See `docs/better-workspace.md` for why this is a takeover rather than a menu item.
59
+
60
+
61
+ ## [0.8.2] - 2026-10-28
62
+
63
+ ### Fixed
64
+
65
+ - The camera floated over the panel with metrics of its own, so it read as a
66
+ foreign control. It now mounts into the docking kit's chrome seat beside the
67
+ Shell's own presentation and collapse buttons, wearing that cluster's 28px box,
68
+ 15px glyph, hover state and tooltip, and it follows the strip through docked,
69
+ split and fullscreen layouts.
70
+
3
71
  ## [0.8.1] - 2026-10-28
4
72
 
5
73
  ### Changed
6
74
 
7
- - The camera moved out of the composer row and into the open right panel's own
8
- top-right corner, where the panel is what you are looking at. The composer
9
- dock entry stays, invisibly, to publish the session context the button needs.
75
+ - The camera moved out of the composer row and next to the right panel's own
76
+ controls. The composer dock entry stays, invisibly, to publish the session
77
+ context the button needs.
10
78
 
11
79
  ## [0.8.0] - 2026-10-28
12
80
 
package/README.md CHANGED
@@ -7,7 +7,7 @@ installed into and patches nothing. It is one package, one Plugins row, and one
7
7
  switch per feature, so a refinement that turns out to be wrong for a machine can
8
8
  be turned off without uninstalling anything.
9
9
 
10
- Status: **six features shipped.** The version is the package's, and the card
10
+ Status: **seven features shipped.** The version is the package's, and the card
11
11
  header already shows it.
12
12
 
13
13
  ## What it is
@@ -30,6 +30,7 @@ Configure page automatically.
30
30
 
31
31
  | Switch | Default | What it does |
32
32
  | --- | --- | --- |
33
+ | Better workspace | On | Replaces the sidebar workspace list with our own tree and a right-click menu on each workspace. |
33
34
  | **Estimated session cost** (`sessionCost`) | On | A `≈$0.39` pill in the composer dock that opens a breakdown card — cache hit, uncached input, cached input, output, requests, and the estimate — priced from the session's recorded usage at the published DeepSeek rates. |
34
35
  | **Account balance** (`accountBalance`) | On | The account's remaining wallet (`$17.33`) in the sidebar foot, above the account row. Renders nothing when the Host exposes no wallet, so it never shows a number nobody gave it. |
35
36
  | **Open workspace folder** (`openWorkspaceFolder`) | On | An **Open workspace folder** row in a Session's "…" menu that opens the Session's workspace in Explorer/Finder. Renders nothing when the Session has no workspace or the Host cannot open a path. See [Open a workspace folder](docs/workspace-folder.md) for why the request had to land here instead of on the workspace row itself. |
package/client/face.js CHANGED
@@ -1572,7 +1572,6 @@ function shotName() {
1572
1572
  + '-' + pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds()) + '.png'
1573
1573
  }
1574
1574
 
1575
- const SHOT_GLYPH_STYLE = { width: '14px', height: '14px', flex: 'none', display: 'block' }
1576
1575
 
1577
1576
  /**
1578
1577
  * Camera glyph, drawn here so the face depends on no icon package.
@@ -1581,51 +1580,94 @@ const SHOT_GLYPH_STYLE = { width: '14px', height: '14px', flex: 'none', display:
1581
1580
  function CameraGlyph() {
1582
1581
  return React.createElement('svg', {
1583
1582
  viewBox: '0 0 16 16',
1584
- width: 14,
1585
- height: 14,
1583
+ width: 15,
1584
+ height: 15,
1585
+ fill: 'none',
1586
+ stroke: 'currentColor',
1587
+ strokeWidth: 1,
1588
+ strokeLinecap: 'round',
1589
+ strokeLinejoin: 'round',
1590
+ 'aria-hidden': 'true',
1591
+ },
1592
+ React.createElement('path', { d: 'M2.6 5.4h1.9l1-1.5h5l1 1.5h1.9a1 1 0 0 1 1 1v5.7a1 1 0 0 1-1 1H2.6a1 1 0 0 1-1-1V6.4a1 1 0 0 1 1-1Z' }),
1593
+ React.createElement('circle', { cx: 8, cy: 9.3, r: 2.5 }))
1594
+ }
1595
+
1596
+ /** Confirmation tick, drawn at the strip's own glyph size. */
1597
+ function CheckGlyph() {
1598
+ return React.createElement('svg', {
1599
+ viewBox: '0 0 16 16',
1600
+ width: 15,
1601
+ height: 15,
1602
+ fill: 'none',
1603
+ stroke: 'currentColor',
1604
+ strokeWidth: 1.4,
1605
+ strokeLinecap: 'round',
1606
+ strokeLinejoin: 'round',
1607
+ 'aria-hidden': 'true',
1608
+ },
1609
+ React.createElement('path', { d: 'M3.4 8.6l3 3 6.2-6.6' }))
1610
+ }
1611
+
1612
+ /** Refusal mark, drawn at the strip's own glyph size. */
1613
+ function BangGlyph() {
1614
+ return React.createElement('svg', {
1615
+ viewBox: '0 0 16 16',
1616
+ width: 15,
1617
+ height: 15,
1586
1618
  fill: 'none',
1587
1619
  stroke: 'currentColor',
1588
- strokeWidth: 1.2,
1620
+ strokeWidth: 1.4,
1589
1621
  strokeLinecap: 'round',
1590
1622
  strokeLinejoin: 'round',
1591
1623
  'aria-hidden': 'true',
1592
- style: SHOT_GLYPH_STYLE,
1593
1624
  },
1594
- React.createElement('path', { d: 'M2 5.5h2.2l1-1.5h5.6l1 1.5H14v7H2z' }),
1595
- React.createElement('circle', { cx: 8, cy: 9, r: 2.2 }))
1625
+ React.createElement('path', { d: 'M8 3.6v5.2' }),
1626
+ React.createElement('path', { d: 'M8 12v.4' }))
1596
1627
  }
1597
1628
 
1598
1629
  /**
1599
- * Where the camera button is drawn: the panel's own top-right corner, just
1600
- * inside the frame and left of the Shell's expand/close controls.
1630
+ * The Shell has no header slot for the panel, so the camera rides the docking
1631
+ * kit's chrome seat: the element at the end of the top-right pane's tab strip
1632
+ * that already holds the presentation and collapse controls. Portalling into
1633
+ * that element, instead of floating over the panel at fixed coordinates, is what
1634
+ * makes the button share the cluster's gap, its vertical centring and whatever
1635
+ * geometry the strip has docked, fullscreened or split.
1601
1636
  */
1602
- const SHOT_ANCHOR_EDGE = 8
1603
- const SHOT_ANCHOR_BUTTON = 28
1604
- const SHOT_ANCHOR_CONTROLS = 96
1637
+ const SHOT_CHROME_SELECTOR = '[data-dockkit-strip-chrome]'
1605
1638
 
1606
1639
  /**
1607
- * Where the open right panel's header ends, in viewport coordinates.
1608
- * @returns the corner, or undefined when no panel is open.
1640
+ * Mirrors the Shell's own strip control: a 28px box with 6px padding around a
1641
+ * 15px glyph, secondary label colour, transparent until hovered.
1609
1642
  */
1610
- function shotAnchor() {
1611
- const panel = document.querySelector('[data-sidebar-right-session][data-sidebar-right-open]')
1612
- if (panel === null) return undefined
1613
- const rect = panel.getBoundingClientRect()
1614
- if (rect.width < 120 || rect.height < 120) return undefined
1615
- return {
1616
- top: rect.top + SHOT_ANCHOR_EDGE,
1617
- left: Math.max(rect.left + SHOT_ANCHOR_EDGE, rect.right - SHOT_ANCHOR_CONTROLS - SHOT_ANCHOR_BUTTON),
1618
- }
1643
+ const SHOT_BUTTON_STYLE = {
1644
+ display: 'inline-flex',
1645
+ flex: 'none',
1646
+ alignItems: 'center',
1647
+ justifyContent: 'center',
1648
+ width: '28px',
1649
+ height: '28px',
1650
+ padding: '6px',
1651
+ color: 'var(--dsw-alias-label-secondary, #61666b)',
1652
+ lineHeight: 1,
1653
+ background: 'transparent',
1654
+ border: 'none',
1655
+ borderRadius: 'var(--dsw-radius-sm, 6px)',
1656
+ cursor: 'pointer',
1657
+ // Ahead of the Shell's own two controls: the last thing you would reach for,
1658
+ // and never between them and the panel edge.
1659
+ order: -1,
1619
1660
  }
1620
1661
 
1621
1662
  /**
1622
- * The composer context the overlay button attaches through.
1663
+ * The composer context the chrome button attaches through.
1623
1664
  *
1624
- * The camera button lives in a root overlay so it can sit in the panel header,
1625
- * but attaching an image needs the *session-scoped* composer props (`sessionId`
1626
- * and `inputActions`), which only a session slot receives. The composer dock is
1665
+ * The camera renders inside the panel strip, which is root chrome, but attaching
1666
+ * an image needs the *session-scoped* composer props (`sessionId` and
1667
+ * `inputActions`), which only a session slot receives. The composer dock is
1627
1668
  * therefore kept as an invisible publisher and this store carries its context up
1628
- * to the overlay; with no open Session the store is empty and the button hides.
1669
+ * to the chrome button; with no open Session the store is empty and the button
1670
+ * does not render.
1629
1671
  */
1630
1672
  const shotStore = (() => {
1631
1673
  const listeners = new Set()
@@ -1643,24 +1685,8 @@ const shotStore = (() => {
1643
1685
  }
1644
1686
  })()
1645
1687
 
1646
- const SHOT_BUTTON_STYLE = {
1647
- position: 'fixed',
1648
- display: 'inline-flex',
1649
- alignItems: 'center',
1650
- justifyContent: 'center',
1651
- width: SHOT_ANCHOR_BUTTON + 'px',
1652
- height: SHOT_ANCHOR_BUTTON + 'px',
1653
- padding: '0',
1654
- border: 'none',
1655
- borderRadius: 'var(--dsw-radius-sm, 6px)',
1656
- background: 'transparent',
1657
- color: 'var(--dsw-alias-label-tertiary, #61666b)',
1658
- cursor: 'pointer',
1659
- zIndex: 40,
1660
- }
1661
-
1662
1688
  /**
1663
- * Invisible composer-dock entry that publishes the session context the overlay
1689
+ * Invisible composer-dock entry that publishes the session context the chrome
1664
1690
  * button needs, and nothing else.
1665
1691
  * @param props - session slot props (`sessionId`, `inputActions`) plus the
1666
1692
  * late `conversation` lookup.
@@ -1679,29 +1705,35 @@ function PanelShotContext(props) {
1679
1705
  }
1680
1706
 
1681
1707
  /**
1682
- * Camera button pinned to the right panel's top-right corner: photographs the
1683
- * open tab, copies it, and attaches it to the message being written.
1684
- * @param props - slot props plus the late `conversation` lookup.
1685
- * @returns the button, or null while there is nothing to shoot or nowhere to
1686
- * attach.
1708
+ * Camera button in the right panel's tab strip: photographs the open tab, copies
1709
+ * it, and attaches it to the message being written.
1710
+ * @param props - overlay slot props plus the late `conversation` lookup.
1711
+ * @returns the portalled button, or null while there is no panel or no Session.
1687
1712
  */
1688
1713
  function PanelShotButton(props) {
1689
1714
  const on = useSwitch(props.configForms, 'panelScreenshot', true)
1690
1715
  const context = React.useSyncExternalStore(shotStore.subscribe, shotStore.get)
1691
- const [anchor, setAnchor] = React.useState(() => shotAnchor())
1716
+ const [host, setHost] = React.useState(undefined)
1692
1717
  const [phase, setPhase] = React.useState('idle')
1693
1718
  const [note, setNote] = React.useState('')
1694
1719
  React.useEffect(() => {
1695
- const measure = () => setAnchor(shotAnchor())
1696
- measure()
1697
- // The panel opens, closes, splits and resizes without any event this
1698
- // component would otherwise hear, so watch the attribute and the geometry.
1699
- const observer = new MutationObserver(measure)
1700
- observer.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['data-sidebar-right-open', 'data-dockkit-pane-active'] })
1701
- window.addEventListener('resize', measure)
1720
+ // The strip is created, destroyed and re-created as panes open, split and
1721
+ // close, so follow the document rather than measuring it once. Frames are
1722
+ // coalesced because React commits fire many mutations in a row.
1723
+ let frame = 0
1724
+ const find = () => {
1725
+ frame = 0
1726
+ setHost(document.querySelector(SHOT_CHROME_SELECTOR) ?? undefined)
1727
+ }
1728
+ const schedule = () => {
1729
+ if (frame === 0) frame = window.requestAnimationFrame(find)
1730
+ }
1731
+ find()
1732
+ const observer = new MutationObserver(schedule)
1733
+ observer.observe(document.body, { childList: true, subtree: true })
1702
1734
  return () => {
1735
+ if (frame !== 0) window.cancelAnimationFrame(frame)
1703
1736
  observer.disconnect()
1704
- window.removeEventListener('resize', measure)
1705
1737
  }
1706
1738
  }, [])
1707
1739
  React.useEffect(() => {
@@ -1712,7 +1744,7 @@ function PanelShotButton(props) {
1712
1744
  }, 4000)
1713
1745
  return () => clearTimeout(timer)
1714
1746
  }, [note])
1715
- if (!on || context === undefined || anchor === undefined) return null
1747
+ if (!on || context === undefined || host === undefined) return null
1716
1748
  const take = async () => {
1717
1749
  if (phase === 'busy') return
1718
1750
  const node = panelTarget()
@@ -1748,63 +1780,46 @@ function PanelShotButton(props) {
1748
1780
  throw new Error('the composer refused the image')
1749
1781
  }
1750
1782
  setPhase('done')
1751
- setNote(copied ? 'right panel copied and attached' : 'right panel attached, clipboard refused')
1783
+ setNote(copied ? 'Right panel copied and attached' : 'Right panel attached, clipboard refused')
1752
1784
  } catch (error) {
1753
1785
  setPhase('failed')
1754
1786
  setNote(error instanceof Error ? error.message : String(error))
1755
1787
  console.warn('[aryacode-polish] panel screenshot failed:', error)
1756
1788
  }
1757
1789
  }
1758
- const label = phase === 'done' ? '✓' : phase === 'failed' ? '!' : '●'
1759
- return React.createElement(React.Fragment, null,
1760
- React.createElement('span', {
1761
- 'data-polish-shot-anchor': 'panel',
1762
- style: { position: 'fixed', top: anchor.top + 'px', left: anchor.left + 'px', width: '0', height: '0' },
1763
- }),
1764
- React.createElement('button', {
1765
- type: 'button',
1766
- 'data-polish-shot': phase,
1767
- title: note === '' ? 'Screenshot this panel into the chat (also copies it)' : note,
1768
- 'aria-label': 'Screenshot this panel',
1769
- disabled: phase === 'busy',
1770
- onClick: () => { void take() },
1771
- onMouseEnter: (event) => { event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
1772
- onMouseLeave: (event) => { event.currentTarget.style.background = 'transparent' },
1773
- style: {
1774
- ...SHOT_BUTTON_STYLE,
1775
- top: anchor.top + 'px',
1776
- left: anchor.left + 'px',
1777
- color: phase === 'failed'
1778
- ? 'var(--dsw-alias-label-error, #b42318)'
1779
- : 'var(--dsw-alias-label-tertiary, #61666b)',
1780
- opacity: phase === 'busy' ? 0.5 : 1,
1781
- cursor: phase === 'busy' ? 'progress' : 'pointer',
1782
- fontSize: '11px',
1783
- },
1790
+ const tip = note !== ''
1791
+ ? note
1792
+ : phase === 'busy' ? 'Capturing the right panel…' : 'Screenshot this panel into the chat (also copies it)'
1793
+ const glyph = phase === 'done'
1794
+ ? React.createElement(CheckGlyph, null)
1795
+ : phase === 'failed'
1796
+ ? React.createElement(BangGlyph, null)
1797
+ : React.createElement(CameraGlyph, null)
1798
+ const button = React.createElement('button', {
1799
+ type: 'button',
1800
+ 'data-polish-shot': phase,
1801
+ 'aria-label': 'Screenshot this panel',
1802
+ disabled: phase === 'busy',
1803
+ onClick: () => { void take() },
1804
+ onMouseEnter: (event) => { event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
1805
+ onMouseLeave: (event) => { event.currentTarget.style.background = 'transparent' },
1806
+ style: {
1807
+ ...SHOT_BUTTON_STYLE,
1808
+ color: phase === 'failed'
1809
+ ? 'var(--dsw-alias-label-error, #b42318)'
1810
+ : phase === 'done'
1811
+ ? 'var(--dsw-alias-label-primary, #111418)'
1812
+ : SHOT_BUTTON_STYLE.color,
1813
+ cursor: phase === 'busy' ? 'progress' : 'pointer',
1784
1814
  },
1785
- phase === 'done' || phase === 'failed'
1786
- ? React.createElement('span', { 'aria-hidden': 'true' }, label)
1787
- : React.createElement(CameraGlyph, null)),
1788
- note === ''
1789
- ? null
1790
- : React.createElement('span', {
1791
- 'data-polish-shot-note': phase,
1792
- style: {
1793
- position: 'fixed',
1794
- top: anchor.top + SHOT_ANCHOR_BUTTON + 6 + 'px',
1795
- left: Math.max(12, anchor.left - 140) + 'px',
1796
- maxWidth: '260px',
1797
- padding: '3px 8px',
1798
- borderRadius: 'var(--dsw-radius-sm, 6px)',
1799
- background: 'Canvas',
1800
- boxShadow: 'var(--dsw-elevation-prominent, 0 3px 8px rgba(0,0,0,0.12))',
1801
- color: phase === 'failed' ? 'var(--dsw-alias-label-error, #b42318)' : 'var(--dsw-alias-label-secondary, #61666b)',
1802
- fontSize: '12px',
1803
- lineHeight: '18px',
1804
- whiteSpace: 'nowrap',
1805
- zIndex: 41,
1806
- },
1807
- }, note))
1815
+ }, glyph)
1816
+ const Tooltip = primitives === undefined ? undefined : primitives.Tooltip
1817
+ return ReactDOM.createPortal(
1818
+ Tooltip === undefined
1819
+ ? button
1820
+ : React.createElement(Tooltip, { label: tip, side: 'bottom', delayMs: 500 }, button),
1821
+ host,
1822
+ )
1808
1823
  }
1809
1824
 
1810
1825
  /**
@@ -1834,6 +1849,9 @@ return {
1834
1849
  return undefined
1835
1850
  }
1836
1851
  }
1852
+ // The Better workspace seat shadows the shipped browser at a different priority,
1853
+ // so it must be present only while its switch is on.
1854
+ installWorkspaceSeat(ctx, configForms, { sourceFlow: WorkspaceSourceFlow })
1837
1855
  ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
1838
1856
  name: 'plugins.row.config',
1839
1857
  key: ROW_KEY,
@@ -36,6 +36,12 @@
36
36
  "label": "Right-panel screenshot",
37
37
  "description": "Copy the open right panel into the chat as an image, and onto the clipboard.",
38
38
  "defaultOn": true
39
+ },
40
+ {
41
+ "key": "betterWorkspace",
42
+ "label": "Better workspace",
43
+ "description": "Replace the sidebar workspace list with our own tree: workspaces with their sessions, a right-click menu on a workspace (open, copy path, remove), and the shipped session menu inside it.",
44
+ "defaultOn": true
39
45
  }
40
46
  ]
41
47
  }