@aryacode/dsh-aryacode-polish 0.3.7 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,94 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.4] - 2026-10-27
4
+
5
+ ### Fixed
6
+
7
+ - **The row reported "this Host cannot open a folder" on a Host that can.** A Remote
8
+ method answers an envelope — `{ ok, value }` — not the bare return value, so
9
+ `canOpenWorkspacePath()` never looked like `true`. The row now reads
10
+ `ok === true && value === true`, tolerates a plain boolean, and checks the
11
+ `ok` flag of the open call itself instead of trusting that no throw means opened.
12
+
13
+ ## [0.4.3] - 2026-10-27
14
+
15
+ ### Fixed
16
+
17
+ - **The row still could not open anything.** 0.4.2 declared `remote.session` but the
18
+ guard checks each namespace on its own: reading `ctx.remote.session` needs both
19
+ `remote` and `remote.session` on the inject list, so the call failed one level
20
+ earlier. The registration now declares `['workspaces', 'remote', 'remote.session']`.
21
+
22
+ ## [0.4.2] - 2026-10-27
23
+
24
+ ### Fixed
25
+
26
+ - **The row did nothing when clicked.** The Shell refuses `ctx.get('remote.session')`
27
+ — a Remote namespace may only be read through a declared injection — so the open
28
+ call failed before it reached the Host. The registration now declares
29
+ `ctx.inject(['workspaces', 'remote.session'], …)` and calls the namespace the
30
+ injected context hands it. Found by the row reporting its own failure (0.4.1).
31
+
32
+ ## [0.4.1] - 2026-10-27
33
+
34
+ ### Changed
35
+
36
+ - The **Open workspace folder** row reports a failed open instead of doing nothing.
37
+ A menu row that silently ignores the click is impossible to diagnose in the
38
+ field; the bundle now writes one console.warn naming the failing step (no
39
+ Session Remote, a Host that cannot open a path, or the Remote error itself).
40
+
41
+ ## [0.4.0] - 2026-10-27
42
+
43
+ ### Added
44
+
45
+ - **Open workspace folder.** A Session's "…" menu gains an **Open workspace folder**
46
+ row that opens the Session's workspace directory in Explorer/Finder through the
47
+ Session Remote (`openWorkspacePath`), gated by `canOpenWorkspacePath` and resolved
48
+ from the Workspace snapshot. New switch `openWorkspaceFolder`, on by default, and
49
+ the row hides itself when the Session has no workspace or the Host cannot open a
50
+ path.
51
+ - `docs/workspace-folder.md` records why the requested surface — right-clicking the
52
+ workspace row itself — is not reachable from a plugin: that row has no context
53
+ menu, the workspace-scoped menu list slot does not exist, and native menus and
54
+ desktop IPC are closed to plugins. The bundle ships on the documented Session-row
55
+ slot rather than hijacking private markup, and the note says where the feature
56
+ should move if a workspace-scoped slot ever lands.
57
+
58
+ ## [0.3.9] - 2026-10-27
59
+
60
+ ### Changed
61
+
62
+ - **The cost pill now looks like it belongs in the composer.** It was a foreign
63
+ widget next to the harness pills: two lines of text ("≈$0.538 partially
64
+ priced") in the row's flow. It now wears the same skin as the statistics pills
65
+ — the same 13 px-minus-one scale, 20 px line height, `1px 8px` padding, tertiary
66
+ label colour, tabular digits, a 14 px glyph, and `white-space: nowrap`, so it
67
+ reads as one line at the same size as its neighbours.
68
+ - The "partially priced" caveat moved out of the pill and into the card, where it
69
+ can be read. The pill keeps a single `*` marker for the same fact.
70
+ - The breakdown card is now the harness's own stat-dialog: same panel width
71
+ clamp, 16 px padding, `--dsw-radius-lg` corners, menu backdrop, prominent
72
+ elevation, 12/18 type, a two-column definition list with tabular figures, and
73
+ a portaled `position: fixed` panel measured from the pill so it cannot be
74
+ clipped by the composer scroller. It closes on outside click and on Escape.
75
+
76
+ ## [0.3.8] - 2026-10-27
77
+
78
+ ### Fixed
79
+
80
+ - **The balance line never showed a number.** It waited for the host account
81
+ bundle's own access grant before reading, on the theory that the grant proves
82
+ the call is allowed. That grant belongs to the bundle that owns the account
83
+ stream, so for a plugin it can simply never arrive — the read hit its timeout,
84
+ resolved to nothing, and the sidebar looked empty with no error anywhere. The
85
+ line now reads the account Remote directly inside
86
+ `ctx.inject(['remote', 'remote.account'], …)`, and the Host still decides: an
87
+ unauthorised call comes back refused and the line stays hidden.
88
+ - The read retries on its own (1.5 s, 5 s, 15 s, 30 s, and again whenever the
89
+ window regains focus), so a Shell that is still signing in at first paint no
90
+ longer leaves a permanently blank line.
91
+
3
92
  ## [0.3.7] - 2026-10-27
4
93
 
5
94
  ### Fixed
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: **two features shipped.** The version is the package's, and the card
10
+ Status: **three features shipped.** The version is the package's, and the card
11
11
  header already shows it.
12
12
 
13
13
  ## What it is
@@ -32,6 +32,7 @@ Configure page automatically.
32
32
  | --- | --- | --- |
33
33
  | **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
34
  | **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
+ | **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. |
35
36
 
36
37
  ### How the cost is computed
37
38
 
@@ -130,7 +131,7 @@ client/switches.json the browser half of the seam — one entry per switch
130
131
  scripts/build-client.mjs wraps client/face.js into lib/client.js
131
132
  lib/client.js generated: the bundle the Shell loads
132
133
  locale/ card title and description (en, zh)
133
- docs/ parked investigations
134
+ docs/ feature notes and parked investigations
134
135
  test/ node --test specs
135
136
  ```
136
137
 
package/client/account.js CHANGED
@@ -1,12 +1,19 @@
1
1
  /**
2
2
  * Reading the account balance from a plugin face.
3
3
  *
4
- * The account Remote methods are gated, so the read runs inside a
5
- * `remoteAccess.whenAccess('account/watch', …)` grant and the grant is released
6
- * as soon as the read settles. Every failure — no service in this Shell, a
7
- * refused grant, a failed call — resolves to `undefined` instead of throwing: a
8
- * sidebar line is never worth breaking the shell over, and a line that stays
9
- * empty is honest about a number it never received.
4
+ * The account Remote methods are the same ones the shipped account row uses. An
5
+ * earlier version waited for the host account bundle's own access grant before
6
+ * reading, on the theory that the grant proves the call is allowed. It does not:
7
+ * that grant belongs to the bundle that owns the account stream, and a plugin
8
+ * that waits for it can wait forever — the sidebar line then stays empty with no
9
+ * error anywhere, which is exactly what a user reports as "I don't see the
10
+ * balance".
11
+ *
12
+ * So this reads directly. The Host still decides: an unauthorised call comes back
13
+ * `{ ok: false }` (or throws) and this resolves to `undefined`. Every failure — no
14
+ * account namespace in this Shell, a refused call, a failed call — resolves to
15
+ * `undefined` instead of throwing, because a sidebar line is never worth breaking
16
+ * the shell over.
10
17
  *
11
18
  * This module is inlined into `lib/client.js` by `scripts/build-client.mjs` and
12
19
  * imported directly by `test/account.spec.mjs`, so the tested source is the one
@@ -35,62 +42,48 @@ export function clientMetadata(version) {
35
42
  }
36
43
 
37
44
  /**
38
- * Build the one-shot balance reader.
45
+ * The account Remote namespace, or undefined when this Shell has none.
39
46
  *
40
- * @param ctx - the browser plugin context; `ctx.get` resolves an optional service.
41
- * @param version - the version this build stamps into the account metadata.
42
- * @returns a reader that resolves to the balance view, or undefined when unavailable.
47
+ * @param ctx - the browser plugin context; the balance surface is registered
48
+ * inside `ctx.inject(['remote', 'remote.account'], …)` so this normally resolves.
49
+ * @returns the namespace, or undefined.
43
50
  */
44
- export function makeBalanceReader(ctx, version, { timeoutMs = 10000 } = {}) {
45
- const service = name => {
46
- try {
47
- return ctx.get(name)
48
- } catch {
49
- return undefined
50
- }
51
+ export function accountService(ctx) {
52
+ try {
53
+ const remote = ctx.remote !== undefined ? ctx.remote : ctx.get('remote')
54
+ const account = remote !== undefined && remote !== null ? remote.account : undefined
55
+ return account !== undefined && account !== null && typeof account.getBalance === 'function' ? account : undefined
56
+ } catch {
57
+ return undefined
51
58
  }
52
- return async () => {
53
- const access = service('remoteAccess')
54
- const remote = service('remote')
55
- if (access === undefined || remote === undefined) return undefined
56
- const metadata = clientMetadata(version)
57
- return await new Promise(resolve => {
58
- let settled = false
59
- let stop
60
- const timer = setTimeout(() => finish(undefined), timeoutMs)
61
- const finish = value => {
62
- if (settled) return
63
- settled = true
64
- clearTimeout(timer)
65
- resolve(value)
66
- if (typeof stop === 'function') {
67
- try {
68
- stop()
69
- } catch {
70
- /* the grant may already have been released */
71
- }
72
- }
73
- }
74
- // The read starts before the yield so an effect runner that stops at the
75
- // first yielded disposer still performs the call.
76
- stop = access.whenAccess('account/watch', function* () {
77
- void (async () => {
78
- try {
79
- const result = await remote.account.getBalance(metadata)
80
- const value = result !== undefined && result.ok === true ? result.value : undefined
81
- finish(value !== undefined && value.status === 'ready' ? value : undefined)
82
- } catch {
83
- finish(undefined)
84
- }
85
- })()
86
- yield () => {
87
- if (!settled) {
88
- settled = true
89
- clearTimeout(timer)
90
- resolve(undefined)
91
- }
92
- }
93
- })
94
- })
59
+ }
60
+
61
+ /**
62
+ * Read the balance once.
63
+ *
64
+ * @param ctx - the browser plugin context.
65
+ * @param version - the version this build stamps into the account metadata.
66
+ * @param options - `timeoutMs` bounds a call that never settles.
67
+ * @returns the ready balance view, or undefined when there is no number to show.
68
+ */
69
+ export async function readBalance(ctx, version, { timeoutMs = 15000 } = {}) {
70
+ const account = accountService(ctx)
71
+ if (account === undefined) return undefined
72
+ const call = (async () => {
73
+ const result = await account.getBalance(clientMetadata(version))
74
+ if (result === undefined || result === null || result.ok !== true) return undefined
75
+ const value = result.value
76
+ return value !== undefined && value !== null && value.status === 'ready' ? value : undefined
77
+ })().catch(() => undefined)
78
+ let timer
79
+ const timeout = new Promise(resolve => {
80
+ timer = setTimeout(() => resolve(undefined), timeoutMs)
81
+ // A pending balance read must not keep a Node process alive during tests.
82
+ if (typeof timer === 'object' && timer !== null && typeof timer.unref === 'function') timer.unref()
83
+ })
84
+ try {
85
+ return await Promise.race([call, timeout])
86
+ } finally {
87
+ clearTimeout(timer)
95
88
  }
96
89
  }