@stigmer/react 0.2.1 → 0.2.3
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/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +22 -32
- package/composer/SessionComposer.js.map +1 -1
- package/github/index.d.ts +1 -1
- package/github/index.d.ts.map +1 -1
- package/github/index.js.map +1 -1
- package/github/useGitHubConnection.d.ts +70 -1
- package/github/useGitHubConnection.d.ts.map +1 -1
- package/github/useGitHubConnection.js +99 -20
- package/github/useGitHubConnection.js.map +1 -1
- package/identity-provider/IdentityProviderWizard.d.ts.map +1 -1
- package/identity-provider/IdentityProviderWizard.js +19 -3
- package/identity-provider/IdentityProviderWizard.js.map +1 -1
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js.map +1 -1
- package/organization/OrgProfilePanel.d.ts.map +1 -1
- package/organization/OrgProfilePanel.js +23 -2
- package/organization/OrgProfilePanel.js.map +1 -1
- package/package.json +4 -4
- package/runner/RunnerFileBrowser.d.ts +11 -1
- package/runner/RunnerFileBrowser.d.ts.map +1 -1
- package/runner/RunnerFileBrowser.js +70 -7
- package/runner/RunnerFileBrowser.js.map +1 -1
- package/runner/WorkspaceRunnerSelector.d.ts +36 -0
- package/runner/WorkspaceRunnerSelector.d.ts.map +1 -0
- package/runner/WorkspaceRunnerSelector.js +63 -0
- package/runner/WorkspaceRunnerSelector.js.map +1 -0
- package/runner/index.d.ts +2 -0
- package/runner/index.d.ts.map +1 -1
- package/runner/index.js +1 -0
- package/runner/index.js.map +1 -1
- package/runner/useRunnerFileBrowser.d.ts.map +1 -1
- package/runner/useRunnerFileBrowser.js +26 -2
- package/runner/useRunnerFileBrowser.js.map +1 -1
- package/settings/MembersSection.d.ts.map +1 -1
- package/settings/MembersSection.js +7 -2
- package/settings/MembersSection.js.map +1 -1
- package/src/composer/SessionComposer.tsx +46 -43
- package/src/github/index.ts +1 -0
- package/src/github/useGitHubConnection.ts +162 -22
- package/src/identity-provider/IdentityProviderWizard.tsx +112 -3
- package/src/index.ts +1 -0
- package/src/organization/OrgProfilePanel.tsx +98 -0
- package/src/runner/RunnerFileBrowser.tsx +227 -8
- package/src/runner/WorkspaceRunnerSelector.tsx +180 -0
- package/src/runner/index.ts +3 -0
- package/src/runner/useRunnerFileBrowser.ts +39 -3
- package/src/settings/MembersSection.tsx +23 -1
- package/src/workspace/WorkspaceEditor.tsx +176 -126
- package/src/workspace/index.ts +5 -0
- package/src/workspace/useRecentWorkspaces.ts +162 -0
- package/src/workspace/useWorkspaceEntries.ts +13 -0
- package/styles.css +1 -1
- package/workspace/WorkspaceEditor.d.ts +25 -22
- package/workspace/WorkspaceEditor.d.ts.map +1 -1
- package/workspace/WorkspaceEditor.js +64 -43
- package/workspace/WorkspaceEditor.js.map +1 -1
- package/workspace/index.d.ts +2 -0
- package/workspace/index.d.ts.map +1 -1
- package/workspace/index.js +1 -0
- package/workspace/index.js.map +1 -1
- package/workspace/useRecentWorkspaces.d.ts +31 -0
- package/workspace/useRecentWorkspaces.d.ts.map +1 -0
- package/workspace/useRecentWorkspaces.js +117 -0
- package/workspace/useRecentWorkspaces.js.map +1 -0
- package/workspace/useWorkspaceEntries.d.ts +8 -0
- package/workspace/useWorkspaceEntries.d.ts.map +1 -1
- package/workspace/useWorkspaceEntries.js +4 -0
- package/workspace/useWorkspaceEntries.js.map +1 -1
|
@@ -18,58 +18,60 @@ export interface WorkspaceEditorProps {
|
|
|
18
18
|
readonly disabled?: boolean;
|
|
19
19
|
/** GitHub connection state. When provided, enables the GitHub repo picker. */
|
|
20
20
|
readonly gitHubConnection?: UseGitHubConnectionReturn;
|
|
21
|
-
/**
|
|
21
|
+
/** Enable the "Connect GitHub" action. Default: true. */
|
|
22
22
|
readonly enableGitHub?: boolean;
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* Enable the "Browse Folder" action.
|
|
25
25
|
*
|
|
26
|
-
* The
|
|
27
|
-
* file browser requires a connected runner to query.
|
|
26
|
+
* The action is only functional when `runnerId` is also provided,
|
|
27
|
+
* since the file browser requires a connected runner to query.
|
|
28
|
+
* When `runnerId` is null (Auto selected), the action is disabled.
|
|
28
29
|
*/
|
|
29
30
|
readonly enableLocal?: boolean;
|
|
30
31
|
/**
|
|
31
32
|
* ID of the runner to use for filesystem browsing.
|
|
32
33
|
*
|
|
33
|
-
* When provided together with `enableLocal`, the
|
|
34
|
-
*
|
|
35
|
-
* filesystem via the `ListDirectory` command.
|
|
34
|
+
* When provided together with `enableLocal`, the "Browse Folder"
|
|
35
|
+
* action drills into a {@link RunnerFileBrowser} that queries the
|
|
36
|
+
* runner's filesystem via the `ListDirectory` command.
|
|
36
37
|
*/
|
|
37
38
|
readonly runnerId?: string | null;
|
|
38
39
|
/**
|
|
39
40
|
* Native folder picker callback for desktop environments.
|
|
40
41
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* across web and desktop.
|
|
42
|
+
* When provided alongside `runnerId`, renders an "Open system dialog"
|
|
43
|
+
* button in the Browse Folder drill-in view. Desktop-only enhancement.
|
|
44
44
|
*/
|
|
45
45
|
readonly onBrowseLocalFolder?: () => Promise<string | null>;
|
|
46
46
|
/**
|
|
47
47
|
* Display name of the currently selected runner.
|
|
48
|
-
*
|
|
49
|
-
* When provided, a contextual hint is shown above the manual local
|
|
50
|
-
* path input (fallback when `runnerId` is not set) indicating that
|
|
51
|
-
* paths are relative to this runner's filesystem.
|
|
48
|
+
* Passed through to {@link RunnerFileBrowser} for the context header.
|
|
52
49
|
*/
|
|
53
50
|
readonly runnerName?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Hostname of the runner's machine (e.g. "Alice's MacBook Pro").
|
|
53
|
+
* Passed through to {@link RunnerFileBrowser} for the context header.
|
|
54
|
+
*/
|
|
55
|
+
readonly runnerHostname?: string;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
type
|
|
58
|
+
type ActivePanel = "browse" | "github" | null;
|
|
57
59
|
|
|
58
60
|
const TYPE_LABELS: Record<string, string> = {
|
|
59
61
|
git: "GitHub",
|
|
60
|
-
local: "Local",
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
|
-
* Styled component
|
|
65
|
+
* Styled component for managing workspace entries with a flat-list
|
|
66
|
+
* layout and drill-in sub-views.
|
|
65
67
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* - **GitHub Repo**: Shows the repo picker or connect prompt.
|
|
68
|
+
* The default view shows:
|
|
69
|
+
* 1. Current workspace entries (with remove buttons)
|
|
70
|
+
* 2. Action items: "Browse Folder" and "Connect GitHub"
|
|
70
71
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
72
|
+
* Each action drills into a sub-view (file browser or GitHub picker)
|
|
73
|
+
* with a back button to return to the list. This follows the same
|
|
74
|
+
* progressive-disclosure pattern as the Configure menu.
|
|
73
75
|
*
|
|
74
76
|
* All visual properties flow through `--stgm-*` tokens.
|
|
75
77
|
*
|
|
@@ -101,19 +103,15 @@ export function WorkspaceEditor({
|
|
|
101
103
|
runnerId,
|
|
102
104
|
onBrowseLocalFolder,
|
|
103
105
|
runnerName,
|
|
106
|
+
runnerHostname,
|
|
104
107
|
}: WorkspaceEditorProps) {
|
|
105
|
-
const
|
|
106
|
-
const hasGitHub = enableGitHub;
|
|
107
|
-
const hasBothTabs = hasLocal && hasGitHub;
|
|
108
|
-
|
|
109
|
-
const [activeTab, setActiveTab] = useState<ActiveTab>(
|
|
110
|
-
hasLocal ? "local" : "github",
|
|
111
|
-
);
|
|
112
|
-
|
|
108
|
+
const [activePanel, setActivePanel] = useState<ActivePanel>(null);
|
|
113
109
|
const [manualUrl, setManualUrl] = useState("");
|
|
114
110
|
const [manualBranch, setManualBranch] = useState("");
|
|
115
111
|
const entryList = useScrollShadows();
|
|
116
112
|
|
|
113
|
+
const canBrowse = enableLocal && !!runnerId;
|
|
114
|
+
|
|
117
115
|
const handleGitHubSelect = useCallback(
|
|
118
116
|
(repoUrl: string, branch: string) => {
|
|
119
117
|
workspace.addGitRepo(repoUrl, branch);
|
|
@@ -139,11 +137,87 @@ export function WorkspaceEditor({
|
|
|
139
137
|
[],
|
|
140
138
|
);
|
|
141
139
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
const goBack = useCallback(() => setActivePanel(null), []);
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Drill-in: Browse Folder
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
if (activePanel === "browse" && canBrowse) {
|
|
147
|
+
return (
|
|
148
|
+
<div className={["space-y-2", className].filter(Boolean).join(" ")}>
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={goBack}
|
|
152
|
+
className="inline-flex items-center gap-1 text-[0.65rem] text-muted-foreground hover:text-foreground transition-colors"
|
|
153
|
+
>
|
|
154
|
+
<ChevronLeftIcon />
|
|
155
|
+
Back
|
|
156
|
+
</button>
|
|
157
|
+
<div className="space-y-2">
|
|
158
|
+
<RunnerFileBrowser
|
|
159
|
+
runnerId={runnerId!}
|
|
160
|
+
onSelect={(path) => {
|
|
161
|
+
workspace.addLocalPath(path);
|
|
162
|
+
setActivePanel(null);
|
|
163
|
+
}}
|
|
164
|
+
onCancel={goBack}
|
|
165
|
+
runnerName={runnerName}
|
|
166
|
+
runnerHostname={runnerHostname}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Drill-in: Connect GitHub
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
if (activePanel === "github" && enableGitHub) {
|
|
178
|
+
return (
|
|
179
|
+
<div className={["space-y-2", className].filter(Boolean).join(" ")}>
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
onClick={goBack}
|
|
183
|
+
className="inline-flex items-center gap-1 text-[0.65rem] text-muted-foreground hover:text-foreground transition-colors"
|
|
184
|
+
>
|
|
185
|
+
<ChevronLeftIcon />
|
|
186
|
+
Back
|
|
187
|
+
</button>
|
|
188
|
+
{gitHubConnection ? (
|
|
189
|
+
<GitHubPanel
|
|
190
|
+
connection={gitHubConnection}
|
|
191
|
+
onSelect={(url, branch) => {
|
|
192
|
+
handleGitHubSelect(url, branch);
|
|
193
|
+
setActivePanel(null);
|
|
194
|
+
}}
|
|
195
|
+
onClose={goBack}
|
|
196
|
+
/>
|
|
197
|
+
) : (
|
|
198
|
+
<ManualGitPanel
|
|
199
|
+
url={manualUrl}
|
|
200
|
+
branch={manualBranch}
|
|
201
|
+
onUrlChange={setManualUrl}
|
|
202
|
+
onBranchChange={setManualBranch}
|
|
203
|
+
onAdd={() => {
|
|
204
|
+
handleManualAdd();
|
|
205
|
+
setActivePanel(null);
|
|
206
|
+
}}
|
|
207
|
+
onCancel={goBack}
|
|
208
|
+
onKeyDown={handleKeyDown(() => {
|
|
209
|
+
handleManualAdd();
|
|
210
|
+
setActivePanel(null);
|
|
211
|
+
})}
|
|
212
|
+
/>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Default view: flat list (entries + actions)
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
147
221
|
|
|
148
222
|
return (
|
|
149
223
|
<div className={["space-y-2", className].filter(Boolean).join(" ")}>
|
|
@@ -152,15 +226,21 @@ export function WorkspaceEditor({
|
|
|
152
226
|
<div className="relative">
|
|
153
227
|
{entryList.canScrollUp && <ScrollFade position="top" />}
|
|
154
228
|
|
|
155
|
-
<div ref={entryList.scrollRef} className="max-h-28 space-y-
|
|
229
|
+
<div ref={entryList.scrollRef} className="max-h-28 space-y-1 overflow-y-auto">
|
|
156
230
|
{workspace.entries.map((entry) => (
|
|
157
231
|
<div
|
|
158
232
|
key={entry.id}
|
|
159
233
|
className="flex items-center gap-2 rounded-md border border-border bg-muted-faint px-2.5 py-1.5 text-xs"
|
|
160
234
|
>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
235
|
+
{TYPE_LABELS[entry.type] ? (
|
|
236
|
+
<span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[0.65rem] text-muted-foreground">
|
|
237
|
+
{TYPE_LABELS[entry.type]}
|
|
238
|
+
</span>
|
|
239
|
+
) : (
|
|
240
|
+
<span className="shrink-0 text-muted-foreground">
|
|
241
|
+
<FolderIcon />
|
|
242
|
+
</span>
|
|
243
|
+
)}
|
|
164
244
|
<span
|
|
165
245
|
className={[
|
|
166
246
|
"min-w-0 flex-1 truncate text-foreground",
|
|
@@ -187,81 +267,48 @@ export function WorkspaceEditor({
|
|
|
187
267
|
</div>
|
|
188
268
|
)}
|
|
189
269
|
|
|
190
|
-
{/*
|
|
191
|
-
|
|
192
|
-
|
|
270
|
+
{/* Action items */}
|
|
271
|
+
<div className="space-y-0.5">
|
|
272
|
+
{enableLocal && (
|
|
193
273
|
<button
|
|
194
274
|
type="button"
|
|
195
|
-
onClick={
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
:
|
|
202
|
-
|
|
275
|
+
onClick={
|
|
276
|
+
onBrowseLocalFolder
|
|
277
|
+
? async () => {
|
|
278
|
+
const path = await onBrowseLocalFolder();
|
|
279
|
+
if (path) workspace.addLocalPath(path);
|
|
280
|
+
}
|
|
281
|
+
: () => setActivePanel("browse")
|
|
282
|
+
}
|
|
283
|
+
disabled={disabled || !runnerId}
|
|
284
|
+
className="flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-xs text-foreground transition-colors hover:bg-accent-hover disabled:pointer-events-none disabled:opacity-40"
|
|
203
285
|
>
|
|
204
286
|
<FolderIcon />
|
|
205
|
-
|
|
287
|
+
<span className="flex-1 text-left">Browse Folder</span>
|
|
288
|
+
{!onBrowseLocalFolder && <ChevronRightIcon />}
|
|
206
289
|
</button>
|
|
290
|
+
)}
|
|
291
|
+
{enableGitHub && (
|
|
207
292
|
<button
|
|
208
293
|
type="button"
|
|
209
|
-
onClick={() =>
|
|
294
|
+
onClick={() => setActivePanel("github")}
|
|
210
295
|
disabled={disabled}
|
|
211
|
-
className=
|
|
212
|
-
"flex flex-1 items-center justify-center gap-1.5 rounded px-2 py-1 text-[0.65rem] font-medium transition-colors disabled:pointer-events-none disabled:opacity-50",
|
|
213
|
-
effectiveTab === "github"
|
|
214
|
-
? "bg-background text-foreground shadow-sm"
|
|
215
|
-
: "text-muted-foreground hover:text-foreground",
|
|
216
|
-
].join(" ")}
|
|
296
|
+
className="flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-xs text-foreground transition-colors hover:bg-accent-hover disabled:pointer-events-none disabled:opacity-40"
|
|
217
297
|
>
|
|
218
298
|
<GitHubIcon />
|
|
219
|
-
GitHub
|
|
299
|
+
<span className="flex-1 text-left">Connect GitHub</span>
|
|
300
|
+
<ChevronRightIcon />
|
|
220
301
|
</button>
|
|
221
|
-
</div>
|
|
222
|
-
)}
|
|
223
|
-
|
|
224
|
-
{/* Tab content */}
|
|
225
|
-
<div className="rounded-md border border-border bg-card p-3">
|
|
226
|
-
{effectiveTab === "local" && hasLocal && (
|
|
227
|
-
<RunnerFileBrowser
|
|
228
|
-
runnerId={runnerId!}
|
|
229
|
-
onSelect={(path) => workspace.addLocalPath(path)}
|
|
230
|
-
onCancel={() => {
|
|
231
|
-
if (hasGitHub) setActiveTab("github");
|
|
232
|
-
}}
|
|
233
|
-
/>
|
|
234
|
-
)}
|
|
235
|
-
|
|
236
|
-
{effectiveTab === "github" && hasGitHub && (
|
|
237
|
-
gitHubConnection ? (
|
|
238
|
-
<GitHubPanel
|
|
239
|
-
connection={gitHubConnection}
|
|
240
|
-
onSelect={handleGitHubSelect}
|
|
241
|
-
onClose={() => {
|
|
242
|
-
if (hasLocal) setActiveTab("local");
|
|
243
|
-
}}
|
|
244
|
-
/>
|
|
245
|
-
) : (
|
|
246
|
-
<ManualGitPanel
|
|
247
|
-
url={manualUrl}
|
|
248
|
-
branch={manualBranch}
|
|
249
|
-
onUrlChange={setManualUrl}
|
|
250
|
-
onBranchChange={setManualBranch}
|
|
251
|
-
onAdd={handleManualAdd}
|
|
252
|
-
onCancel={() => {
|
|
253
|
-
if (hasLocal) setActiveTab("local");
|
|
254
|
-
}}
|
|
255
|
-
onKeyDown={handleKeyDown(handleManualAdd)}
|
|
256
|
-
/>
|
|
257
|
-
)
|
|
258
302
|
)}
|
|
259
303
|
</div>
|
|
260
304
|
</div>
|
|
261
305
|
);
|
|
262
306
|
}
|
|
263
307
|
|
|
264
|
-
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// GitHub panel (progressive disclosure: connect prompt or repo picker)
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
265
312
|
function GitHubPanel({
|
|
266
313
|
connection,
|
|
267
314
|
onSelect,
|
|
@@ -295,16 +342,6 @@ function GitHubPanel({
|
|
|
295
342
|
|
|
296
343
|
return (
|
|
297
344
|
<div className="space-y-3 text-center">
|
|
298
|
-
<div className="flex justify-end">
|
|
299
|
-
<button
|
|
300
|
-
type="button"
|
|
301
|
-
onClick={onClose}
|
|
302
|
-
className="rounded p-0.5 text-muted-foreground hover:text-foreground transition-colors"
|
|
303
|
-
aria-label="Close"
|
|
304
|
-
>
|
|
305
|
-
<XIcon />
|
|
306
|
-
</button>
|
|
307
|
-
</div>
|
|
308
345
|
<div className="space-y-1">
|
|
309
346
|
<p className="text-xs font-medium text-foreground">
|
|
310
347
|
Choose a GitHub repo to add to workspace
|
|
@@ -365,23 +402,13 @@ function GitHubPanel({
|
|
|
365
402
|
{connection.user?.login ?? "Connected"}
|
|
366
403
|
</span>
|
|
367
404
|
</div>
|
|
368
|
-
<
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
</button>
|
|
376
|
-
<button
|
|
377
|
-
type="button"
|
|
378
|
-
onClick={onClose}
|
|
379
|
-
className="rounded p-0.5 text-muted-foreground hover:text-foreground transition-colors"
|
|
380
|
-
aria-label="Close"
|
|
381
|
-
>
|
|
382
|
-
<XIcon />
|
|
383
|
-
</button>
|
|
384
|
-
</div>
|
|
405
|
+
<button
|
|
406
|
+
type="button"
|
|
407
|
+
onClick={connection.disconnect}
|
|
408
|
+
className="text-[0.6rem] text-muted-foreground hover:text-destructive transition-colors"
|
|
409
|
+
>
|
|
410
|
+
Disconnect
|
|
411
|
+
</button>
|
|
385
412
|
</div>
|
|
386
413
|
<GitHubRepoPicker
|
|
387
414
|
token={connection.token!}
|
|
@@ -392,7 +419,10 @@ function GitHubPanel({
|
|
|
392
419
|
);
|
|
393
420
|
}
|
|
394
421
|
|
|
395
|
-
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
// Manual git URL input (fallback for platform builders without GitHub OAuth)
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
|
|
396
426
|
function ManualGitPanel({
|
|
397
427
|
url,
|
|
398
428
|
branch,
|
|
@@ -450,6 +480,26 @@ function ManualGitPanel({
|
|
|
450
480
|
);
|
|
451
481
|
}
|
|
452
482
|
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
// Icons
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
|
|
487
|
+
function ChevronLeftIcon() {
|
|
488
|
+
return (
|
|
489
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
490
|
+
<path d="M7.5 2.5L4 6L7.5 9.5" />
|
|
491
|
+
</svg>
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function ChevronRightIcon() {
|
|
496
|
+
return (
|
|
497
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="text-muted-foreground">
|
|
498
|
+
<path d="M4.5 2.5L8 6L4.5 9.5" />
|
|
499
|
+
</svg>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
453
503
|
function XIcon() {
|
|
454
504
|
return (
|
|
455
505
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
package/src/workspace/index.ts
CHANGED
|
@@ -7,3 +7,8 @@ export { WorkspaceEditor } from "./WorkspaceEditor";
|
|
|
7
7
|
export type { WorkspaceEditorProps } from "./WorkspaceEditor";
|
|
8
8
|
export { WorkspaceSummary } from "./WorkspaceSummary";
|
|
9
9
|
export type { WorkspaceSummaryProps } from "./WorkspaceSummary";
|
|
10
|
+
export { useRecentWorkspaces } from "./useRecentWorkspaces";
|
|
11
|
+
export type {
|
|
12
|
+
RecentWorkspace,
|
|
13
|
+
UseRecentWorkspacesReturn,
|
|
14
|
+
} from "./useRecentWorkspaces";
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
4
|
+
|
|
5
|
+
const STORAGE_KEY_PREFIX = "stigmer:recent-workspaces:";
|
|
6
|
+
const MAX_RECENT = 8;
|
|
7
|
+
|
|
8
|
+
/** A recently used or favorited workspace path for a specific runner. */
|
|
9
|
+
export interface RecentWorkspace {
|
|
10
|
+
/** Absolute path on the runner's filesystem. */
|
|
11
|
+
readonly path: string;
|
|
12
|
+
/** Whether the user pinned this path as a favorite. */
|
|
13
|
+
readonly pinned: boolean;
|
|
14
|
+
/** Epoch ms when this path was last selected. */
|
|
15
|
+
readonly lastUsed: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Return value of {@link useRecentWorkspaces}. */
|
|
19
|
+
export interface UseRecentWorkspacesReturn {
|
|
20
|
+
/** Recent paths sorted: pinned first, then by recency. */
|
|
21
|
+
readonly entries: readonly RecentWorkspace[];
|
|
22
|
+
/** Record a path selection (adds or bumps it in the list). */
|
|
23
|
+
readonly recordSelection: (path: string) => void;
|
|
24
|
+
/** Toggle the pinned state of a path. */
|
|
25
|
+
readonly togglePin: (path: string) => void;
|
|
26
|
+
/** Remove a path from the recent list. */
|
|
27
|
+
readonly remove: (path: string) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function storageKey(runnerId: string): string {
|
|
31
|
+
return `${STORAGE_KEY_PREFIX}${runnerId}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readEntries(runnerId: string): RecentWorkspace[] {
|
|
35
|
+
try {
|
|
36
|
+
const raw = localStorage.getItem(storageKey(runnerId));
|
|
37
|
+
if (!raw) return [];
|
|
38
|
+
return JSON.parse(raw) as RecentWorkspace[];
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeEntries(runnerId: string, entries: RecentWorkspace[]): void {
|
|
45
|
+
try {
|
|
46
|
+
localStorage.setItem(storageKey(runnerId), JSON.stringify(entries));
|
|
47
|
+
} catch {
|
|
48
|
+
// localStorage full or unavailable — silently degrade.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sortEntries(entries: RecentWorkspace[]): RecentWorkspace[] {
|
|
53
|
+
return [...entries].sort((a, b) => {
|
|
54
|
+
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
|
|
55
|
+
return b.lastUsed - a.lastUsed;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const listeners = new Set<() => void>();
|
|
60
|
+
let snapshotVersion = 0;
|
|
61
|
+
|
|
62
|
+
function notifyListeners(): void {
|
|
63
|
+
snapshotVersion++;
|
|
64
|
+
for (const fn of listeners) fn();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function subscribe(callback: () => void): () => void {
|
|
68
|
+
listeners.add(callback);
|
|
69
|
+
return () => listeners.delete(callback);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const EMPTY: readonly RecentWorkspace[] = [];
|
|
73
|
+
|
|
74
|
+
let cachedRunnerId: string | null = null;
|
|
75
|
+
let cachedVersion = -1;
|
|
76
|
+
let cachedResult: readonly RecentWorkspace[] = EMPTY;
|
|
77
|
+
|
|
78
|
+
function getSnapshot(runnerId: string | null): readonly RecentWorkspace[] {
|
|
79
|
+
if (!runnerId) return EMPTY;
|
|
80
|
+
if (runnerId === cachedRunnerId && snapshotVersion === cachedVersion) {
|
|
81
|
+
return cachedResult;
|
|
82
|
+
}
|
|
83
|
+
cachedRunnerId = runnerId;
|
|
84
|
+
cachedVersion = snapshotVersion;
|
|
85
|
+
cachedResult = sortEntries(readEntries(runnerId));
|
|
86
|
+
return cachedResult;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Manages recently used workspace paths for a specific runner,
|
|
91
|
+
* persisted in `localStorage`.
|
|
92
|
+
*
|
|
93
|
+
* Entries are keyed by `runner_id` so each runner maintains its own
|
|
94
|
+
* history. Pinned paths appear first, then by most-recently-used.
|
|
95
|
+
*
|
|
96
|
+
* @param runnerId - Runner to scope the history to. When `null`, returns empty.
|
|
97
|
+
*/
|
|
98
|
+
export function useRecentWorkspaces(
|
|
99
|
+
runnerId: string | null,
|
|
100
|
+
): UseRecentWorkspacesReturn {
|
|
101
|
+
const entries = useSyncExternalStore(
|
|
102
|
+
subscribe,
|
|
103
|
+
() => getSnapshot(runnerId),
|
|
104
|
+
() => EMPTY,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const recordSelection = useCallback(
|
|
108
|
+
(path: string) => {
|
|
109
|
+
if (!runnerId) return;
|
|
110
|
+
const existing = readEntries(runnerId);
|
|
111
|
+
const idx = existing.findIndex((e) => e.path === path);
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
|
|
114
|
+
let updated: RecentWorkspace[];
|
|
115
|
+
if (idx >= 0) {
|
|
116
|
+
updated = [...existing];
|
|
117
|
+
updated[idx] = { ...updated[idx], lastUsed: now };
|
|
118
|
+
} else {
|
|
119
|
+
updated = [{ path, pinned: false, lastUsed: now }, ...existing];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (updated.length > MAX_RECENT) {
|
|
123
|
+
const unpinned = updated.filter((e) => !e.pinned);
|
|
124
|
+
if (unpinned.length > 0) {
|
|
125
|
+
unpinned.sort((a, b) => a.lastUsed - b.lastUsed);
|
|
126
|
+
const oldest = unpinned[0];
|
|
127
|
+
updated = updated.filter((e) => e !== oldest);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
writeEntries(runnerId, updated);
|
|
132
|
+
notifyListeners();
|
|
133
|
+
},
|
|
134
|
+
[runnerId],
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const togglePin = useCallback(
|
|
138
|
+
(path: string) => {
|
|
139
|
+
if (!runnerId) return;
|
|
140
|
+
const existing = readEntries(runnerId);
|
|
141
|
+
const idx = existing.findIndex((e) => e.path === path);
|
|
142
|
+
if (idx < 0) return;
|
|
143
|
+
const updated = [...existing];
|
|
144
|
+
updated[idx] = { ...updated[idx], pinned: !updated[idx].pinned };
|
|
145
|
+
writeEntries(runnerId, updated);
|
|
146
|
+
notifyListeners();
|
|
147
|
+
},
|
|
148
|
+
[runnerId],
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const remove = useCallback(
|
|
152
|
+
(path: string) => {
|
|
153
|
+
if (!runnerId) return;
|
|
154
|
+
const updated = readEntries(runnerId).filter((e) => e.path !== path);
|
|
155
|
+
writeEntries(runnerId, updated);
|
|
156
|
+
notifyListeners();
|
|
157
|
+
},
|
|
158
|
+
[runnerId],
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return { entries, recordSelection, togglePin, remove };
|
|
162
|
+
}
|
|
@@ -49,6 +49,14 @@ export interface UseWorkspaceEntriesReturn {
|
|
|
49
49
|
readonly remove: (id: string) => void;
|
|
50
50
|
/** Remove all entries. */
|
|
51
51
|
readonly clear: () => void;
|
|
52
|
+
/**
|
|
53
|
+
* Remove all local folder entries, keeping git entries intact.
|
|
54
|
+
*
|
|
55
|
+
* Used when the user switches runners — local paths from the previous
|
|
56
|
+
* runner are invalid on the new runner, but git repos are
|
|
57
|
+
* runner-independent and can stay.
|
|
58
|
+
*/
|
|
59
|
+
readonly clearLocal: () => void;
|
|
52
60
|
/** Convert entries to the `WorkspaceEntryInput[]` shape required by the SDK. */
|
|
53
61
|
readonly toInput: () => WorkspaceEntryInput[];
|
|
54
62
|
/** `true` when at least one entry exists. */
|
|
@@ -133,6 +141,10 @@ export function useWorkspaceEntries(): UseWorkspaceEntriesReturn {
|
|
|
133
141
|
setEntries([]);
|
|
134
142
|
}, []);
|
|
135
143
|
|
|
144
|
+
const clearLocal = useCallback(() => {
|
|
145
|
+
setEntries((prev) => prev.filter((e) => e.type !== "local"));
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
136
148
|
const toInput = useCallback((): WorkspaceEntryInput[] => {
|
|
137
149
|
return entries.map((entry): WorkspaceEntryInput => {
|
|
138
150
|
const source: WorkspaceSourceInput =
|
|
@@ -150,6 +162,7 @@ export function useWorkspaceEntries(): UseWorkspaceEntriesReturn {
|
|
|
150
162
|
addLocalPath,
|
|
151
163
|
remove,
|
|
152
164
|
clear,
|
|
165
|
+
clearLocal,
|
|
153
166
|
toInput,
|
|
154
167
|
hasEntries: entries.length > 0,
|
|
155
168
|
};
|