@aryacode/dsh-aryacode-polish 0.4.4 → 0.6.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/client/face.js CHANGED
@@ -466,6 +466,7 @@ function CostPill(props) {
466
466
  */
467
467
  const BALANCE_RETRY_DELAYS = [1500, 5000, 15000, 30000]
468
468
 
469
+
469
470
  /**
470
471
  * The remaining-balance line in the sidebar foot.
471
472
  *
@@ -530,22 +531,24 @@ function BalanceLine(props) {
530
531
  return React.createElement('div', { 'data-polish-balance': 'empty', style: { display: 'none' } })
531
532
  }
532
533
  const money = formatBalance(wallet)
534
+ // The foot is a single row shared with the GitHub entry, so the amount stays
535
+ // bare: a tooltip names it, and nothing here competes for width.
533
536
  return React.createElement('div', {
534
537
  title: 'Remaining account balance',
538
+ 'data-polish-balance': 'ready',
535
539
  style: {
536
540
  display: 'flex',
537
541
  alignItems: 'center',
538
542
  gap: '6px',
539
- padding: '2px 8px 4px',
543
+ padding: '2px 4px 4px 8px',
540
544
  fontSize: '12px',
541
545
  lineHeight: '16px',
542
- minWidth: 0,
543
- overflow: 'hidden',
546
+ flex: '0 0 auto',
544
547
  whiteSpace: 'nowrap',
545
- textOverflow: 'ellipsis',
546
548
  opacity: 0.75,
549
+ fontVariantNumeric: 'tabular-nums',
547
550
  },
548
- }, props.wide === false ? money : 'Balance ' + money)
551
+ }, money)
549
552
  }
550
553
 
551
554
  /**
@@ -616,6 +619,604 @@ function OpenFolderMenuItem(props) {
616
619
  return React.createElement('button', { type: 'button', role: 'menuitem', onClick: onSelect }, label)
617
620
  }
618
621
 
622
+ /** The Host route the GitHub halves talk over. Document-relative, like every app route. */
623
+ const GITHUB_ROUTE = 'api/polish.github'
624
+
625
+ /**
626
+ * Whether the GitHub panel is open. One store for the whole bundle: the footer
627
+ * row opens it, and the overlay panel is the only reader.
628
+ */
629
+ const githubStore = (() => {
630
+ let open = false
631
+ // Set when the dialog was opened from the add-workspace flow: the flow owns
632
+ // workspace creation through `onPicked`, so a clone hands it the path rather
633
+ // than calling the Workspace service itself.
634
+ let picker
635
+ const listeners = new Set()
636
+ const publish = () => { for (const listener of listeners) listener() }
637
+ return {
638
+ isOpen: () => open,
639
+ subscribe(listener) {
640
+ listeners.add(listener)
641
+ return () => { listeners.delete(listener) }
642
+ },
643
+ set(next) {
644
+ if (open === next) return
645
+ open = next
646
+ if (!next) picker = undefined
647
+ publish()
648
+ },
649
+ /** Open the dialog, optionally on behalf of the add-workspace flow. */
650
+ open(onPicked) {
651
+ picker = onPicked
652
+ if (open) return
653
+ open = true
654
+ publish()
655
+ },
656
+ /** Take the flow's `onPicked` once, so a second clone cannot reuse it. */
657
+ takePicker() {
658
+ const current = picker
659
+ picker = undefined
660
+ return current
661
+ },
662
+ }
663
+ })()
664
+
665
+ /**
666
+ * Post one action to the GitHub route.
667
+ * @param action - `status`, `repos` or `clone`.
668
+ * @param extra - fields the action needs.
669
+ * @returns the route's `value`, or throws with its own message.
670
+ */
671
+ async function githubCall(action, extra) {
672
+ const response = await fetch(GITHUB_ROUTE, {
673
+ method: 'POST',
674
+ headers: { 'content-type': 'application/json' },
675
+ body: JSON.stringify({ action, ...(extra ?? {}) }),
676
+ })
677
+ const payload = await response.json().catch(() => undefined)
678
+ if (payload === undefined || typeof payload !== 'object') {
679
+ throw new Error(`the Host answered ${String(response.status)}`)
680
+ }
681
+ if (payload.ok !== true) throw new Error(payload.error || 'GitHub request failed')
682
+ return payload.value
683
+ }
684
+
685
+ /** The Host route the folder browser talks over. Document-relative, like every app route. */
686
+ const FOLDERS_ROUTE = 'api/polish.folders'
687
+
688
+ /**
689
+ * Post one action to the folder route.
690
+ * @param body - `{ action: 'roots' }` or `{ action: 'ls', path }`.
691
+ * @returns the route's listing, or throws with its own message.
692
+ */
693
+ async function foldersCall(body) {
694
+ const response = await fetch(FOLDERS_ROUTE, {
695
+ method: 'POST',
696
+ headers: { 'content-type': 'application/json' },
697
+ body: JSON.stringify(body),
698
+ })
699
+ const payload = await response.json().catch(() => undefined)
700
+ if (payload === undefined || typeof payload !== 'object') {
701
+ throw new Error(`the Host answered ${String(response.status)}`)
702
+ }
703
+ if (payload.error !== undefined) throw new Error(String(payload.error))
704
+ return payload
705
+ }
706
+
707
+ /** Modal backdrop and panel skin, matching the Shell's own dialogs. */
708
+ const GITHUB_BACKDROP_STYLE = {
709
+ position: 'fixed',
710
+ inset: 0,
711
+ zIndex: 1200,
712
+ display: 'flex',
713
+ alignItems: 'center',
714
+ justifyContent: 'center',
715
+ background: 'rgba(16, 18, 22, 0.32)',
716
+ }
717
+
718
+ const GITHUB_CARD_STYLE = {
719
+ ...PANEL_STYLE,
720
+ position: 'static',
721
+ width: 'min(560px, calc(100vw - 32px))',
722
+ maxHeight: 'min(560px, calc(100vh - 64px))',
723
+ display: 'flex',
724
+ flexDirection: 'column',
725
+ overflow: 'hidden',
726
+ }
727
+
728
+ const GITHUB_LIST_STYLE = {
729
+ marginTop: '10px',
730
+ overflowY: 'auto',
731
+ borderTop: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.12))',
732
+ }
733
+
734
+ const GITHUB_ROW_STYLE = {
735
+ display: 'flex',
736
+ alignItems: 'center',
737
+ gap: '8px',
738
+ width: '100%',
739
+ padding: '8px 4px',
740
+ border: 'none',
741
+ background: 'transparent',
742
+ color: 'inherit',
743
+ font: 'inherit',
744
+ textAlign: 'left',
745
+ cursor: 'pointer',
746
+ borderBottom: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.08))',
747
+ }
748
+
749
+ const GITHUB_INPUT_STYLE = {
750
+ width: '100%',
751
+ boxSizing: 'border-box',
752
+ padding: '6px 8px',
753
+ font: 'inherit',
754
+ color: 'inherit',
755
+ background: 'transparent',
756
+ border: '0.5px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.2))',
757
+ borderRadius: 'var(--dsw-radius-sm, 6px)',
758
+ }
759
+
760
+ /**
761
+ * The add-a-workspace-from-GitHub dialog.
762
+ *
763
+ * Sign-in is the GitHub CLI's own session: the Host asks `gh` for the token it
764
+ * already holds, so this dialog never collects a credential. A clone becomes a
765
+ * workspace through the Workspace Remote, which is the same call the "+" menu
766
+ * makes after its folder picker returns a path.
767
+ *
768
+ * @param props - our lazy `configForms` and the `createWorkspace` opener.
769
+ * @returns the dialog, or null while it is closed or switched off.
770
+ */
771
+ function GitHubPanel(props) {
772
+ const open = React.useSyncExternalStore(githubStore.subscribe, githubStore.isOpen)
773
+ const enabled = useSwitch(props.configForms, 'githubWorkspace', true)
774
+ const [phase, setPhase] = React.useState('idle')
775
+ const [account, setAccount] = React.useState(undefined)
776
+ const [repos, setRepos] = React.useState([])
777
+ const [query, setQuery] = React.useState('')
778
+ const [error, setError] = React.useState('')
779
+ const [busy, setBusy] = React.useState('')
780
+
781
+ React.useEffect(() => {
782
+ if (!open || !enabled) return undefined
783
+ let live = true
784
+ setPhase('loading')
785
+ setError('')
786
+ setBusy('')
787
+ setQuery('')
788
+ Promise.resolve()
789
+ .then(() => githubCall('status'))
790
+ .then(status => {
791
+ if (!live) return undefined
792
+ setAccount(status)
793
+ if (status.signedIn !== true) return undefined
794
+ return githubCall('repos').then(list => {
795
+ if (!live) return
796
+ setRepos(Array.isArray(list.repos) ? list.repos : [])
797
+ })
798
+ })
799
+ .then(() => { if (live) setPhase('ready') })
800
+ .catch(reason => {
801
+ if (!live) return
802
+ setError(reason instanceof Error ? reason.message : String(reason))
803
+ setPhase('ready')
804
+ })
805
+ return () => { live = false }
806
+ }, [open, enabled])
807
+
808
+ React.useEffect(() => {
809
+ if (!open) return undefined
810
+ const onKey = event => { if (event.key === 'Escape') githubStore.set(false) }
811
+ document.addEventListener('keydown', onKey)
812
+ return () => document.removeEventListener('keydown', onKey)
813
+ }, [open])
814
+
815
+ if (!open || !enabled) return null
816
+
817
+ const signedIn = account !== undefined && account.signedIn === true
818
+ const needle = query.trim().toLowerCase()
819
+ const visible = needle === ''
820
+ ? repos
821
+ : repos.filter(repo => `${repo.fullName} ${repo.description}`.toLowerCase().includes(needle))
822
+ const root = account !== undefined && typeof account.root === 'string' ? account.root : ''
823
+
824
+ const clone = repo => {
825
+ if (busy !== '') return
826
+ setBusy(repo.fullName)
827
+ setError('')
828
+ Promise.resolve()
829
+ .then(() => githubCall('clone', { owner: repo.owner, name: repo.name }))
830
+ // The add-workspace flow owns creation when it opened this dialog; a
831
+ // dialog opened from the footer creates the workspace itself.
832
+ .then(result => {
833
+ const onPicked = githubStore.takePicker()
834
+ const land = onPicked === undefined
835
+ ? Promise.resolve(props.createWorkspace(result.path))
836
+ : Promise.resolve(onPicked(result.path))
837
+ return land.then(() => result)
838
+ })
839
+ .then(result => {
840
+ setBusy('')
841
+ githubStore.set(false)
842
+ if (typeof console !== 'undefined' && typeof console.info === 'function') {
843
+ console.info('[aryacode-polish] workspace ready:', result.path)
844
+ }
845
+ })
846
+ .catch(reason => {
847
+ setBusy('')
848
+ setError(reason instanceof Error ? reason.message : String(reason))
849
+ })
850
+ }
851
+
852
+ const rows = visible.slice(0, 200).map(repo => React.createElement('button', {
853
+ key: repo.fullName,
854
+ type: 'button',
855
+ style: GITHUB_ROW_STYLE,
856
+ disabled: busy !== '',
857
+ onClick: () => clone(repo),
858
+ title: repo.description === '' ? repo.fullName : `${repo.fullName} — ${repo.description}`,
859
+ 'data-polish-github-row': repo.fullName,
860
+ },
861
+ React.createElement('span', { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
862
+ repo.fullName,
863
+ repo.isPrivate ? React.createElement('span', { style: { marginLeft: '6px', opacity: 0.6 } }, 'private') : null,
864
+ repo.isFork ? React.createElement('span', { style: { marginLeft: '6px', opacity: 0.6 } }, 'fork') : null),
865
+ repo.cloned === true
866
+ ? React.createElement('span', { style: { opacity: 0.6 } }, 'cloned')
867
+ : null,
868
+ busy === repo.fullName ? React.createElement('span', { style: { opacity: 0.7 } }, 'cloning…') : null))
869
+
870
+ return ReactDOM.createPortal(
871
+ React.createElement('div', {
872
+ style: GITHUB_BACKDROP_STYLE,
873
+ 'data-polish-github': 'dialog',
874
+ onPointerDown: event => { if (event.target === event.currentTarget) githubStore.set(false) },
875
+ },
876
+ React.createElement('div', { style: GITHUB_CARD_STYLE, role: 'dialog', 'aria-label': 'Add a workspace from GitHub' },
877
+ React.createElement('div', { style: { ...PANEL_TITLE_STYLE, marginBottom: '4px' } },
878
+ React.createElement('span', null, 'Add a workspace from GitHub'),
879
+ React.createElement('button', {
880
+ type: 'button',
881
+ onClick: () => githubStore.set(false),
882
+ style: { border: 'none', background: 'transparent', color: 'inherit', font: 'inherit', cursor: 'pointer', opacity: 0.7 },
883
+ 'aria-label': 'Close',
884
+ }, '×')),
885
+ React.createElement('div', { style: { opacity: 0.75, minHeight: '18px' } },
886
+ phase === 'loading' && !signedIn ? 'Checking the GitHub CLI session…' : null,
887
+ signedIn ? `Signed in as ${String(account.login)} · cloning into ${root}` : null,
888
+ !signedIn && phase !== 'loading'
889
+ ? 'The GitHub CLI is not signed in. Run `gh auth login` in a terminal, then reopen this dialog.'
890
+ : null),
891
+ signedIn
892
+ ? React.createElement('input', {
893
+ style: { ...GITHUB_INPUT_STYLE, marginTop: '10px' },
894
+ value: query,
895
+ placeholder: 'Search your repositories',
896
+ 'aria-label': 'Search your repositories',
897
+ autoFocus: true,
898
+ onChange: event => setQuery(event.target.value),
899
+ })
900
+ : null,
901
+ error !== ''
902
+ ? React.createElement('div', { style: { marginTop: '10px', color: 'var(--dsw-alias-label-error, #b42318)' } }, error)
903
+ : null,
904
+ signedIn
905
+ ? React.createElement('div', { style: GITHUB_LIST_STYLE, 'data-polish-github-list': String(visible.length) },
906
+ phase === 'loading' && visible.length === 0 ? React.createElement('div', { style: { padding: '10px 4px', opacity: 0.7 } }, 'Loading repositories…') : null,
907
+ phase === 'ready' && visible.length === 0 ? React.createElement('div', { style: { padding: '10px 4px', opacity: 0.7 } }, 'No repository matches that search.') : null,
908
+ ...rows)
909
+ : null,
910
+ React.createElement('p', { style: { margin: '10px 0 0', color: 'var(--dsw-alias-label-tertiary, inherit)' } },
911
+ 'The clone is added as a workspace. Sign-in comes from the GitHub CLI, so this plugin stores no token.')))
912
+ , document.body)
913
+ }
914
+
915
+ /** The compact footer-row skin shared by the status line. */
916
+ const FOOT_ROW_STYLE = {
917
+ display: 'flex',
918
+ alignItems: 'center',
919
+ gap: '6px',
920
+ maxWidth: '100%',
921
+ minWidth: 0,
922
+ padding: '2px 6px',
923
+ border: 'none',
924
+ borderRadius: 'var(--dsw-radius-sm, 6px)',
925
+ background: 'transparent',
926
+ color: 'var(--dsw-alias-label-tertiary, inherit)',
927
+ font: 'inherit',
928
+ fontSize: '12px',
929
+ lineHeight: '18px',
930
+ fontVariantNumeric: 'tabular-nums',
931
+ whiteSpace: 'nowrap',
932
+ overflow: 'hidden',
933
+ textOverflow: 'ellipsis',
934
+ cursor: 'pointer',
935
+ }
936
+
937
+ /**
938
+ * The footer status line: which GitHub session the "+" menu will clone with.
939
+ *
940
+ * It reads the same `status` action the dialog does, so it costs one request
941
+ * when the Shell loads and one whenever the window regains focus.
942
+ *
943
+ * @param props - our lazy `configForms`.
944
+ * @returns the row, or null when the feature is switched off.
945
+ */
946
+ function GithubStatus(props) {
947
+ const enabled = useSwitch(props.configForms, 'githubWorkspace', true)
948
+ const [status, setStatus] = React.useState(undefined)
949
+ React.useEffect(() => {
950
+ if (!enabled) return undefined
951
+ let live = true
952
+ const read = () => {
953
+ Promise.resolve()
954
+ .then(() => githubCall('status'))
955
+ .then(next => { if (live) setStatus(next) })
956
+ .catch(() => { if (live) setStatus({ signedIn: false }) })
957
+ }
958
+ read()
959
+ const onFocus = () => { read() }
960
+ window.addEventListener('focus', onFocus)
961
+ return () => {
962
+ live = false
963
+ window.removeEventListener('focus', onFocus)
964
+ }
965
+ }, [enabled])
966
+ if (!enabled) return null
967
+ const signedIn = status !== undefined && status.signedIn === true
968
+ const label = status === undefined
969
+ ? 'GitHub · checking…'
970
+ : signedIn ? `GitHub · ${String(status.login)}` : 'GitHub · signed out'
971
+ return React.createElement('div', { style: { padding: '2px 8px 0', minWidth: 0 } },
972
+ React.createElement('button', {
973
+ type: 'button',
974
+ 'data-polish-github': 'status',
975
+ 'data-polish-github-state': signedIn ? 'in' : status === undefined ? 'checking' : 'out',
976
+ title: signedIn
977
+ ? 'Add a workspace from GitHub'
978
+ : 'Sign in with the GitHub CLI (gh auth login), then add a repository as a workspace',
979
+ 'aria-label': label,
980
+ onClick: () => githubStore.open(),
981
+ style: FOOT_ROW_STYLE,
982
+ onMouseEnter: event => { event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
983
+ onMouseLeave: event => { event.currentTarget.style.background = 'transparent' },
984
+ }, label))
985
+ }
986
+
987
+ /** One row of the add-workspace source menu. */
988
+ const SOURCE_ROW_STYLE = {
989
+ display: 'flex',
990
+ flexDirection: 'column',
991
+ alignItems: 'flex-start',
992
+ gap: '2px',
993
+ width: '100%',
994
+ padding: '8px 10px',
995
+ border: 'none',
996
+ borderRadius: 'var(--dsw-radius-sm, 6px)',
997
+ background: 'transparent',
998
+ color: 'inherit',
999
+ font: 'inherit',
1000
+ textAlign: 'left',
1001
+ cursor: 'pointer',
1002
+ }
1003
+
1004
+ /**
1005
+ * The "+" add-workspace flow: choose where the workspace comes from.
1006
+ *
1007
+ * The Harness shows its own `Add workspace…` row only while this slot is
1008
+ * occupied, and a single-owner slot means this dialog stands in for the folder
1009
+ * picker. It therefore offers both sources: the machine (the Host's own OS
1010
+ * chooser when the composition serves the `native` capability, otherwise a
1011
+ * folder list driven by the browse primitives) and GitHub.
1012
+ *
1013
+ * @param props - the slot owner's `open`/`busy`/`onPicked`/`onCancel`/`onError`,
1014
+ * our lazy `configForms`, and the directory-picker calls.
1015
+ * @returns the flow, or null while it is closed or switched off.
1016
+ */
1017
+ function WorkspaceSourceFlow(props) {
1018
+ const enabled = useSwitch(props.configForms, 'workspaceSources', true)
1019
+ const [step, setStep] = React.useState('menu')
1020
+ const [listing, setListing] = React.useState(undefined)
1021
+ const [error, setError] = React.useState('')
1022
+ const [busy, setBusy] = React.useState(false)
1023
+
1024
+ const cancel = () => {
1025
+ if (typeof props.onCancel === 'function') props.onCancel()
1026
+ }
1027
+
1028
+ // Escape closes the flow. Declared with the other hooks, before the early
1029
+ // return, so the hook order stays fixed while the flow opens and closes.
1030
+ React.useEffect(() => {
1031
+ if (!enabled || props.open !== true) return undefined
1032
+ const onKey = event => { if (event.key === 'Escape') cancel() }
1033
+ document.addEventListener('keydown', onKey)
1034
+ return () => document.removeEventListener('keydown', onKey)
1035
+ }, [enabled, props.open])
1036
+
1037
+ if (!enabled || props.open !== true) return null
1038
+
1039
+ const fail = message => {
1040
+ setBusy(false)
1041
+ setError(message)
1042
+ }
1043
+ const messageOf = reason => (reason instanceof Error ? reason.message : String(reason))
1044
+
1045
+ const land = path => {
1046
+ setBusy(true)
1047
+ setError('')
1048
+ Promise.resolve()
1049
+ .then(() => props.onPicked(path))
1050
+ .catch(reason => fail(messageOf(reason)))
1051
+ }
1052
+
1053
+ const browse = path => {
1054
+ setBusy(true)
1055
+ setError('')
1056
+ Promise.resolve()
1057
+ .then(() => foldersCall({ action: 'ls', path }))
1058
+ .then(level => {
1059
+ setListing(level)
1060
+ setBusy(false)
1061
+ setStep('browse')
1062
+ })
1063
+ .catch(reason => fail(messageOf(reason)))
1064
+ }
1065
+
1066
+ /**
1067
+ * Start the folder browser at the machine's roots.
1068
+ *
1069
+ * This deliberately does not call the Host's directory picker: a composed
1070
+ * picker serves either `browse` or `native`, never both, and the browser build
1071
+ * composes the one whose chooser cannot open there — a `pick` call would wait
1072
+ * for a dialog that never appears. Our own listing behaves the same in the app
1073
+ * and in the browser, and hands the chosen path back through `onPicked`.
1074
+ */
1075
+ const chooseOnPc = () => {
1076
+ if (busy) return
1077
+ setBusy(true)
1078
+ setError('')
1079
+ Promise.resolve()
1080
+ .then(() => foldersCall({ action: 'roots' }))
1081
+ .then(payload => {
1082
+ const roots = Array.isArray(payload.roots) ? payload.roots : []
1083
+ setListing({
1084
+ path: '',
1085
+ parent: null,
1086
+ entries: roots.map(root => ({ name: root.label, path: root.path })),
1087
+ })
1088
+ setBusy(false)
1089
+ setStep('browse')
1090
+ })
1091
+ .catch(reason => fail(messageOf(reason)))
1092
+ }
1093
+
1094
+ const openGithub = () => {
1095
+ githubStore.open(path => props.onPicked(path))
1096
+ cancel()
1097
+ }
1098
+
1099
+ const rows = step === 'menu'
1100
+ ? [
1101
+ React.createElement('button', {
1102
+ key: 'pc',
1103
+ type: 'button',
1104
+ 'data-polish-source': 'machine',
1105
+ disabled: busy,
1106
+ onClick: chooseOnPc,
1107
+ style: SOURCE_ROW_STYLE,
1108
+ onMouseEnter: event => { event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
1109
+ onMouseLeave: event => { event.currentTarget.style.background = 'transparent' },
1110
+ },
1111
+ React.createElement('span', { style: { fontWeight: 500 } }, 'On this PC'),
1112
+ React.createElement('span', { style: { opacity: 0.7 } }, 'Choose a folder on this machine')),
1113
+ React.createElement('button', {
1114
+ key: 'github',
1115
+ type: 'button',
1116
+ 'data-polish-source': 'github',
1117
+ disabled: busy,
1118
+ onClick: openGithub,
1119
+ style: SOURCE_ROW_STYLE,
1120
+ onMouseEnter: event => { event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
1121
+ onMouseLeave: event => { event.currentTarget.style.background = 'transparent' },
1122
+ },
1123
+ React.createElement('span', { style: { fontWeight: 500 } }, 'GitHub repository…'),
1124
+ React.createElement('span', { style: { opacity: 0.7 } }, 'Clone one of your repositories as a workspace')),
1125
+ ]
1126
+ : [
1127
+ React.createElement('div', {
1128
+ key: 'head',
1129
+ style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '2px 4px 6px', minWidth: 0 },
1130
+ },
1131
+ React.createElement('span', {
1132
+ style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
1133
+ title: listing !== undefined ? listing.path : '',
1134
+ }, listing === undefined || listing.path === '' ? 'This PC' : listing.path),
1135
+ listing === undefined || listing.path === ''
1136
+ ? null
1137
+ : React.createElement('button', {
1138
+ type: 'button',
1139
+ 'data-polish-pick': 'here',
1140
+ disabled: busy,
1141
+ onClick: () => { if (listing !== undefined) land(listing.path) },
1142
+ style: { ...FOOT_ROW_STYLE, color: 'var(--dsw-alias-label-primary, inherit)', background: 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
1143
+ }, 'Use this folder')),
1144
+ ...(listing === undefined || listing.parent === null || listing.parent === undefined
1145
+ ? []
1146
+ : [React.createElement('button', {
1147
+ key: 'up',
1148
+ type: 'button',
1149
+ 'data-polish-folder': '..',
1150
+ disabled: busy,
1151
+ onClick: () => browse(listing.parent),
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
+ }, React.createElement('span', { title: listing.parent }, '..'))]),
1156
+ ...(listing === undefined
1157
+ ? []
1158
+ : listing.entries.map(entry => React.createElement('button', {
1159
+ key: entry.path,
1160
+ type: 'button',
1161
+ 'data-polish-folder': entry.name,
1162
+ disabled: busy,
1163
+ onClick: () => browse(entry.path),
1164
+ style: SOURCE_ROW_STYLE,
1165
+ onMouseEnter: event => { event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,0.06))' },
1166
+ onMouseLeave: event => { event.currentTarget.style.background = 'transparent' },
1167
+ }, React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, title: entry.path }, entry.name)))),
1168
+ ]
1169
+
1170
+ // The flow is rendered inside the Shell's add-workspace popover, which is
1171
+ // narrow and sits under the sidebar's own stacking order. A portal to the body
1172
+ // puts it above everything and keeps the rows readable at their own width.
1173
+ return ReactDOM.createPortal(
1174
+ React.createElement('div', {
1175
+ style: GITHUB_BACKDROP_STYLE,
1176
+ 'data-polish-flow-backdrop': step,
1177
+ onPointerDown: event => { if (event.target === event.currentTarget) cancel() },
1178
+ },
1179
+ React.createElement('div', {
1180
+ 'data-polish-flow': step,
1181
+ role: 'dialog',
1182
+ 'aria-label': 'Add a workspace',
1183
+ style: {
1184
+ display: 'flex',
1185
+ flexDirection: 'column',
1186
+ gap: '2px',
1187
+ width: 'min(360px, calc(100vw - 32px))',
1188
+ maxHeight: 'min(420px, calc(100vh - 96px))',
1189
+ padding: '6px',
1190
+ overflowY: 'auto',
1191
+ borderRadius: 'var(--dsw-radius-lg, 12px)',
1192
+ background: 'var(--dsw-specific-menu, Canvas)',
1193
+ boxShadow: 'var(--dsw-elevation-prominent, 0 8px 24px rgba(0,0,0,0.18))',
1194
+ fontSize: '12px',
1195
+ lineHeight: '18px',
1196
+ color: 'var(--dsw-alias-label-secondary, inherit)',
1197
+ },
1198
+ },
1199
+ error !== ''
1200
+ ? React.createElement('div', { style: { padding: '4px 6px', color: 'var(--dsw-alias-label-error, #b42318)' } }, error)
1201
+ : null,
1202
+ ...rows,
1203
+ step === 'menu'
1204
+ ? null
1205
+ : React.createElement('button', {
1206
+ type: 'button',
1207
+ 'data-polish-pick': 'back',
1208
+ onClick: () => { setStep('menu'); setListing(undefined); setError('') },
1209
+ style: { ...FOOT_ROW_STYLE, justifyContent: 'flex-start' },
1210
+ }, 'Back'),
1211
+ React.createElement('button', {
1212
+ type: 'button',
1213
+ 'data-polish-pick': 'cancel',
1214
+ onClick: cancel,
1215
+ style: { ...FOOT_ROW_STYLE, justifyContent: 'flex-start' },
1216
+ }, 'Cancel'))),
1217
+ document.body)
1218
+ }
1219
+
619
1220
  return {
620
1221
  inject: ['slots'],
621
1222
  /**
@@ -678,5 +1279,45 @@ return {
678
1279
  inject: () => ({ configForms, openFolder, workspaces: workspaceCtx.get('workspaces') }),
679
1280
  }, OpenFolderMenuItem))
680
1281
  })
1282
+ // Adding a workspace from GitHub goes through the Workspace service's own
1283
+ // create call — the same one the "+" menu makes once its folder picker
1284
+ // returns a path — so a clone lands in the sidebar exactly like a picked
1285
+ // folder does, with no Harness change anywhere. The service takes the plain
1286
+ // path and throws on refusal; the Remote namespace underneath it is
1287
+ // `ctx.remote.workspace` (singular), which is why this injects the service.
1288
+ ctx.inject(['workspaces'], (githubCtx) => {
1289
+ const createWorkspace = async (path) => {
1290
+ const workspace = await githubCtx.workspaces.create({ path })
1291
+ return workspace
1292
+ }
1293
+ githubCtx.slots.inject('shell.overlay', () => githubCtx.slots.register({
1294
+ name: 'shell.overlay',
1295
+ id: 'polish-github',
1296
+ inject: () => ({ configForms, createWorkspace }),
1297
+ }, GitHubPanel))
1298
+ // The footer keeps the sign-in state, not an add button: adding a
1299
+ // workspace belongs in the "+" menu, where the Shell says it does.
1300
+ githubCtx.slots.inject('sidebar.footer.action', () => githubCtx.slots.register({
1301
+ name: 'sidebar.footer.action',
1302
+ id: 'polish-github-status',
1303
+ order: 4,
1304
+ inject: () => ({ configForms }),
1305
+ }, GithubStatus))
1306
+ })
1307
+ // The "+" menu's `Add workspace…` row exists only while a plugin occupies
1308
+ // this single-owner hole, so occupying it means standing in for the folder
1309
+ // picker: the flow offers the machine and GitHub as sources. It needs no
1310
+ // Remote injection: the folder list and the clone both come from this
1311
+ // bundle's own Host routes, which is what makes the flow behave the same in
1312
+ // the browser and in the app.
1313
+ ctx.slots.inject('sidebar.workspaces.directoryFlow', () => ctx.slots.register({
1314
+ name: 'sidebar.workspaces.directoryFlow',
1315
+ // A single slot clashes only at the exact priority; another priority
1316
+ // shadows it and the LOWEST priority renders. The shipped pickers
1317
+ // register at 0, so -1 is what puts this flow in front of them without
1318
+ // making their own activation throw.
1319
+ priority: -1,
1320
+ inject: () => ({ configForms }),
1321
+ }, WorkspaceSourceFlow))
681
1322
  },
682
1323
  }