@aryacode/dsh-aryacode-polish 0.5.0 → 0.6.2
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 +51 -0
- package/README.md +3 -2
- package/client/face.js +396 -46
- package/client/switches.json +6 -0
- package/docs/github-workspaces.md +31 -10
- package/entries/folders.js +115 -0
- package/entries/registry.js +8 -0
- package/lib/client.js +403 -47
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.2] - 2026-10-28
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **The "+" menu and the GitHub dialog are opaque again in the desktop app.**
|
|
8
|
+
Both are portaled to `document.body`, which puts them outside the themed tree,
|
|
9
|
+
and they painted themselves with the harness menu fill `--dsw-specific-menu`
|
|
10
|
+
(`#f8f9fa94`, 58% alpha) relying on `backdrop-filter` to make that read solid.
|
|
11
|
+
The AryaCode desktop build does not composite that blur, so the page showed
|
|
12
|
+
through the panel. Every portaled surface now lays that fill over an opaque
|
|
13
|
+
`Canvas` base and keeps the blur only where the platform honours it.
|
|
14
|
+
|
|
15
|
+
## [0.6.1] - 2026-10-28
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **"On this PC" opens the real Windows folder dialog in the desktop app again.**
|
|
20
|
+
The desktop preload exposes the OS chooser as `__DSH_DIRECTORY_PICKER__.pick()`
|
|
21
|
+
— the same bridge the shipped native picker uses — and the flow now calls it
|
|
22
|
+
directly, so picking a folder behaves exactly like the picker it stands in for.
|
|
23
|
+
The in-app folder list is now only the browser fallback, where no such bridge
|
|
24
|
+
exists and the composed picker's `pick` would wait on a dialog that cannot open.
|
|
25
|
+
|
|
26
|
+
## [0.6.0] - 2026-10-28
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **Adding a workspace moved into the Workspaces "+" menu.** A new switch
|
|
31
|
+
(`workspaceSources`) occupies `sidebar.workspaces.directoryFlow`, which is what
|
|
32
|
+
makes the Shell's **Add workspace…** row appear at all, and stands in for the
|
|
33
|
+
folder picker with the source menu the owner asked for: **On this PC** (a folder
|
|
34
|
+
browser driven by this bundle's own `api/polish.folders` route) and **GitHub
|
|
35
|
+
repository…** (the existing dialog). A finished clone hands its path to the
|
|
36
|
+
flow's `onPicked`, so the Shell creates the workspace exactly as it does for a
|
|
37
|
+
picked folder.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- The sidebar foot now carries the GitHub sign-in state next to the balance
|
|
42
|
+
(`GitHub · <login>` / `GitHub · signed out`); clicking it opens the repository
|
|
43
|
+
dialog. It replaced the foot's add-workspace row, which the "+" menu supersedes.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- "On this PC" lists folders instead of waiting on a chooser that cannot open. A
|
|
48
|
+
composed directory picker serves one capability, `browse` or `native`, and the
|
|
49
|
+
browser build composes the one with no chooser to show, so the previous
|
|
50
|
+
`directoryPicker.pick()` call simply never settled. The flow now walks folders
|
|
51
|
+
through its own route: roots, a `..` row for going back, directories only, and
|
|
52
|
+
hidden or system entries filtered out.
|
|
53
|
+
|
|
3
54
|
## [0.5.0] - 2026-10-27
|
|
4
55
|
|
|
5
56
|
### Added
|
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: **five features shipped.** The version is the package's, and the card
|
|
11
11
|
header already shows it.
|
|
12
12
|
|
|
13
13
|
## What it is
|
|
@@ -33,7 +33,8 @@ Configure page automatically.
|
|
|
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
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 |
|
|
36
|
+
| **Add a workspace from GitHub** (`githubWorkspace`) | On | Adds the GitHub half of the workspace sources: 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. The sidebar foot carries the sign-in state (`GitHub · <login>`). See [GitHub workspaces](docs/github-workspaces.md). |
|
|
37
|
+
| **Add a workspace from this PC or GitHub** (`workspaceSources`) | On | The Workspaces **+** button gets a source menu: **On this PC** (the Host's OS chooser, or a folder list when the composition has no native chooser) and **GitHub repository…**. Off, the **+** button goes back to the shipped folder picker. |
|
|
37
38
|
|
|
38
39
|
### How the cost is computed
|
|
39
40
|
|
package/client/face.js
CHANGED
|
@@ -252,6 +252,24 @@ const PILL_STYLE = {
|
|
|
252
252
|
/** The label half of the pill, cut with an ellipsis rather than wrapped. */
|
|
253
253
|
const PILL_LABEL_STYLE = { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* The menu skin of the harness, made opaque on purpose.
|
|
257
|
+
*
|
|
258
|
+
* `--dsw-specific-menu` is a translucent fill (`#f8f9fa94`) that only reads as a
|
|
259
|
+
* solid surface because the harness pairs it with `backdrop-filter`. A plugin
|
|
260
|
+
* face has to portal its panels to `document.body`, and in the AryaCode desktop
|
|
261
|
+
* build that blur does not composite: the panel then showed the page behind it
|
|
262
|
+
* through a 58% fill. So the token stays as a tint over an opaque backdrop
|
|
263
|
+
* colour, and the blur is kept only where the platform actually honours it.
|
|
264
|
+
*/
|
|
265
|
+
const PANEL_SURFACE = {
|
|
266
|
+
backgroundColor: 'Canvas',
|
|
267
|
+
backgroundImage:
|
|
268
|
+
'linear-gradient(var(--dsw-specific-menu, rgba(248, 249, 250, 0.94)), var(--dsw-specific-menu, rgba(248, 249, 250, 0.94)))',
|
|
269
|
+
backdropFilter: 'var(--dsw-menu-backdrop-filter, none)',
|
|
270
|
+
border: '0.5px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12))',
|
|
271
|
+
}
|
|
272
|
+
|
|
255
273
|
/** The dialog surface, copied from the harness `stat-dialog.module.css` skin. */
|
|
256
274
|
const PANEL_STYLE = {
|
|
257
275
|
position: 'fixed',
|
|
@@ -261,10 +279,8 @@ const PANEL_STYLE = {
|
|
|
261
279
|
minWidth: 'min(300px, calc(100vw - 24px))',
|
|
262
280
|
maxWidth: 'min(440px, calc(100vw - 24px))',
|
|
263
281
|
padding: '16px',
|
|
264
|
-
border: 0,
|
|
265
282
|
borderRadius: 'var(--dsw-radius-lg, 12px)',
|
|
266
|
-
|
|
267
|
-
backdropFilter: 'var(--dsw-menu-backdrop-filter, none)',
|
|
283
|
+
...PANEL_SURFACE,
|
|
268
284
|
boxShadow: 'var(--dsw-elevation-prominent, 0 10px 30px rgba(0, 0, 0, 0.18))',
|
|
269
285
|
fontSize: '12px',
|
|
270
286
|
lineHeight: '18px',
|
|
@@ -466,6 +482,7 @@ function CostPill(props) {
|
|
|
466
482
|
*/
|
|
467
483
|
const BALANCE_RETRY_DELAYS = [1500, 5000, 15000, 30000]
|
|
468
484
|
|
|
485
|
+
|
|
469
486
|
/**
|
|
470
487
|
* The remaining-balance line in the sidebar foot.
|
|
471
488
|
*
|
|
@@ -627,7 +644,12 @@ const GITHUB_ROUTE = 'api/polish.github'
|
|
|
627
644
|
*/
|
|
628
645
|
const githubStore = (() => {
|
|
629
646
|
let open = false
|
|
647
|
+
// Set when the dialog was opened from the add-workspace flow: the flow owns
|
|
648
|
+
// workspace creation through `onPicked`, so a clone hands it the path rather
|
|
649
|
+
// than calling the Workspace service itself.
|
|
650
|
+
let picker
|
|
630
651
|
const listeners = new Set()
|
|
652
|
+
const publish = () => { for (const listener of listeners) listener() }
|
|
631
653
|
return {
|
|
632
654
|
isOpen: () => open,
|
|
633
655
|
subscribe(listener) {
|
|
@@ -637,7 +659,21 @@ const githubStore = (() => {
|
|
|
637
659
|
set(next) {
|
|
638
660
|
if (open === next) return
|
|
639
661
|
open = next
|
|
640
|
-
|
|
662
|
+
if (!next) picker = undefined
|
|
663
|
+
publish()
|
|
664
|
+
},
|
|
665
|
+
/** Open the dialog, optionally on behalf of the add-workspace flow. */
|
|
666
|
+
open(onPicked) {
|
|
667
|
+
picker = onPicked
|
|
668
|
+
if (open) return
|
|
669
|
+
open = true
|
|
670
|
+
publish()
|
|
671
|
+
},
|
|
672
|
+
/** Take the flow's `onPicked` once, so a second clone cannot reuse it. */
|
|
673
|
+
takePicker() {
|
|
674
|
+
const current = picker
|
|
675
|
+
picker = undefined
|
|
676
|
+
return current
|
|
641
677
|
},
|
|
642
678
|
}
|
|
643
679
|
})()
|
|
@@ -662,6 +698,28 @@ async function githubCall(action, extra) {
|
|
|
662
698
|
return payload.value
|
|
663
699
|
}
|
|
664
700
|
|
|
701
|
+
/** The Host route the folder browser talks over. Document-relative, like every app route. */
|
|
702
|
+
const FOLDERS_ROUTE = 'api/polish.folders'
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Post one action to the folder route.
|
|
706
|
+
* @param body - `{ action: 'roots' }` or `{ action: 'ls', path }`.
|
|
707
|
+
* @returns the route's listing, or throws with its own message.
|
|
708
|
+
*/
|
|
709
|
+
async function foldersCall(body) {
|
|
710
|
+
const response = await fetch(FOLDERS_ROUTE, {
|
|
711
|
+
method: 'POST',
|
|
712
|
+
headers: { 'content-type': 'application/json' },
|
|
713
|
+
body: JSON.stringify(body),
|
|
714
|
+
})
|
|
715
|
+
const payload = await response.json().catch(() => undefined)
|
|
716
|
+
if (payload === undefined || typeof payload !== 'object') {
|
|
717
|
+
throw new Error(`the Host answered ${String(response.status)}`)
|
|
718
|
+
}
|
|
719
|
+
if (payload.error !== undefined) throw new Error(String(payload.error))
|
|
720
|
+
return payload
|
|
721
|
+
}
|
|
722
|
+
|
|
665
723
|
/** Modal backdrop and panel skin, matching the Shell's own dialogs. */
|
|
666
724
|
const GITHUB_BACKDROP_STYLE = {
|
|
667
725
|
position: 'fixed',
|
|
@@ -785,7 +843,15 @@ function GitHubPanel(props) {
|
|
|
785
843
|
setError('')
|
|
786
844
|
Promise.resolve()
|
|
787
845
|
.then(() => githubCall('clone', { owner: repo.owner, name: repo.name }))
|
|
788
|
-
|
|
846
|
+
// The add-workspace flow owns creation when it opened this dialog; a
|
|
847
|
+
// dialog opened from the footer creates the workspace itself.
|
|
848
|
+
.then(result => {
|
|
849
|
+
const onPicked = githubStore.takePicker()
|
|
850
|
+
const land = onPicked === undefined
|
|
851
|
+
? Promise.resolve(props.createWorkspace(result.path))
|
|
852
|
+
: Promise.resolve(onPicked(result.path))
|
|
853
|
+
return land.then(() => result)
|
|
854
|
+
})
|
|
789
855
|
.then(result => {
|
|
790
856
|
setBusy('')
|
|
791
857
|
githubStore.set(false)
|
|
@@ -862,59 +928,326 @@ function GitHubPanel(props) {
|
|
|
862
928
|
, document.body)
|
|
863
929
|
}
|
|
864
930
|
|
|
931
|
+
/** The compact footer-row skin shared by the status line. */
|
|
932
|
+
const FOOT_ROW_STYLE = {
|
|
933
|
+
display: 'flex',
|
|
934
|
+
alignItems: 'center',
|
|
935
|
+
gap: '6px',
|
|
936
|
+
maxWidth: '100%',
|
|
937
|
+
minWidth: 0,
|
|
938
|
+
padding: '2px 6px',
|
|
939
|
+
border: 'none',
|
|
940
|
+
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
941
|
+
background: 'transparent',
|
|
942
|
+
color: 'var(--dsw-alias-label-tertiary, inherit)',
|
|
943
|
+
font: 'inherit',
|
|
944
|
+
fontSize: '12px',
|
|
945
|
+
lineHeight: '18px',
|
|
946
|
+
fontVariantNumeric: 'tabular-nums',
|
|
947
|
+
whiteSpace: 'nowrap',
|
|
948
|
+
overflow: 'hidden',
|
|
949
|
+
textOverflow: 'ellipsis',
|
|
950
|
+
cursor: 'pointer',
|
|
951
|
+
}
|
|
952
|
+
|
|
865
953
|
/**
|
|
866
|
-
* The footer
|
|
954
|
+
* The footer status line: which GitHub session the "+" menu will clone with.
|
|
955
|
+
*
|
|
956
|
+
* It reads the same `status` action the dialog does, so it costs one request
|
|
957
|
+
* when the Shell loads and one whenever the window regains focus.
|
|
958
|
+
*
|
|
867
959
|
* @param props - our lazy `configForms`.
|
|
868
960
|
* @returns the row, or null when the feature is switched off.
|
|
869
961
|
*/
|
|
870
|
-
function
|
|
962
|
+
function GithubStatus(props) {
|
|
871
963
|
const enabled = useSwitch(props.configForms, 'githubWorkspace', true)
|
|
964
|
+
const [status, setStatus] = React.useState(undefined)
|
|
965
|
+
React.useEffect(() => {
|
|
966
|
+
if (!enabled) return undefined
|
|
967
|
+
let live = true
|
|
968
|
+
const read = () => {
|
|
969
|
+
Promise.resolve()
|
|
970
|
+
.then(() => githubCall('status'))
|
|
971
|
+
.then(next => { if (live) setStatus(next) })
|
|
972
|
+
.catch(() => { if (live) setStatus({ signedIn: false }) })
|
|
973
|
+
}
|
|
974
|
+
read()
|
|
975
|
+
const onFocus = () => { read() }
|
|
976
|
+
window.addEventListener('focus', onFocus)
|
|
977
|
+
return () => {
|
|
978
|
+
live = false
|
|
979
|
+
window.removeEventListener('focus', onFocus)
|
|
980
|
+
}
|
|
981
|
+
}, [enabled])
|
|
872
982
|
if (!enabled) return null
|
|
873
|
-
|
|
983
|
+
const signedIn = status !== undefined && status.signedIn === true
|
|
984
|
+
const label = status === undefined
|
|
985
|
+
? 'GitHub · checking…'
|
|
986
|
+
: signedIn ? `GitHub · ${String(status.login)}` : 'GitHub · signed out'
|
|
987
|
+
return React.createElement('div', { style: { padding: '2px 8px 0', minWidth: 0 } },
|
|
874
988
|
React.createElement('button', {
|
|
875
989
|
type: 'button',
|
|
876
|
-
'data-polish-github': '
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
990
|
+
'data-polish-github': 'status',
|
|
991
|
+
'data-polish-github-state': signedIn ? 'in' : status === undefined ? 'checking' : 'out',
|
|
992
|
+
title: signedIn
|
|
993
|
+
? 'Add a workspace from GitHub'
|
|
994
|
+
: 'Sign in with the GitHub CLI (gh auth login), then add a repository as a workspace',
|
|
995
|
+
'aria-label': label,
|
|
996
|
+
onClick: () => githubStore.open(),
|
|
997
|
+
style: FOOT_ROW_STYLE,
|
|
998
|
+
onMouseEnter: event => { event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
|
|
999
|
+
onMouseLeave: event => { event.currentTarget.style.background = 'transparent' },
|
|
1000
|
+
}, label))
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/** One row of the add-workspace source menu. */
|
|
1004
|
+
const SOURCE_ROW_STYLE = {
|
|
1005
|
+
display: 'flex',
|
|
1006
|
+
flexDirection: 'column',
|
|
1007
|
+
alignItems: 'flex-start',
|
|
1008
|
+
gap: '2px',
|
|
1009
|
+
width: '100%',
|
|
1010
|
+
padding: '8px 10px',
|
|
1011
|
+
border: 'none',
|
|
1012
|
+
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
1013
|
+
background: 'transparent',
|
|
1014
|
+
color: 'inherit',
|
|
1015
|
+
font: 'inherit',
|
|
1016
|
+
textAlign: 'left',
|
|
1017
|
+
cursor: 'pointer',
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* The "+" add-workspace flow: choose where the workspace comes from.
|
|
1022
|
+
*
|
|
1023
|
+
* The Harness shows its own `Add workspace…` row only while this slot is
|
|
1024
|
+
* occupied, and a single-owner slot means this dialog stands in for the folder
|
|
1025
|
+
* picker. It therefore offers both sources: the machine (the Host's own OS
|
|
1026
|
+
* chooser when the composition serves the `native` capability, otherwise a
|
|
1027
|
+
* folder list driven by the browse primitives) and GitHub.
|
|
1028
|
+
*
|
|
1029
|
+
* @param props - the slot owner's `open`/`busy`/`onPicked`/`onCancel`/`onError`,
|
|
1030
|
+
* our lazy `configForms`, and the directory-picker calls.
|
|
1031
|
+
* @returns the flow, or null while it is closed or switched off.
|
|
1032
|
+
*/
|
|
1033
|
+
function WorkspaceSourceFlow(props) {
|
|
1034
|
+
const enabled = useSwitch(props.configForms, 'workspaceSources', true)
|
|
1035
|
+
const [step, setStep] = React.useState('menu')
|
|
1036
|
+
const [listing, setListing] = React.useState(undefined)
|
|
1037
|
+
const [error, setError] = React.useState('')
|
|
1038
|
+
const [busy, setBusy] = React.useState(false)
|
|
1039
|
+
|
|
1040
|
+
const cancel = () => {
|
|
1041
|
+
if (typeof props.onCancel === 'function') props.onCancel()
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Escape closes the flow. Declared with the other hooks, before the early
|
|
1045
|
+
// return, so the hook order stays fixed while the flow opens and closes.
|
|
1046
|
+
React.useEffect(() => {
|
|
1047
|
+
if (!enabled || props.open !== true) return undefined
|
|
1048
|
+
const onKey = event => { if (event.key === 'Escape') cancel() }
|
|
1049
|
+
document.addEventListener('keydown', onKey)
|
|
1050
|
+
return () => document.removeEventListener('keydown', onKey)
|
|
1051
|
+
}, [enabled, props.open])
|
|
1052
|
+
|
|
1053
|
+
if (!enabled || props.open !== true) return null
|
|
1054
|
+
|
|
1055
|
+
const fail = message => {
|
|
1056
|
+
setBusy(false)
|
|
1057
|
+
setError(message)
|
|
1058
|
+
}
|
|
1059
|
+
const messageOf = reason => (reason instanceof Error ? reason.message : String(reason))
|
|
1060
|
+
|
|
1061
|
+
const land = path => {
|
|
1062
|
+
setBusy(true)
|
|
1063
|
+
setError('')
|
|
1064
|
+
Promise.resolve()
|
|
1065
|
+
.then(() => props.onPicked(path))
|
|
1066
|
+
.catch(reason => fail(messageOf(reason)))
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const browse = path => {
|
|
1070
|
+
setBusy(true)
|
|
1071
|
+
setError('')
|
|
1072
|
+
Promise.resolve()
|
|
1073
|
+
.then(() => foldersCall({ action: 'ls', path }))
|
|
1074
|
+
.then(level => {
|
|
1075
|
+
setListing(level)
|
|
1076
|
+
setBusy(false)
|
|
1077
|
+
setStep('browse')
|
|
1078
|
+
})
|
|
1079
|
+
.catch(reason => fail(messageOf(reason)))
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Hand folder choice to the Host.
|
|
1084
|
+
*
|
|
1085
|
+
* In the desktop app the preload exposes the real OS chooser as
|
|
1086
|
+
* `__DSH_DIRECTORY_PICKER__.pick()` — the same bridge the shipped native
|
|
1087
|
+
* picker uses — so the flow opens the Windows folder dialog and behaves like
|
|
1088
|
+
* the stock picker it stands in for. In a browser there is no such bridge, and
|
|
1089
|
+
* the composed picker's `pick` waits on a dialog that cannot open, so the flow
|
|
1090
|
+
* lists folders itself instead of hanging.
|
|
1091
|
+
*/
|
|
1092
|
+
const chooseOnPc = () => {
|
|
1093
|
+
if (busy) return
|
|
1094
|
+
const bridge = typeof globalThis === 'undefined' ? undefined : globalThis.__DSH_DIRECTORY_PICKER__
|
|
1095
|
+
if (bridge !== undefined && typeof bridge.pick === 'function') {
|
|
1096
|
+
setBusy(true)
|
|
1097
|
+
setError('')
|
|
1098
|
+
Promise.resolve()
|
|
1099
|
+
.then(() => bridge.pick())
|
|
1100
|
+
.then(path => {
|
|
1101
|
+
if (typeof path === 'string' && path !== '') {
|
|
1102
|
+
land(path)
|
|
1103
|
+
return
|
|
1104
|
+
}
|
|
1105
|
+
setBusy(false)
|
|
1106
|
+
})
|
|
1107
|
+
.catch(reason => fail(messageOf(reason)))
|
|
1108
|
+
return
|
|
1109
|
+
}
|
|
1110
|
+
setBusy(true)
|
|
1111
|
+
setError('')
|
|
1112
|
+
Promise.resolve()
|
|
1113
|
+
.then(() => foldersCall({ action: 'roots' }))
|
|
1114
|
+
.then(payload => {
|
|
1115
|
+
const roots = Array.isArray(payload.roots) ? payload.roots : []
|
|
1116
|
+
setListing({
|
|
1117
|
+
path: '',
|
|
1118
|
+
parent: null,
|
|
1119
|
+
entries: roots.map(root => ({ name: root.label, path: root.path })),
|
|
1120
|
+
})
|
|
1121
|
+
setBusy(false)
|
|
1122
|
+
setStep('browse')
|
|
1123
|
+
})
|
|
1124
|
+
.catch(reason => fail(messageOf(reason)))
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const openGithub = () => {
|
|
1128
|
+
githubStore.open(path => props.onPicked(path))
|
|
1129
|
+
cancel()
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const rows = step === 'menu'
|
|
1133
|
+
? [
|
|
1134
|
+
React.createElement('button', {
|
|
1135
|
+
key: 'pc',
|
|
1136
|
+
type: 'button',
|
|
1137
|
+
'data-polish-source': 'machine',
|
|
1138
|
+
disabled: busy,
|
|
1139
|
+
onClick: chooseOnPc,
|
|
1140
|
+
style: SOURCE_ROW_STYLE,
|
|
1141
|
+
onMouseEnter: event => { event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
|
|
1142
|
+
onMouseLeave: event => { event.currentTarget.style.background = 'transparent' },
|
|
1143
|
+
},
|
|
1144
|
+
React.createElement('span', { style: { fontWeight: 500 } }, 'On this PC'),
|
|
1145
|
+
React.createElement('span', { style: { opacity: 0.7 } }, 'Choose a folder on this machine')),
|
|
1146
|
+
React.createElement('button', {
|
|
1147
|
+
key: 'github',
|
|
1148
|
+
type: 'button',
|
|
1149
|
+
'data-polish-source': 'github',
|
|
1150
|
+
disabled: busy,
|
|
1151
|
+
onClick: openGithub,
|
|
1152
|
+
style: SOURCE_ROW_STYLE,
|
|
1153
|
+
onMouseEnter: event => { event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
|
|
1154
|
+
onMouseLeave: event => { event.currentTarget.style.background = 'transparent' },
|
|
1155
|
+
},
|
|
1156
|
+
React.createElement('span', { style: { fontWeight: 500 } }, 'GitHub repository…'),
|
|
1157
|
+
React.createElement('span', { style: { opacity: 0.7 } }, 'Clone one of your repositories as a workspace')),
|
|
1158
|
+
]
|
|
1159
|
+
: [
|
|
1160
|
+
React.createElement('div', {
|
|
1161
|
+
key: 'head',
|
|
1162
|
+
style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '2px 4px 6px', minWidth: 0 },
|
|
1163
|
+
},
|
|
1164
|
+
React.createElement('span', {
|
|
1165
|
+
style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
1166
|
+
title: listing !== undefined ? listing.path : '',
|
|
1167
|
+
}, listing === undefined || listing.path === '' ? 'This PC' : listing.path),
|
|
1168
|
+
listing === undefined || listing.path === ''
|
|
1169
|
+
? null
|
|
1170
|
+
: React.createElement('button', {
|
|
1171
|
+
type: 'button',
|
|
1172
|
+
'data-polish-pick': 'here',
|
|
1173
|
+
disabled: busy,
|
|
1174
|
+
onClick: () => { if (listing !== undefined) land(listing.path) },
|
|
1175
|
+
style: { ...FOOT_ROW_STYLE, color: 'var(--dsw-alias-label-primary, inherit)', background: 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
|
|
1176
|
+
}, 'Use this folder')),
|
|
1177
|
+
...(listing === undefined || listing.parent === null || listing.parent === undefined
|
|
1178
|
+
? []
|
|
1179
|
+
: [React.createElement('button', {
|
|
1180
|
+
key: 'up',
|
|
1181
|
+
type: 'button',
|
|
1182
|
+
'data-polish-folder': '..',
|
|
1183
|
+
disabled: busy,
|
|
1184
|
+
onClick: () => browse(listing.parent),
|
|
1185
|
+
style: SOURCE_ROW_STYLE,
|
|
1186
|
+
onMouseEnter: event => { event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
|
|
1187
|
+
onMouseLeave: event => { event.currentTarget.style.background = 'transparent' },
|
|
1188
|
+
}, React.createElement('span', { title: listing.parent }, '..'))]),
|
|
1189
|
+
...(listing === undefined
|
|
1190
|
+
? []
|
|
1191
|
+
: listing.entries.map(entry => React.createElement('button', {
|
|
1192
|
+
key: entry.path,
|
|
1193
|
+
type: 'button',
|
|
1194
|
+
'data-polish-folder': entry.name,
|
|
1195
|
+
disabled: busy,
|
|
1196
|
+
onClick: () => browse(entry.path),
|
|
1197
|
+
style: SOURCE_ROW_STYLE,
|
|
1198
|
+
onMouseEnter: event => { event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
|
|
1199
|
+
onMouseLeave: event => { event.currentTarget.style.background = 'transparent' },
|
|
1200
|
+
}, React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: entry.path }, entry.name)))),
|
|
1201
|
+
]
|
|
1202
|
+
|
|
1203
|
+
// The flow is rendered inside the Shell's add-workspace popover, which is
|
|
1204
|
+
// narrow and sits under the sidebar's own stacking order. A portal to the body
|
|
1205
|
+
// puts it above everything and keeps the rows readable at their own width.
|
|
1206
|
+
return ReactDOM.createPortal(
|
|
1207
|
+
React.createElement('div', {
|
|
1208
|
+
style: GITHUB_BACKDROP_STYLE,
|
|
1209
|
+
'data-polish-flow-backdrop': step,
|
|
1210
|
+
onPointerDown: event => { if (event.target === event.currentTarget) cancel() },
|
|
1211
|
+
},
|
|
1212
|
+
React.createElement('div', {
|
|
1213
|
+
'data-polish-flow': step,
|
|
1214
|
+
role: 'dialog',
|
|
1215
|
+
'aria-label': 'Add a workspace',
|
|
880
1216
|
style: {
|
|
881
1217
|
display: 'flex',
|
|
882
|
-
|
|
883
|
-
gap: '
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
padding: '
|
|
887
|
-
|
|
888
|
-
borderRadius: 'var(--dsw-radius-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
font: 'inherit',
|
|
1218
|
+
flexDirection: 'column',
|
|
1219
|
+
gap: '2px',
|
|
1220
|
+
width: 'min(360px, calc(100vw - 32px))',
|
|
1221
|
+
maxHeight: 'min(420px, calc(100vh - 96px))',
|
|
1222
|
+
padding: '6px',
|
|
1223
|
+
overflowY: 'auto',
|
|
1224
|
+
borderRadius: 'var(--dsw-radius-lg, 12px)',
|
|
1225
|
+
...PANEL_SURFACE,
|
|
1226
|
+
boxShadow: 'var(--dsw-elevation-prominent, 0 8px 24px rgba(0,0,0,0.18))',
|
|
892
1227
|
fontSize: '12px',
|
|
893
1228
|
lineHeight: '18px',
|
|
894
|
-
|
|
895
|
-
whiteSpace: 'nowrap',
|
|
896
|
-
overflow: 'hidden',
|
|
897
|
-
textOverflow: 'ellipsis',
|
|
898
|
-
cursor: 'pointer',
|
|
1229
|
+
color: 'var(--dsw-alias-label-secondary, inherit)',
|
|
899
1230
|
},
|
|
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' },
|
|
902
|
-
},
|
|
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
1231
|
},
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1232
|
+
error !== ''
|
|
1233
|
+
? React.createElement('div', { style: { padding: '4px 6px', color: 'var(--dsw-alias-label-error, #b42318)' } }, error)
|
|
1234
|
+
: null,
|
|
1235
|
+
...rows,
|
|
1236
|
+
step === 'menu'
|
|
1237
|
+
? null
|
|
1238
|
+
: React.createElement('button', {
|
|
1239
|
+
type: 'button',
|
|
1240
|
+
'data-polish-pick': 'back',
|
|
1241
|
+
onClick: () => { setStep('menu'); setListing(undefined); setError('') },
|
|
1242
|
+
style: { ...FOOT_ROW_STYLE, justifyContent: 'flex-start' },
|
|
1243
|
+
}, 'Back'),
|
|
1244
|
+
React.createElement('button', {
|
|
1245
|
+
type: 'button',
|
|
1246
|
+
'data-polish-pick': 'cancel',
|
|
1247
|
+
onClick: cancel,
|
|
1248
|
+
style: { ...FOOT_ROW_STYLE, justifyContent: 'flex-start' },
|
|
1249
|
+
}, 'Cancel'))),
|
|
1250
|
+
document.body)
|
|
918
1251
|
}
|
|
919
1252
|
|
|
920
1253
|
return {
|
|
@@ -995,12 +1328,29 @@ return {
|
|
|
995
1328
|
id: 'polish-github',
|
|
996
1329
|
inject: () => ({ configForms, createWorkspace }),
|
|
997
1330
|
}, GitHubPanel))
|
|
1331
|
+
// The footer keeps the sign-in state, not an add button: adding a
|
|
1332
|
+
// workspace belongs in the "+" menu, where the Shell says it does.
|
|
998
1333
|
githubCtx.slots.inject('sidebar.footer.action', () => githubCtx.slots.register({
|
|
999
1334
|
name: 'sidebar.footer.action',
|
|
1000
|
-
id: 'polish-github-
|
|
1335
|
+
id: 'polish-github-status',
|
|
1001
1336
|
order: 4,
|
|
1002
1337
|
inject: () => ({ configForms }),
|
|
1003
|
-
},
|
|
1338
|
+
}, GithubStatus))
|
|
1004
1339
|
})
|
|
1340
|
+
// The "+" menu's `Add workspace…` row exists only while a plugin occupies
|
|
1341
|
+
// this single-owner hole, so occupying it means standing in for the folder
|
|
1342
|
+
// picker: the flow offers the machine and GitHub as sources. It needs no
|
|
1343
|
+
// Remote injection: the folder list and the clone both come from this
|
|
1344
|
+
// bundle's own Host routes, which is what makes the flow behave the same in
|
|
1345
|
+
// the browser and in the app.
|
|
1346
|
+
ctx.slots.inject('sidebar.workspaces.directoryFlow', () => ctx.slots.register({
|
|
1347
|
+
name: 'sidebar.workspaces.directoryFlow',
|
|
1348
|
+
// A single slot clashes only at the exact priority; another priority
|
|
1349
|
+
// shadows it and the LOWEST priority renders. The shipped pickers
|
|
1350
|
+
// register at 0, so -1 is what puts this flow in front of them without
|
|
1351
|
+
// making their own activation throw.
|
|
1352
|
+
priority: -1,
|
|
1353
|
+
inject: () => ({ configForms }),
|
|
1354
|
+
}, WorkspaceSourceFlow))
|
|
1005
1355
|
},
|
|
1006
1356
|
}
|
package/client/switches.json
CHANGED
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
"label": "Add a workspace from GitHub",
|
|
25
25
|
"description": "Sign in with the GitHub CLI, browse your repositories, and clone one as a workspace.",
|
|
26
26
|
"defaultOn": true
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"key": "workspaceSources",
|
|
30
|
+
"label": "Add a workspace from this PC or GitHub",
|
|
31
|
+
"description": "Put an On this PC / GitHub repository source menu behind the Workspaces + button.",
|
|
32
|
+
"defaultOn": true
|
|
27
33
|
}
|
|
28
34
|
]
|
|
29
35
|
}
|
|
@@ -31,16 +31,37 @@ server rejects a bearer token for clone, which is why the header carries
|
|
|
31
31
|
`x-access-token` basic auth. Every line of git output is scrubbed of the token
|
|
32
32
|
before it can reach the browser, and the route answers `cache-control: no-store`.
|
|
33
33
|
|
|
34
|
-
##
|
|
35
|
-
|
|
36
|
-
The "+" menu's **Add workspace
|
|
37
|
-
`sidebar.workspaces.directoryFlow`, and that slot is `kind: 'single'
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
## How the entry point works
|
|
35
|
+
|
|
36
|
+
The "+" menu's **Add workspace…** entry only appears when something occupies
|
|
37
|
+
`sidebar.workspaces.directoryFlow`, and that slot is `kind: 'single'` — whoever
|
|
38
|
+
takes it stands in for the folder picker. This bundle takes it and offers the
|
|
39
|
+
two sources the owner asked for:
|
|
40
|
+
|
|
41
|
+
- **On this PC** — the OS folder dialog. The desktop preload exposes it as
|
|
42
|
+
`window.__DSH_DIRECTORY_PICKER__.pick()`, the same bridge the shipped native
|
|
43
|
+
picker uses, so the flow calls it directly and behaves exactly like the picker
|
|
44
|
+
it stands in for. Where that bridge is absent (the web build) the flow falls
|
|
45
|
+
back to a folder browser driven by this bundle's own route
|
|
46
|
+
(`api/polish.folders`, actions `roots` and `ls`): roots, a `..` row for going
|
|
47
|
+
back, directories only, hidden and system entries filtered out, and **Use this
|
|
48
|
+
folder** hands the current path to the flow's `onPicked`.
|
|
49
|
+
- **GitHub repository…** — the dialog described above. It opens on behalf of the
|
|
50
|
+
flow, so a finished clone hands its path back through the flow's `onPicked`,
|
|
51
|
+
and the Shell creates the workspace exactly as it does for a picked folder.
|
|
52
|
+
|
|
53
|
+
### Why the fallback exists at all
|
|
54
|
+
|
|
55
|
+
A composed directory picker serves exactly one capability: `browse` answers
|
|
56
|
+
`list` and `createDirectory`, `native` answers `pick`. The web profile composes
|
|
57
|
+
the picker whose chooser cannot open there, so `directoryPicker.pick()` waits on
|
|
58
|
+
a dialog that never appears — an earlier build of this flow did exactly that and
|
|
59
|
+
the row spun forever. The preload bridge is the reliable door in the app; the
|
|
60
|
+
listing is what a browser gets instead of a spinner.
|
|
61
|
+
|
|
62
|
+
The tray of the foot keeps the sign-in state (`GitHub · <login>`, or
|
|
63
|
+
`GitHub · signed out`) next to the balance, so the account is visible without
|
|
64
|
+
opening a dialog; clicking it opens the repository dialog directly.
|
|
44
65
|
|
|
45
66
|
## Not used from the AryaAI-era plugin
|
|
46
67
|
|