@aryacode/dsh-aryacode-polish 0.3.9 → 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 +55 -0
- package/README.md +3 -2
- package/client/face.js +107 -0
- package/client/switches.json +6 -0
- package/docs/workspace-folder.md +67 -0
- package/entries/registry.js +6 -0
- package/lib/client.js +114 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
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
|
+
|
|
3
58
|
## [0.3.9] - 2026-10-27
|
|
4
59
|
|
|
5
60
|
### Changed
|
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: **
|
|
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/face.js
CHANGED
|
@@ -17,6 +17,19 @@
|
|
|
17
17
|
const React = require('react')
|
|
18
18
|
const ReactDOM = require('react-dom')
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The Shell's own UI primitives. Guarded because the menu row degrades to a plain
|
|
22
|
+
* button when a Shell answers no primitives, and a bundle that throws on load
|
|
23
|
+
* would take every other surface down with it.
|
|
24
|
+
*/
|
|
25
|
+
const primitives = (() => {
|
|
26
|
+
try {
|
|
27
|
+
return require('@deepseek-ai/dsh-client-ui-primitives')
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
})()
|
|
32
|
+
|
|
20
33
|
/** Shared spacing for the page's prose. */
|
|
21
34
|
const SPACING = { margin: '0 0 10px', opacity: 0.85, lineHeight: '1.5' }
|
|
22
35
|
|
|
@@ -535,6 +548,74 @@ function BalanceLine(props) {
|
|
|
535
548
|
}, props.wide === false ? money : 'Balance ' + money)
|
|
536
549
|
}
|
|
537
550
|
|
|
551
|
+
/**
|
|
552
|
+
* The workspace a Session belongs to.
|
|
553
|
+
*
|
|
554
|
+
* Occupants of `sidebar.workspaces.session.menu.item` address their data by
|
|
555
|
+
* Session id and must not bind or retain the Session, so the workspace is read
|
|
556
|
+
* from the Workspace snapshot at render time and never memoized.
|
|
557
|
+
*
|
|
558
|
+
* @param workspaces - the Shell's Workspace service, when it is loaded.
|
|
559
|
+
* @param sessionId - the row's Session.
|
|
560
|
+
* @returns the owning workspace, or undefined when it cannot be resolved.
|
|
561
|
+
*/
|
|
562
|
+
function workspaceOf(workspaces, sessionId) {
|
|
563
|
+
if (workspaces === undefined || workspaces === null || sessionId === undefined) return undefined
|
|
564
|
+
let items
|
|
565
|
+
try {
|
|
566
|
+
items = workspaces.list.getSnapshot().items
|
|
567
|
+
} catch {
|
|
568
|
+
return undefined
|
|
569
|
+
}
|
|
570
|
+
if (!Array.isArray(items)) return undefined
|
|
571
|
+
return items.find(item => Array.isArray(item.sessionIds)
|
|
572
|
+
&& item.sessionIds.indexOf(sessionId) !== -1
|
|
573
|
+
&& typeof item.path === 'string'
|
|
574
|
+
&& item.path !== '')
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* "Open workspace folder": one row in a Session's "…" menu.
|
|
579
|
+
*
|
|
580
|
+
* It renders nothing when the switch is off or the Session has no workspace the
|
|
581
|
+
* Host could open, because a menu row that cannot act is worse than no row.
|
|
582
|
+
*
|
|
583
|
+
* @param props - the row's Session id, our lazy `configForms`/`workspaces`, the
|
|
584
|
+
* opener, and the slot's injected `useMenuOpenState` hook.
|
|
585
|
+
* @returns the menu row, or null.
|
|
586
|
+
*/
|
|
587
|
+
function OpenFolderMenuItem(props) {
|
|
588
|
+
const enabled = useSwitch(props.configForms, 'openWorkspaceFolder', true)
|
|
589
|
+
// Hooks run unconditionally; the row simply renders nothing when it cannot act.
|
|
590
|
+
const menu = typeof props.useMenuOpenState === 'function' ? props.useMenuOpenState() : undefined
|
|
591
|
+
const closeMenu = Array.isArray(menu) && typeof menu[1] === 'function' ? menu[1] : undefined
|
|
592
|
+
const workspace = enabled ? workspaceOf(props.workspaces, props.sessionId) : undefined
|
|
593
|
+
if (!enabled || workspace === undefined) return null
|
|
594
|
+
const label = 'Open workspace folder'
|
|
595
|
+
const onSelect = () => {
|
|
596
|
+
if (closeMenu !== undefined) closeMenu(false)
|
|
597
|
+
const open = props.openFolder
|
|
598
|
+
if (typeof open !== 'function') return
|
|
599
|
+
void Promise.resolve()
|
|
600
|
+
.then(() => open(workspace))
|
|
601
|
+
.catch(error => {
|
|
602
|
+
// A menu row that silently does nothing is unfixable in the field, so the
|
|
603
|
+
// failure is reported instead of dropped. This is the only diagnostic the
|
|
604
|
+
// bundle writes.
|
|
605
|
+
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
606
|
+
console.warn('[aryacode-polish] open workspace folder failed:', error && error.message ? error.message : error)
|
|
607
|
+
}
|
|
608
|
+
})
|
|
609
|
+
}
|
|
610
|
+
if (primitives !== undefined && typeof primitives.MenuItemButton === 'function') {
|
|
611
|
+
const icon = typeof primitives.IconFolderOpenRegular === 'function'
|
|
612
|
+
? React.createElement(primitives.IconFolderOpenRegular)
|
|
613
|
+
: undefined
|
|
614
|
+
return React.createElement(primitives.MenuItemButton, { icon, onSelect }, label)
|
|
615
|
+
}
|
|
616
|
+
return React.createElement('button', { type: 'button', role: 'menuitem', onClick: onSelect }, label)
|
|
617
|
+
}
|
|
618
|
+
|
|
538
619
|
return {
|
|
539
620
|
inject: ['slots'],
|
|
540
621
|
/**
|
|
@@ -571,5 +652,31 @@ return {
|
|
|
571
652
|
inject: () => ({ configForms, loadBalance }),
|
|
572
653
|
}, BalanceLine))
|
|
573
654
|
})
|
|
655
|
+
// The Workspace service and the Session Remote are both required for the menu
|
|
656
|
+
// row: it reads the folder from the first and opens it through the second.
|
|
657
|
+
// They are declared on the inject list because a Remote namespace may not be
|
|
658
|
+
// read through `ctx.get` — the Shell's guard refuses it.
|
|
659
|
+
ctx.inject(['workspaces', 'remote', 'remote.session'], (workspaceCtx) => {
|
|
660
|
+
const openFolder = async (workspace) => {
|
|
661
|
+
const remote = workspaceCtx.remote.session
|
|
662
|
+
// A Remote method answers an envelope (`{ ok, value }`), not the bare
|
|
663
|
+
// return value; a Shell that answered a plain boolean is tolerated too.
|
|
664
|
+
const answer = await remote.canOpenWorkspacePath()
|
|
665
|
+
const available = answer !== null && typeof answer === 'object'
|
|
666
|
+
? answer.ok === true && answer.value === true
|
|
667
|
+
: answer === true
|
|
668
|
+
if (!available) throw new Error('this Host cannot open a folder')
|
|
669
|
+
const result = await remote.openWorkspacePath({ path: workspace.path })
|
|
670
|
+
if (result !== null && typeof result === 'object' && 'ok' in result && result.ok !== true) {
|
|
671
|
+
throw new Error('the Host refused to open the folder')
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
workspaceCtx.slots.inject('sidebar.workspaces.session.menu.item', () => workspaceCtx.slots.register({
|
|
675
|
+
name: 'sidebar.workspaces.session.menu.item',
|
|
676
|
+
id: 'polish-open-folder',
|
|
677
|
+
order: 460,
|
|
678
|
+
inject: () => ({ configForms, openFolder, workspaces: workspaceCtx.get('workspaces') }),
|
|
679
|
+
}, OpenFolderMenuItem))
|
|
680
|
+
})
|
|
574
681
|
},
|
|
575
682
|
}
|
package/client/switches.json
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
"label": "Account balance",
|
|
13
13
|
"description": "Show the remaining account balance in the sidebar foot, above the account row.",
|
|
14
14
|
"defaultOn": true
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"key": "openWorkspaceFolder",
|
|
18
|
+
"label": "Open workspace folder",
|
|
19
|
+
"description": "Add an \u201cOpen workspace folder\u201d row to a Session\u2019s menu that opens its workspace in the file browser.",
|
|
20
|
+
"defaultOn": true
|
|
15
21
|
}
|
|
16
22
|
]
|
|
17
23
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Open a workspace folder in the file browser
|
|
2
|
+
|
|
3
|
+
## What ships
|
|
4
|
+
|
|
5
|
+
A row in a Session's "…" menu: **Open workspace folder**. It resolves the workspace
|
|
6
|
+
that owns the Session and asks the Host to open that directory in the platform file
|
|
7
|
+
manager (Explorer on Windows, Finder on macOS, the desktop's file manager elsewhere).
|
|
8
|
+
|
|
9
|
+
- Switch: `openWorkspaceFolder` (on by default).
|
|
10
|
+
- Seat: `sidebar.workspaces.session.menu.item`, registered as `polish-open-folder`
|
|
11
|
+
at `order: 460`, after the shipped `session-id` row at `450`.
|
|
12
|
+
- Host call: `ctx.remote.session.openWorkspacePath({ path })`, gated by
|
|
13
|
+
`ctx.remote.session.canOpenWorkspacePath()`.
|
|
14
|
+
- Path source: the Workspace snapshot (`ctx.workspaces.list`), matched through
|
|
15
|
+
`WorkspaceView.sessionIds`.
|
|
16
|
+
|
|
17
|
+
The row renders nothing when the switch is off, when the Session belongs to no
|
|
18
|
+
workspace, or when the Host reports it cannot open a path. A menu row that cannot
|
|
19
|
+
act is worse than no row.
|
|
20
|
+
|
|
21
|
+
## What cannot ship, and why
|
|
22
|
+
|
|
23
|
+
The requested surface was **right-click on the workspace row itself**, with a menu
|
|
24
|
+
like the one AryaAI has (`Open in Explorer`, `Rename`, `Manage folders`, `Delete
|
|
25
|
+
workspace`). That is not reachable from a plugin, for three independent reasons:
|
|
26
|
+
|
|
27
|
+
1. The workspace row is drawn by `packages/client/ui-workspace/src/client/rows/Rows.tsx`.
|
|
28
|
+
The row element carries `data-row-key`, `role="treeitem"` and `onClick`, and has
|
|
29
|
+
no `onContextMenu`. Its "…" button opens a menu whose entries are hardcoded
|
|
30
|
+
(`rename`, `delete.workspace`).
|
|
31
|
+
2. No slot exists for per-workspace menu entries. The only workspace-related list
|
|
32
|
+
slots are `sidebar.workspaces.session.menu.item` and
|
|
33
|
+
`sidebar.workspaces.session.row.action`, both scoped to a **Session** row. The
|
|
34
|
+
single `sidebar.workspaces` slot replaces the whole browser
|
|
35
|
+
(`replaceRisk: 'shadows-shipped-ui'`), which would mean rebuilding the shipped
|
|
36
|
+
sidebar.
|
|
37
|
+
3. Native menus are closed to plugins: `apps/desktop/src/main.ts` allows only the
|
|
38
|
+
`application` and `edit` roles from the main window's main frame, and the
|
|
39
|
+
desktop IPC surface (`apps/desktop/src/ipc.ts`, `DshDesktopProductApi`) exposes
|
|
40
|
+
no open/reveal call.
|
|
41
|
+
|
|
42
|
+
Attaching an `onContextMenu` to the row's DOM by selector would be a hack against
|
|
43
|
+
private markup — the thing this bundle's ground rules forbid, and the first thing
|
|
44
|
+
to break when the sidebar changes. The honest options were:
|
|
45
|
+
|
|
46
|
+
- **Ship on the Session row menu** (chosen): documented slot, supported API,
|
|
47
|
+
survives harness updates.
|
|
48
|
+
- Change the shell to declare `sidebar.workspaces.menu.item` and render it from the
|
|
49
|
+
workspace row, then release a harness patch. Declined: the owner wants to keep
|
|
50
|
+
taking harness updates from public releases.
|
|
51
|
+
- Park the feature. Declined: the underlying capability (open a workspace folder)
|
|
52
|
+
is worth shipping now.
|
|
53
|
+
|
|
54
|
+
If the harness ever declares a workspace-scoped menu slot, this feature should move
|
|
55
|
+
there: the Host call and the path resolution stay exactly as they are, and only the
|
|
56
|
+
registration line changes.
|
|
57
|
+
|
|
58
|
+
## Evidence for the next reader
|
|
59
|
+
|
|
60
|
+
- Slot contract: `packages/client/ui-workspace/src/client/contract/slots.ts:152-205`.
|
|
61
|
+
- Shipped row to copy: `packages/client/ui-workspace/src/client/session-actions/SessionId.tsx`.
|
|
62
|
+
- Remote methods: `packages/api/session-controller/src/index.ts:336-339`
|
|
63
|
+
(`canOpenWorkspacePath`) and `:357-367` (`openWorkspacePath`).
|
|
64
|
+
- Windows reveal path: `packages/util/native-command/src/path-opener.ts:307`
|
|
65
|
+
(`runExplorer(['/select,', …])`); `open-in-app` is mounted in the web bundle at
|
|
66
|
+
`packages/bundle/web-app/cordis.patch.yml:73-81`, so this works under `start:web`.
|
|
67
|
+
- Workspace view: `packages/api/workspace-controller/src/types.ts:18-30`.
|
package/entries/registry.js
CHANGED
|
@@ -46,6 +46,12 @@ export const features = [
|
|
|
46
46
|
description: 'Show the remaining account balance in the sidebar foot, above the account row.',
|
|
47
47
|
defaultOn: true,
|
|
48
48
|
},
|
|
49
|
+
{
|
|
50
|
+
id: 'openWorkspaceFolder',
|
|
51
|
+
label: 'Open workspace folder',
|
|
52
|
+
description: 'Add an "Open workspace folder" row to a Session\u2019s menu that opens its workspace in the file browser.',
|
|
53
|
+
defaultOn: true,
|
|
54
|
+
},
|
|
49
55
|
]
|
|
50
56
|
|
|
51
57
|
/**
|
package/lib/client.js
CHANGED
|
@@ -19,11 +19,17 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
"label": "Account balance",
|
|
20
20
|
"description": "Show the remaining account balance in the sidebar foot, above the account row.",
|
|
21
21
|
"defaultOn": true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"key": "openWorkspaceFolder",
|
|
25
|
+
"label": "Open workspace folder",
|
|
26
|
+
"description": "Add an “Open workspace folder” row to a Session’s menu that opens its workspace in the file browser.",
|
|
27
|
+
"defaultOn": true
|
|
22
28
|
}
|
|
23
29
|
]
|
|
24
30
|
const ROW_ID = "polish"
|
|
25
31
|
const ROW_KEY = "@aryacode/dsh-aryacode-polish" + '#' + ROW_ID
|
|
26
|
-
const CLIENT_VERSION = "0.
|
|
32
|
+
const CLIENT_VERSION = "0.4.4"
|
|
27
33
|
|
|
28
34
|
/**
|
|
29
35
|
* The two pure decisions behind the balance line: which wallet to show, and how
|
|
@@ -166,6 +172,19 @@ window.__ModuleLoader__.load({
|
|
|
166
172
|
const React = require('react')
|
|
167
173
|
const ReactDOM = require('react-dom')
|
|
168
174
|
|
|
175
|
+
/**
|
|
176
|
+
* The Shell's own UI primitives. Guarded because the menu row degrades to a plain
|
|
177
|
+
* button when a Shell answers no primitives, and a bundle that throws on load
|
|
178
|
+
* would take every other surface down with it.
|
|
179
|
+
*/
|
|
180
|
+
const primitives = (() => {
|
|
181
|
+
try {
|
|
182
|
+
return require('@deepseek-ai/dsh-client-ui-primitives')
|
|
183
|
+
} catch {
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
})()
|
|
187
|
+
|
|
169
188
|
/** Shared spacing for the page's prose. */
|
|
170
189
|
const SPACING = { margin: '0 0 10px', opacity: 0.85, lineHeight: '1.5' }
|
|
171
190
|
|
|
@@ -684,6 +703,74 @@ window.__ModuleLoader__.load({
|
|
|
684
703
|
}, props.wide === false ? money : 'Balance ' + money)
|
|
685
704
|
}
|
|
686
705
|
|
|
706
|
+
/**
|
|
707
|
+
* The workspace a Session belongs to.
|
|
708
|
+
*
|
|
709
|
+
* Occupants of `sidebar.workspaces.session.menu.item` address their data by
|
|
710
|
+
* Session id and must not bind or retain the Session, so the workspace is read
|
|
711
|
+
* from the Workspace snapshot at render time and never memoized.
|
|
712
|
+
*
|
|
713
|
+
* @param workspaces - the Shell's Workspace service, when it is loaded.
|
|
714
|
+
* @param sessionId - the row's Session.
|
|
715
|
+
* @returns the owning workspace, or undefined when it cannot be resolved.
|
|
716
|
+
*/
|
|
717
|
+
function workspaceOf(workspaces, sessionId) {
|
|
718
|
+
if (workspaces === undefined || workspaces === null || sessionId === undefined) return undefined
|
|
719
|
+
let items
|
|
720
|
+
try {
|
|
721
|
+
items = workspaces.list.getSnapshot().items
|
|
722
|
+
} catch {
|
|
723
|
+
return undefined
|
|
724
|
+
}
|
|
725
|
+
if (!Array.isArray(items)) return undefined
|
|
726
|
+
return items.find(item => Array.isArray(item.sessionIds)
|
|
727
|
+
&& item.sessionIds.indexOf(sessionId) !== -1
|
|
728
|
+
&& typeof item.path === 'string'
|
|
729
|
+
&& item.path !== '')
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* "Open workspace folder": one row in a Session's "…" menu.
|
|
734
|
+
*
|
|
735
|
+
* It renders nothing when the switch is off or the Session has no workspace the
|
|
736
|
+
* Host could open, because a menu row that cannot act is worse than no row.
|
|
737
|
+
*
|
|
738
|
+
* @param props - the row's Session id, our lazy `configForms`/`workspaces`, the
|
|
739
|
+
* opener, and the slot's injected `useMenuOpenState` hook.
|
|
740
|
+
* @returns the menu row, or null.
|
|
741
|
+
*/
|
|
742
|
+
function OpenFolderMenuItem(props) {
|
|
743
|
+
const enabled = useSwitch(props.configForms, 'openWorkspaceFolder', true)
|
|
744
|
+
// Hooks run unconditionally; the row simply renders nothing when it cannot act.
|
|
745
|
+
const menu = typeof props.useMenuOpenState === 'function' ? props.useMenuOpenState() : undefined
|
|
746
|
+
const closeMenu = Array.isArray(menu) && typeof menu[1] === 'function' ? menu[1] : undefined
|
|
747
|
+
const workspace = enabled ? workspaceOf(props.workspaces, props.sessionId) : undefined
|
|
748
|
+
if (!enabled || workspace === undefined) return null
|
|
749
|
+
const label = 'Open workspace folder'
|
|
750
|
+
const onSelect = () => {
|
|
751
|
+
if (closeMenu !== undefined) closeMenu(false)
|
|
752
|
+
const open = props.openFolder
|
|
753
|
+
if (typeof open !== 'function') return
|
|
754
|
+
void Promise.resolve()
|
|
755
|
+
.then(() => open(workspace))
|
|
756
|
+
.catch(error => {
|
|
757
|
+
// A menu row that silently does nothing is unfixable in the field, so the
|
|
758
|
+
// failure is reported instead of dropped. This is the only diagnostic the
|
|
759
|
+
// bundle writes.
|
|
760
|
+
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
761
|
+
console.warn('[aryacode-polish] open workspace folder failed:', error && error.message ? error.message : error)
|
|
762
|
+
}
|
|
763
|
+
})
|
|
764
|
+
}
|
|
765
|
+
if (primitives !== undefined && typeof primitives.MenuItemButton === 'function') {
|
|
766
|
+
const icon = typeof primitives.IconFolderOpenRegular === 'function'
|
|
767
|
+
? React.createElement(primitives.IconFolderOpenRegular)
|
|
768
|
+
: undefined
|
|
769
|
+
return React.createElement(primitives.MenuItemButton, { icon, onSelect }, label)
|
|
770
|
+
}
|
|
771
|
+
return React.createElement('button', { type: 'button', role: 'menuitem', onClick: onSelect }, label)
|
|
772
|
+
}
|
|
773
|
+
|
|
687
774
|
return {
|
|
688
775
|
inject: ['slots'],
|
|
689
776
|
/**
|
|
@@ -720,6 +807,32 @@ window.__ModuleLoader__.load({
|
|
|
720
807
|
inject: () => ({ configForms, loadBalance }),
|
|
721
808
|
}, BalanceLine))
|
|
722
809
|
})
|
|
810
|
+
// The Workspace service and the Session Remote are both required for the menu
|
|
811
|
+
// row: it reads the folder from the first and opens it through the second.
|
|
812
|
+
// They are declared on the inject list because a Remote namespace may not be
|
|
813
|
+
// read through `ctx.get` — the Shell's guard refuses it.
|
|
814
|
+
ctx.inject(['workspaces', 'remote', 'remote.session'], (workspaceCtx) => {
|
|
815
|
+
const openFolder = async (workspace) => {
|
|
816
|
+
const remote = workspaceCtx.remote.session
|
|
817
|
+
// A Remote method answers an envelope (`{ ok, value }`), not the bare
|
|
818
|
+
// return value; a Shell that answered a plain boolean is tolerated too.
|
|
819
|
+
const answer = await remote.canOpenWorkspacePath()
|
|
820
|
+
const available = answer !== null && typeof answer === 'object'
|
|
821
|
+
? answer.ok === true && answer.value === true
|
|
822
|
+
: answer === true
|
|
823
|
+
if (!available) throw new Error('this Host cannot open a folder')
|
|
824
|
+
const result = await remote.openWorkspacePath({ path: workspace.path })
|
|
825
|
+
if (result !== null && typeof result === 'object' && 'ok' in result && result.ok !== true) {
|
|
826
|
+
throw new Error('the Host refused to open the folder')
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
workspaceCtx.slots.inject('sidebar.workspaces.session.menu.item', () => workspaceCtx.slots.register({
|
|
830
|
+
name: 'sidebar.workspaces.session.menu.item',
|
|
831
|
+
id: 'polish-open-folder',
|
|
832
|
+
order: 460,
|
|
833
|
+
inject: () => ({ configForms, openFolder, workspaces: workspaceCtx.get('workspaces') }),
|
|
834
|
+
}, OpenFolderMenuItem))
|
|
835
|
+
})
|
|
723
836
|
},
|
|
724
837
|
}
|
|
725
838
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryacode/dsh-aryacode-polish",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "AryaCode Polish: one install for the small things that make AryaCode feel finished. One Plugins row, one switch per quality-of-life feature.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|