@aryacode/dsh-aryacode-polish 0.3.9 → 0.5.0
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 +85 -2
- package/README.md +5 -2
- package/client/face.js +436 -5
- package/client/switches.json +12 -0
- package/docs/github-workspaces.md +63 -0
- package/docs/workspace-folder.md +73 -0
- package/entries/github.js +347 -0
- package/entries/registry.js +14 -0
- package/lib/client.js +449 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,88 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-10-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Add a workspace from GitHub.** A new switch (`githubWorkspace`) adds an
|
|
8
|
+
**Add workspace from GitHub** entry to the sidebar foot. It lists the
|
|
9
|
+
repositories the GitHub CLI session can clone, clones the chosen one into
|
|
10
|
+
`<home>\AryaCode\<owner>\<repo>`, and registers that folder as a workspace
|
|
11
|
+
through the same controller call the "+" menu uses. The token never reaches the
|
|
12
|
+
browser: the Host reads it with `gh auth token` and hands it to git as an
|
|
13
|
+
`http.extraHeader` in git's environment, so it stays out of the URL, the
|
|
14
|
+
command line, and any stored remote. See
|
|
15
|
+
[`docs/github-workspaces.md`](docs/github-workspaces.md).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **A clone would have failed on a real machine.** `cloneRepo` spawned the
|
|
20
|
+
argument list without the `git` command name and passed the token as an option
|
|
21
|
+
the runner never read, so the clone carried no authentication at all. It now
|
|
22
|
+
spawns `git` with the token in `GIT_CONFIG_*`, which is what GitHub's git
|
|
23
|
+
server accepts, and `test/github.spec.mjs` pins the spawn shape.
|
|
24
|
+
- **The signed-out reason told you nothing you could act on.** It now names the
|
|
25
|
+
command that fixes it (`gh auth login`).
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- `pnpm test` also runs `test/github.spec.mjs`; the suite is 87 tests.
|
|
30
|
+
|
|
31
|
+
## [0.4.4] - 2026-10-27
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **The row reported "this Host cannot open a folder" on a Host that can.** A Remote
|
|
36
|
+
method answers an envelope — `{ ok, value }` — not the bare return value, so
|
|
37
|
+
`canOpenWorkspacePath()` never looked like `true`. The row now reads
|
|
38
|
+
`ok === true && value === true`, tolerates a plain boolean, and checks the
|
|
39
|
+
`ok` flag of the open call itself instead of trusting that no throw means opened.
|
|
40
|
+
|
|
41
|
+
## [0.4.3] - 2026-10-27
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **The row still could not open anything.** 0.4.2 declared `remote.session` but the
|
|
46
|
+
guard checks each namespace on its own: reading `ctx.remote.session` needs both
|
|
47
|
+
`remote` and `remote.session` on the inject list, so the call failed one level
|
|
48
|
+
earlier. The registration now declares `['workspaces', 'remote', 'remote.session']`.
|
|
49
|
+
|
|
50
|
+
## [0.4.2] - 2026-10-27
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **The row did nothing when clicked.** The Shell refuses `ctx.get('remote.session')`
|
|
55
|
+
— a Remote namespace may only be read through a declared injection — so the open
|
|
56
|
+
call failed before it reached the Host. The registration now declares
|
|
57
|
+
`ctx.inject(['workspaces', 'remote.session'], …)` and calls the namespace the
|
|
58
|
+
injected context hands it. Found by the row reporting its own failure (0.4.1).
|
|
59
|
+
|
|
60
|
+
## [0.4.1] - 2026-10-27
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- The **Open workspace folder** row reports a failed open instead of doing nothing.
|
|
65
|
+
A menu row that silently ignores the click is impossible to diagnose in the
|
|
66
|
+
field; the bundle now writes one console.warn naming the failing step (no
|
|
67
|
+
Session Remote, a Host that cannot open a path, or the Remote error itself).
|
|
68
|
+
|
|
69
|
+
## [0.4.0] - 2026-10-27
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- **Open workspace folder.** A Session's "…" menu gains an **Open workspace folder**
|
|
74
|
+
row that opens the Session's workspace directory in Explorer/Finder through the
|
|
75
|
+
Session Remote (`openWorkspacePath`), gated by `canOpenWorkspacePath` and resolved
|
|
76
|
+
from the Workspace snapshot. New switch `openWorkspaceFolder`, on by default, and
|
|
77
|
+
the row hides itself when the Session has no workspace or the Host cannot open a
|
|
78
|
+
path.
|
|
79
|
+
- `docs/workspace-folder.md` records why the requested surface — right-clicking the
|
|
80
|
+
workspace row itself — is not reachable from a plugin: that row has no context
|
|
81
|
+
menu, the workspace-scoped menu list slot does not exist, and native menus and
|
|
82
|
+
desktop IPC are closed to plugins. The bundle ships on the documented Session-row
|
|
83
|
+
slot rather than hijacking private markup, and the note says where the feature
|
|
84
|
+
should move if a workspace-scoped slot ever lands.
|
|
85
|
+
|
|
3
86
|
## [0.3.9] - 2026-10-27
|
|
4
87
|
|
|
5
88
|
### Changed
|
|
@@ -177,8 +260,8 @@
|
|
|
177
260
|
|
|
178
261
|
### Fixed
|
|
179
262
|
|
|
180
|
-
- The pill's `≈` was stored as cp1252 mojibake (
|
|
181
|
-
|
|
263
|
+
- The pill's `≈` was stored as cp1252 mojibake (`≈`), so a real turn rendered
|
|
264
|
+
`≈$0.003` instead of `≈$0.003`. Same cause as the two strings repaired in
|
|
182
265
|
0.2.1 — a PowerShell round-trip over the face source — and the last of that
|
|
183
266
|
damage. Verified against a real turn in Dev AryaCode.
|
|
184
267
|
|
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: **four 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,8 @@ 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. |
|
|
36
|
+
| **Add a workspace from GitHub** (`githubWorkspace`) | On | **Add workspace from GitHub** in the sidebar foot: the dialog lists the repositories your GitHub CLI session can clone, and cloning one creates the workspace. The token never enters the browser — it is read on the Host, used only as git's http auth header, and never stored. See [GitHub workspaces](docs/github-workspaces.md). |
|
|
35
37
|
|
|
36
38
|
### How the cost is computed
|
|
37
39
|
|
|
@@ -121,6 +123,7 @@ entries/polish.js the row: Config derived from the registry, apply()
|
|
|
121
123
|
entries/registry.js the feature list — the one place a feature is declared
|
|
122
124
|
entries/usage.js the fold: model-attributed usage out of the session log
|
|
123
125
|
entries/cost.js the polishCost projection: rates, pricing, host mount
|
|
126
|
+
entries/github.js the GitHub route: gh token, repository list, clone, workspace
|
|
124
127
|
entries/config.js volatile-reference accessors (resolve/flag/list/text)
|
|
125
128
|
lib/index.js mount point re-exporting the row
|
|
126
129
|
client/face.js browser face source: the Configure page and every surface
|
|
@@ -130,7 +133,7 @@ client/switches.json the browser half of the seam — one entry per switch
|
|
|
130
133
|
scripts/build-client.mjs wraps client/face.js into lib/client.js
|
|
131
134
|
lib/client.js generated: the bundle the Shell loads
|
|
132
135
|
locale/ card title and description (en, zh)
|
|
133
|
-
docs/ parked investigations
|
|
136
|
+
docs/ feature notes and parked investigations
|
|
134
137
|
test/ node --test specs
|
|
135
138
|
```
|
|
136
139
|
|
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
|
|
|
@@ -517,22 +530,391 @@ function BalanceLine(props) {
|
|
|
517
530
|
return React.createElement('div', { 'data-polish-balance': 'empty', style: { display: 'none' } })
|
|
518
531
|
}
|
|
519
532
|
const money = formatBalance(wallet)
|
|
533
|
+
// The foot is a single row shared with the GitHub entry, so the amount stays
|
|
534
|
+
// bare: a tooltip names it, and nothing here competes for width.
|
|
520
535
|
return React.createElement('div', {
|
|
521
536
|
title: 'Remaining account balance',
|
|
537
|
+
'data-polish-balance': 'ready',
|
|
522
538
|
style: {
|
|
523
539
|
display: 'flex',
|
|
524
540
|
alignItems: 'center',
|
|
525
541
|
gap: '6px',
|
|
526
|
-
padding: '2px
|
|
542
|
+
padding: '2px 4px 4px 8px',
|
|
527
543
|
fontSize: '12px',
|
|
528
544
|
lineHeight: '16px',
|
|
529
|
-
|
|
530
|
-
overflow: 'hidden',
|
|
545
|
+
flex: '0 0 auto',
|
|
531
546
|
whiteSpace: 'nowrap',
|
|
532
|
-
textOverflow: 'ellipsis',
|
|
533
547
|
opacity: 0.75,
|
|
548
|
+
fontVariantNumeric: 'tabular-nums',
|
|
549
|
+
},
|
|
550
|
+
}, money)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* The workspace a Session belongs to.
|
|
555
|
+
*
|
|
556
|
+
* Occupants of `sidebar.workspaces.session.menu.item` address their data by
|
|
557
|
+
* Session id and must not bind or retain the Session, so the workspace is read
|
|
558
|
+
* from the Workspace snapshot at render time and never memoized.
|
|
559
|
+
*
|
|
560
|
+
* @param workspaces - the Shell's Workspace service, when it is loaded.
|
|
561
|
+
* @param sessionId - the row's Session.
|
|
562
|
+
* @returns the owning workspace, or undefined when it cannot be resolved.
|
|
563
|
+
*/
|
|
564
|
+
function workspaceOf(workspaces, sessionId) {
|
|
565
|
+
if (workspaces === undefined || workspaces === null || sessionId === undefined) return undefined
|
|
566
|
+
let items
|
|
567
|
+
try {
|
|
568
|
+
items = workspaces.list.getSnapshot().items
|
|
569
|
+
} catch {
|
|
570
|
+
return undefined
|
|
571
|
+
}
|
|
572
|
+
if (!Array.isArray(items)) return undefined
|
|
573
|
+
return items.find(item => Array.isArray(item.sessionIds)
|
|
574
|
+
&& item.sessionIds.indexOf(sessionId) !== -1
|
|
575
|
+
&& typeof item.path === 'string'
|
|
576
|
+
&& item.path !== '')
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* "Open workspace folder": one row in a Session's "…" menu.
|
|
581
|
+
*
|
|
582
|
+
* It renders nothing when the switch is off or the Session has no workspace the
|
|
583
|
+
* Host could open, because a menu row that cannot act is worse than no row.
|
|
584
|
+
*
|
|
585
|
+
* @param props - the row's Session id, our lazy `configForms`/`workspaces`, the
|
|
586
|
+
* opener, and the slot's injected `useMenuOpenState` hook.
|
|
587
|
+
* @returns the menu row, or null.
|
|
588
|
+
*/
|
|
589
|
+
function OpenFolderMenuItem(props) {
|
|
590
|
+
const enabled = useSwitch(props.configForms, 'openWorkspaceFolder', true)
|
|
591
|
+
// Hooks run unconditionally; the row simply renders nothing when it cannot act.
|
|
592
|
+
const menu = typeof props.useMenuOpenState === 'function' ? props.useMenuOpenState() : undefined
|
|
593
|
+
const closeMenu = Array.isArray(menu) && typeof menu[1] === 'function' ? menu[1] : undefined
|
|
594
|
+
const workspace = enabled ? workspaceOf(props.workspaces, props.sessionId) : undefined
|
|
595
|
+
if (!enabled || workspace === undefined) return null
|
|
596
|
+
const label = 'Open workspace folder'
|
|
597
|
+
const onSelect = () => {
|
|
598
|
+
if (closeMenu !== undefined) closeMenu(false)
|
|
599
|
+
const open = props.openFolder
|
|
600
|
+
if (typeof open !== 'function') return
|
|
601
|
+
void Promise.resolve()
|
|
602
|
+
.then(() => open(workspace))
|
|
603
|
+
.catch(error => {
|
|
604
|
+
// A menu row that silently does nothing is unfixable in the field, so the
|
|
605
|
+
// failure is reported instead of dropped. This is the only diagnostic the
|
|
606
|
+
// bundle writes.
|
|
607
|
+
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
608
|
+
console.warn('[aryacode-polish] open workspace folder failed:', error && error.message ? error.message : error)
|
|
609
|
+
}
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
if (primitives !== undefined && typeof primitives.MenuItemButton === 'function') {
|
|
613
|
+
const icon = typeof primitives.IconFolderOpenRegular === 'function'
|
|
614
|
+
? React.createElement(primitives.IconFolderOpenRegular)
|
|
615
|
+
: undefined
|
|
616
|
+
return React.createElement(primitives.MenuItemButton, { icon, onSelect }, label)
|
|
617
|
+
}
|
|
618
|
+
return React.createElement('button', { type: 'button', role: 'menuitem', onClick: onSelect }, label)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** The Host route the GitHub halves talk over. Document-relative, like every app route. */
|
|
622
|
+
const GITHUB_ROUTE = 'api/polish.github'
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Whether the GitHub panel is open. One store for the whole bundle: the footer
|
|
626
|
+
* row opens it, and the overlay panel is the only reader.
|
|
627
|
+
*/
|
|
628
|
+
const githubStore = (() => {
|
|
629
|
+
let open = false
|
|
630
|
+
const listeners = new Set()
|
|
631
|
+
return {
|
|
632
|
+
isOpen: () => open,
|
|
633
|
+
subscribe(listener) {
|
|
634
|
+
listeners.add(listener)
|
|
635
|
+
return () => { listeners.delete(listener) }
|
|
636
|
+
},
|
|
637
|
+
set(next) {
|
|
638
|
+
if (open === next) return
|
|
639
|
+
open = next
|
|
640
|
+
for (const listener of listeners) listener()
|
|
641
|
+
},
|
|
642
|
+
}
|
|
643
|
+
})()
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Post one action to the GitHub route.
|
|
647
|
+
* @param action - `status`, `repos` or `clone`.
|
|
648
|
+
* @param extra - fields the action needs.
|
|
649
|
+
* @returns the route's `value`, or throws with its own message.
|
|
650
|
+
*/
|
|
651
|
+
async function githubCall(action, extra) {
|
|
652
|
+
const response = await fetch(GITHUB_ROUTE, {
|
|
653
|
+
method: 'POST',
|
|
654
|
+
headers: { 'content-type': 'application/json' },
|
|
655
|
+
body: JSON.stringify({ action, ...(extra ?? {}) }),
|
|
656
|
+
})
|
|
657
|
+
const payload = await response.json().catch(() => undefined)
|
|
658
|
+
if (payload === undefined || typeof payload !== 'object') {
|
|
659
|
+
throw new Error(`the Host answered ${String(response.status)}`)
|
|
660
|
+
}
|
|
661
|
+
if (payload.ok !== true) throw new Error(payload.error || 'GitHub request failed')
|
|
662
|
+
return payload.value
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Modal backdrop and panel skin, matching the Shell's own dialogs. */
|
|
666
|
+
const GITHUB_BACKDROP_STYLE = {
|
|
667
|
+
position: 'fixed',
|
|
668
|
+
inset: 0,
|
|
669
|
+
zIndex: 1200,
|
|
670
|
+
display: 'flex',
|
|
671
|
+
alignItems: 'center',
|
|
672
|
+
justifyContent: 'center',
|
|
673
|
+
background: 'rgba(16, 18, 22, 0.32)',
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const GITHUB_CARD_STYLE = {
|
|
677
|
+
...PANEL_STYLE,
|
|
678
|
+
position: 'static',
|
|
679
|
+
width: 'min(560px, calc(100vw - 32px))',
|
|
680
|
+
maxHeight: 'min(560px, calc(100vh - 64px))',
|
|
681
|
+
display: 'flex',
|
|
682
|
+
flexDirection: 'column',
|
|
683
|
+
overflow: 'hidden',
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const GITHUB_LIST_STYLE = {
|
|
687
|
+
marginTop: '10px',
|
|
688
|
+
overflowY: 'auto',
|
|
689
|
+
borderTop: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const GITHUB_ROW_STYLE = {
|
|
693
|
+
display: 'flex',
|
|
694
|
+
alignItems: 'center',
|
|
695
|
+
gap: '8px',
|
|
696
|
+
width: '100%',
|
|
697
|
+
padding: '8px 4px',
|
|
698
|
+
border: 'none',
|
|
699
|
+
background: 'transparent',
|
|
700
|
+
color: 'inherit',
|
|
701
|
+
font: 'inherit',
|
|
702
|
+
textAlign: 'left',
|
|
703
|
+
cursor: 'pointer',
|
|
704
|
+
borderBottom: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.08))',
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const GITHUB_INPUT_STYLE = {
|
|
708
|
+
width: '100%',
|
|
709
|
+
boxSizing: 'border-box',
|
|
710
|
+
padding: '6px 8px',
|
|
711
|
+
font: 'inherit',
|
|
712
|
+
color: 'inherit',
|
|
713
|
+
background: 'transparent',
|
|
714
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.2))',
|
|
715
|
+
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* The add-a-workspace-from-GitHub dialog.
|
|
720
|
+
*
|
|
721
|
+
* Sign-in is the GitHub CLI's own session: the Host asks `gh` for the token it
|
|
722
|
+
* already holds, so this dialog never collects a credential. A clone becomes a
|
|
723
|
+
* workspace through the Workspace Remote, which is the same call the "+" menu
|
|
724
|
+
* makes after its folder picker returns a path.
|
|
725
|
+
*
|
|
726
|
+
* @param props - our lazy `configForms` and the `createWorkspace` opener.
|
|
727
|
+
* @returns the dialog, or null while it is closed or switched off.
|
|
728
|
+
*/
|
|
729
|
+
function GitHubPanel(props) {
|
|
730
|
+
const open = React.useSyncExternalStore(githubStore.subscribe, githubStore.isOpen)
|
|
731
|
+
const enabled = useSwitch(props.configForms, 'githubWorkspace', true)
|
|
732
|
+
const [phase, setPhase] = React.useState('idle')
|
|
733
|
+
const [account, setAccount] = React.useState(undefined)
|
|
734
|
+
const [repos, setRepos] = React.useState([])
|
|
735
|
+
const [query, setQuery] = React.useState('')
|
|
736
|
+
const [error, setError] = React.useState('')
|
|
737
|
+
const [busy, setBusy] = React.useState('')
|
|
738
|
+
|
|
739
|
+
React.useEffect(() => {
|
|
740
|
+
if (!open || !enabled) return undefined
|
|
741
|
+
let live = true
|
|
742
|
+
setPhase('loading')
|
|
743
|
+
setError('')
|
|
744
|
+
setBusy('')
|
|
745
|
+
setQuery('')
|
|
746
|
+
Promise.resolve()
|
|
747
|
+
.then(() => githubCall('status'))
|
|
748
|
+
.then(status => {
|
|
749
|
+
if (!live) return undefined
|
|
750
|
+
setAccount(status)
|
|
751
|
+
if (status.signedIn !== true) return undefined
|
|
752
|
+
return githubCall('repos').then(list => {
|
|
753
|
+
if (!live) return
|
|
754
|
+
setRepos(Array.isArray(list.repos) ? list.repos : [])
|
|
755
|
+
})
|
|
756
|
+
})
|
|
757
|
+
.then(() => { if (live) setPhase('ready') })
|
|
758
|
+
.catch(reason => {
|
|
759
|
+
if (!live) return
|
|
760
|
+
setError(reason instanceof Error ? reason.message : String(reason))
|
|
761
|
+
setPhase('ready')
|
|
762
|
+
})
|
|
763
|
+
return () => { live = false }
|
|
764
|
+
}, [open, enabled])
|
|
765
|
+
|
|
766
|
+
React.useEffect(() => {
|
|
767
|
+
if (!open) return undefined
|
|
768
|
+
const onKey = event => { if (event.key === 'Escape') githubStore.set(false) }
|
|
769
|
+
document.addEventListener('keydown', onKey)
|
|
770
|
+
return () => document.removeEventListener('keydown', onKey)
|
|
771
|
+
}, [open])
|
|
772
|
+
|
|
773
|
+
if (!open || !enabled) return null
|
|
774
|
+
|
|
775
|
+
const signedIn = account !== undefined && account.signedIn === true
|
|
776
|
+
const needle = query.trim().toLowerCase()
|
|
777
|
+
const visible = needle === ''
|
|
778
|
+
? repos
|
|
779
|
+
: repos.filter(repo => `${repo.fullName} ${repo.description}`.toLowerCase().includes(needle))
|
|
780
|
+
const root = account !== undefined && typeof account.root === 'string' ? account.root : ''
|
|
781
|
+
|
|
782
|
+
const clone = repo => {
|
|
783
|
+
if (busy !== '') return
|
|
784
|
+
setBusy(repo.fullName)
|
|
785
|
+
setError('')
|
|
786
|
+
Promise.resolve()
|
|
787
|
+
.then(() => githubCall('clone', { owner: repo.owner, name: repo.name }))
|
|
788
|
+
.then(result => Promise.resolve(props.createWorkspace(result.path)).then(() => result))
|
|
789
|
+
.then(result => {
|
|
790
|
+
setBusy('')
|
|
791
|
+
githubStore.set(false)
|
|
792
|
+
if (typeof console !== 'undefined' && typeof console.info === 'function') {
|
|
793
|
+
console.info('[aryacode-polish] workspace ready:', result.path)
|
|
794
|
+
}
|
|
795
|
+
})
|
|
796
|
+
.catch(reason => {
|
|
797
|
+
setBusy('')
|
|
798
|
+
setError(reason instanceof Error ? reason.message : String(reason))
|
|
799
|
+
})
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const rows = visible.slice(0, 200).map(repo => React.createElement('button', {
|
|
803
|
+
key: repo.fullName,
|
|
804
|
+
type: 'button',
|
|
805
|
+
style: GITHUB_ROW_STYLE,
|
|
806
|
+
disabled: busy !== '',
|
|
807
|
+
onClick: () => clone(repo),
|
|
808
|
+
title: repo.description === '' ? repo.fullName : `${repo.fullName} — ${repo.description}`,
|
|
809
|
+
'data-polish-github-row': repo.fullName,
|
|
810
|
+
},
|
|
811
|
+
React.createElement('span', { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
|
|
812
|
+
repo.fullName,
|
|
813
|
+
repo.isPrivate ? React.createElement('span', { style: { marginLeft: '6px', opacity: 0.6 } }, 'private') : null,
|
|
814
|
+
repo.isFork ? React.createElement('span', { style: { marginLeft: '6px', opacity: 0.6 } }, 'fork') : null),
|
|
815
|
+
repo.cloned === true
|
|
816
|
+
? React.createElement('span', { style: { opacity: 0.6 } }, 'cloned')
|
|
817
|
+
: null,
|
|
818
|
+
busy === repo.fullName ? React.createElement('span', { style: { opacity: 0.7 } }, 'cloning…') : null))
|
|
819
|
+
|
|
820
|
+
return ReactDOM.createPortal(
|
|
821
|
+
React.createElement('div', {
|
|
822
|
+
style: GITHUB_BACKDROP_STYLE,
|
|
823
|
+
'data-polish-github': 'dialog',
|
|
824
|
+
onPointerDown: event => { if (event.target === event.currentTarget) githubStore.set(false) },
|
|
825
|
+
},
|
|
826
|
+
React.createElement('div', { style: GITHUB_CARD_STYLE, role: 'dialog', 'aria-label': 'Add a workspace from GitHub' },
|
|
827
|
+
React.createElement('div', { style: { ...PANEL_TITLE_STYLE, marginBottom: '4px' } },
|
|
828
|
+
React.createElement('span', null, 'Add a workspace from GitHub'),
|
|
829
|
+
React.createElement('button', {
|
|
830
|
+
type: 'button',
|
|
831
|
+
onClick: () => githubStore.set(false),
|
|
832
|
+
style: { border: 'none', background: 'transparent', color: 'inherit', font: 'inherit', cursor: 'pointer', opacity: 0.7 },
|
|
833
|
+
'aria-label': 'Close',
|
|
834
|
+
}, '×')),
|
|
835
|
+
React.createElement('div', { style: { opacity: 0.75, minHeight: '18px' } },
|
|
836
|
+
phase === 'loading' && !signedIn ? 'Checking the GitHub CLI session…' : null,
|
|
837
|
+
signedIn ? `Signed in as ${String(account.login)} · cloning into ${root}` : null,
|
|
838
|
+
!signedIn && phase !== 'loading'
|
|
839
|
+
? 'The GitHub CLI is not signed in. Run `gh auth login` in a terminal, then reopen this dialog.'
|
|
840
|
+
: null),
|
|
841
|
+
signedIn
|
|
842
|
+
? React.createElement('input', {
|
|
843
|
+
style: { ...GITHUB_INPUT_STYLE, marginTop: '10px' },
|
|
844
|
+
value: query,
|
|
845
|
+
placeholder: 'Search your repositories',
|
|
846
|
+
'aria-label': 'Search your repositories',
|
|
847
|
+
autoFocus: true,
|
|
848
|
+
onChange: event => setQuery(event.target.value),
|
|
849
|
+
})
|
|
850
|
+
: null,
|
|
851
|
+
error !== ''
|
|
852
|
+
? React.createElement('div', { style: { marginTop: '10px', color: 'var(--dsw-alias-label-error, #b42318)' } }, error)
|
|
853
|
+
: null,
|
|
854
|
+
signedIn
|
|
855
|
+
? React.createElement('div', { style: GITHUB_LIST_STYLE, 'data-polish-github-list': String(visible.length) },
|
|
856
|
+
phase === 'loading' && visible.length === 0 ? React.createElement('div', { style: { padding: '10px 4px', opacity: 0.7 } }, 'Loading repositories…') : null,
|
|
857
|
+
phase === 'ready' && visible.length === 0 ? React.createElement('div', { style: { padding: '10px 4px', opacity: 0.7 } }, 'No repository matches that search.') : null,
|
|
858
|
+
...rows)
|
|
859
|
+
: null,
|
|
860
|
+
React.createElement('p', { style: { margin: '10px 0 0', color: 'var(--dsw-alias-label-tertiary, inherit)' } },
|
|
861
|
+
'The clone is added as a workspace. Sign-in comes from the GitHub CLI, so this plugin stores no token.')))
|
|
862
|
+
, document.body)
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* The footer row that opens the GitHub dialog.
|
|
867
|
+
* @param props - our lazy `configForms`.
|
|
868
|
+
* @returns the row, or null when the feature is switched off.
|
|
869
|
+
*/
|
|
870
|
+
function GithubEntry(props) {
|
|
871
|
+
const enabled = useSwitch(props.configForms, 'githubWorkspace', true)
|
|
872
|
+
if (!enabled) return null
|
|
873
|
+
return React.createElement('div', { style: { padding: '2px 8px 4px', minWidth: 0 } },
|
|
874
|
+
React.createElement('button', {
|
|
875
|
+
type: 'button',
|
|
876
|
+
'data-polish-github': 'entry',
|
|
877
|
+
title: 'Add a workspace from GitHub',
|
|
878
|
+
'aria-label': 'Add a workspace from GitHub',
|
|
879
|
+
onClick: () => githubStore.set(true),
|
|
880
|
+
style: {
|
|
881
|
+
display: 'flex',
|
|
882
|
+
alignItems: 'center',
|
|
883
|
+
gap: '6px',
|
|
884
|
+
maxWidth: '100%',
|
|
885
|
+
minWidth: 0,
|
|
886
|
+
padding: '2px 6px',
|
|
887
|
+
border: 'none',
|
|
888
|
+
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
889
|
+
background: 'transparent',
|
|
890
|
+
color: 'var(--dsw-alias-label-tertiary, inherit)',
|
|
891
|
+
font: 'inherit',
|
|
892
|
+
fontSize: '12px',
|
|
893
|
+
lineHeight: '18px',
|
|
894
|
+
fontVariantNumeric: 'tabular-nums',
|
|
895
|
+
whiteSpace: 'nowrap',
|
|
896
|
+
overflow: 'hidden',
|
|
897
|
+
textOverflow: 'ellipsis',
|
|
898
|
+
cursor: 'pointer',
|
|
899
|
+
},
|
|
900
|
+
onMouseEnter: event => { event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
|
|
901
|
+
onMouseLeave: event => { event.currentTarget.style.background = 'transparent' },
|
|
534
902
|
},
|
|
535
|
-
|
|
903
|
+
React.createElement('svg', {
|
|
904
|
+
width: 14,
|
|
905
|
+
height: 14,
|
|
906
|
+
viewBox: '0 0 16 16',
|
|
907
|
+
fill: 'none',
|
|
908
|
+
stroke: 'currentColor',
|
|
909
|
+
strokeWidth: 1.2,
|
|
910
|
+
strokeLinecap: 'round',
|
|
911
|
+
strokeLinejoin: 'round',
|
|
912
|
+
'aria-hidden': 'true',
|
|
913
|
+
style: { flex: 'none' },
|
|
914
|
+
},
|
|
915
|
+
React.createElement('path', { d: 'M2.5 5.5h4l1.2 1.5h5.8v6.5h-11z' }),
|
|
916
|
+
React.createElement('path', { d: 'M8 9.8v3.4M6.3 11.5h3.4' })),
|
|
917
|
+
'Add from GitHub'))
|
|
536
918
|
}
|
|
537
919
|
|
|
538
920
|
return {
|
|
@@ -571,5 +953,54 @@ return {
|
|
|
571
953
|
inject: () => ({ configForms, loadBalance }),
|
|
572
954
|
}, BalanceLine))
|
|
573
955
|
})
|
|
956
|
+
// The Workspace service and the Session Remote are both required for the menu
|
|
957
|
+
// row: it reads the folder from the first and opens it through the second.
|
|
958
|
+
// They are declared on the inject list because a Remote namespace may not be
|
|
959
|
+
// read through `ctx.get` — the Shell's guard refuses it.
|
|
960
|
+
ctx.inject(['workspaces', 'remote', 'remote.session'], (workspaceCtx) => {
|
|
961
|
+
const openFolder = async (workspace) => {
|
|
962
|
+
const remote = workspaceCtx.remote.session
|
|
963
|
+
// A Remote method answers an envelope (`{ ok, value }`), not the bare
|
|
964
|
+
// return value; a Shell that answered a plain boolean is tolerated too.
|
|
965
|
+
const answer = await remote.canOpenWorkspacePath()
|
|
966
|
+
const available = answer !== null && typeof answer === 'object'
|
|
967
|
+
? answer.ok === true && answer.value === true
|
|
968
|
+
: answer === true
|
|
969
|
+
if (!available) throw new Error('this Host cannot open a folder')
|
|
970
|
+
const result = await remote.openWorkspacePath({ path: workspace.path })
|
|
971
|
+
if (result !== null && typeof result === 'object' && 'ok' in result && result.ok !== true) {
|
|
972
|
+
throw new Error('the Host refused to open the folder')
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
workspaceCtx.slots.inject('sidebar.workspaces.session.menu.item', () => workspaceCtx.slots.register({
|
|
976
|
+
name: 'sidebar.workspaces.session.menu.item',
|
|
977
|
+
id: 'polish-open-folder',
|
|
978
|
+
order: 460,
|
|
979
|
+
inject: () => ({ configForms, openFolder, workspaces: workspaceCtx.get('workspaces') }),
|
|
980
|
+
}, OpenFolderMenuItem))
|
|
981
|
+
})
|
|
982
|
+
// Adding a workspace from GitHub goes through the Workspace service's own
|
|
983
|
+
// create call — the same one the "+" menu makes once its folder picker
|
|
984
|
+
// returns a path — so a clone lands in the sidebar exactly like a picked
|
|
985
|
+
// folder does, with no Harness change anywhere. The service takes the plain
|
|
986
|
+
// path and throws on refusal; the Remote namespace underneath it is
|
|
987
|
+
// `ctx.remote.workspace` (singular), which is why this injects the service.
|
|
988
|
+
ctx.inject(['workspaces'], (githubCtx) => {
|
|
989
|
+
const createWorkspace = async (path) => {
|
|
990
|
+
const workspace = await githubCtx.workspaces.create({ path })
|
|
991
|
+
return workspace
|
|
992
|
+
}
|
|
993
|
+
githubCtx.slots.inject('shell.overlay', () => githubCtx.slots.register({
|
|
994
|
+
name: 'shell.overlay',
|
|
995
|
+
id: 'polish-github',
|
|
996
|
+
inject: () => ({ configForms, createWorkspace }),
|
|
997
|
+
}, GitHubPanel))
|
|
998
|
+
githubCtx.slots.inject('sidebar.footer.action', () => githubCtx.slots.register({
|
|
999
|
+
name: 'sidebar.footer.action',
|
|
1000
|
+
id: 'polish-github-entry',
|
|
1001
|
+
order: 4,
|
|
1002
|
+
inject: () => ({ configForms }),
|
|
1003
|
+
}, GithubEntry))
|
|
1004
|
+
})
|
|
574
1005
|
},
|
|
575
1006
|
}
|
package/client/switches.json
CHANGED
|
@@ -12,6 +12,18 @@
|
|
|
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 “Open workspace folder” row to a Session’s menu that opens its workspace in the file browser.",
|
|
20
|
+
"defaultOn": true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"key": "githubWorkspace",
|
|
24
|
+
"label": "Add a workspace from GitHub",
|
|
25
|
+
"description": "Sign in with the GitHub CLI, browse your repositories, and clone one as a workspace.",
|
|
26
|
+
"defaultOn": true
|
|
15
27
|
}
|
|
16
28
|
]
|
|
17
29
|
}
|