@stigmer/react 0.2.2 → 0.3.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/composer/ComposerToolbar.d.ts +5 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +6 -3
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/SessionComposer.d.ts +17 -1
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +32 -35
- package/composer/SessionComposer.js.map +1 -1
- package/execution/MessageEntry.d.ts +3 -1
- package/execution/MessageEntry.d.ts.map +1 -1
- package/execution/MessageEntry.js +30 -1
- package/execution/MessageEntry.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 +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/index.js.map +1 -1
- package/models/HarnessSelector.d.ts +41 -0
- package/models/HarnessSelector.d.ts.map +1 -0
- package/models/HarnessSelector.js +74 -0
- package/models/HarnessSelector.js.map +1 -0
- package/models/ModelSelector.d.ts +26 -16
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +128 -48
- package/models/ModelSelector.js.map +1 -1
- package/models/__tests__/HarnessSelector.test.d.ts +2 -0
- package/models/__tests__/HarnessSelector.test.d.ts.map +1 -0
- package/models/__tests__/HarnessSelector.test.js +160 -0
- package/models/__tests__/HarnessSelector.test.js.map +1 -0
- package/models/__tests__/harness.test.d.ts +2 -0
- package/models/__tests__/harness.test.d.ts.map +1 -0
- package/models/__tests__/harness.test.js +50 -0
- package/models/__tests__/harness.test.js.map +1 -0
- package/models/__tests__/useModelRegistry.test.d.ts +2 -0
- package/models/__tests__/useModelRegistry.test.d.ts.map +1 -0
- package/models/__tests__/useModelRegistry.test.js +148 -0
- package/models/__tests__/useModelRegistry.test.js.map +1 -0
- package/models/harness.d.ts +21 -0
- package/models/harness.d.ts.map +1 -0
- package/models/harness.js +34 -0
- package/models/harness.js.map +1 -0
- package/models/index.d.ts +7 -2
- package/models/index.d.ts.map +1 -1
- package/models/index.js +3 -1
- package/models/index.js.map +1 -1
- package/models/registry.d.ts +53 -13
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js +51 -40
- package/models/registry.js.map +1 -1
- package/models/useModelRegistry.d.ts +39 -19
- package/models/useModelRegistry.d.ts.map +1 -1
- package/models/useModelRegistry.js +45 -23
- package/models/useModelRegistry.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/RunnerListPanel.js +2 -1
- package/runner/RunnerListPanel.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/__tests__/phase.test.js +6 -2
- package/runner/__tests__/phase.test.js.map +1 -1
- 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/phase.d.ts +9 -7
- package/runner/phase.d.ts.map +1 -1
- package/runner/phase.js +18 -12
- package/runner/phase.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/session/__tests__/useCreateSession.test.d.ts +2 -0
- package/session/__tests__/useCreateSession.test.d.ts.map +1 -0
- package/session/__tests__/useCreateSession.test.js +232 -0
- package/session/__tests__/useCreateSession.test.js.map +1 -0
- package/session/__tests__/useNewSessionFlow.test.d.ts +2 -0
- package/session/__tests__/useNewSessionFlow.test.d.ts.map +1 -0
- package/session/__tests__/useNewSessionFlow.test.js +199 -0
- package/session/__tests__/useNewSessionFlow.test.js.map +1 -0
- package/session/__tests__/useSessionConversation.test.js +37 -0
- package/session/__tests__/useSessionConversation.test.js.map +1 -1
- package/session/index.d.ts +1 -1
- package/session/index.d.ts.map +1 -1
- package/session/useCreateSession.d.ts +8 -0
- package/session/useCreateSession.d.ts.map +1 -1
- package/session/useCreateSession.js +2 -0
- package/session/useCreateSession.js.map +1 -1
- package/session/useNewSessionFlow.d.ts +6 -1
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +34 -8
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/usePersistedModel.d.ts +16 -1
- package/session/usePersistedModel.d.ts.map +1 -1
- package/session/usePersistedModel.js +15 -6
- package/session/usePersistedModel.js.map +1 -1
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +6 -1
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +11 -0
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +11 -2
- package/session/useSessionPageFlow.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/ComposerToolbar.tsx +24 -1
- package/src/composer/SessionComposer.tsx +81 -44
- package/src/execution/MessageEntry.tsx +134 -1
- 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 +16 -1
- package/src/models/HarnessSelector.tsx +130 -0
- package/src/models/ModelSelector.tsx +285 -81
- package/src/models/__tests__/HarnessSelector.test.tsx +190 -0
- package/src/models/__tests__/harness.test.ts +66 -0
- package/src/models/__tests__/useModelRegistry.test.tsx +209 -0
- package/src/models/harness.ts +45 -0
- package/src/models/index.ts +7 -2
- package/src/models/registry.ts +122 -50
- package/src/models/useModelRegistry.ts +74 -24
- package/src/organization/OrgProfilePanel.tsx +98 -0
- package/src/runner/RunnerFileBrowser.tsx +227 -8
- package/src/runner/RunnerListPanel.tsx +13 -5
- package/src/runner/WorkspaceRunnerSelector.tsx +180 -0
- package/src/runner/__tests__/phase.test.ts +6 -2
- package/src/runner/index.ts +3 -0
- package/src/runner/phase.ts +18 -12
- package/src/runner/useRunnerFileBrowser.ts +39 -3
- package/src/session/__tests__/useCreateSession.test.tsx +296 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +258 -0
- package/src/session/__tests__/useSessionConversation.test.tsx +53 -0
- package/src/session/index.ts +1 -1
- package/src/session/useCreateSession.ts +9 -0
- package/src/session/useNewSessionFlow.ts +46 -9
- package/src/session/usePersistedModel.ts +30 -6
- package/src/session/useSessionConversation.ts +6 -1
- package/src/session/useSessionPageFlow.ts +26 -2
- 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
|
@@ -12,6 +12,8 @@ import { getUserMessage } from "@stigmer/sdk";
|
|
|
12
12
|
import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
13
13
|
import { useOrganization } from "./useOrganization";
|
|
14
14
|
import { useUpdateOrganization } from "./useUpdateOrganization";
|
|
15
|
+
import { useIdentityProviderList } from "../identity-provider/useIdentityProviderList";
|
|
16
|
+
import { useResourceAvailable, ApiResourceKind } from "../deployment-mode";
|
|
15
17
|
|
|
16
18
|
// ---------------------------------------------------------------------------
|
|
17
19
|
// Constants
|
|
@@ -338,10 +340,106 @@ export function OrgProfilePanel({
|
|
|
338
340
|
</button>
|
|
339
341
|
)}
|
|
340
342
|
</div>
|
|
343
|
+
|
|
344
|
+
{/* -- Identity Providers summary -- */}
|
|
345
|
+
<IdentityProvidersSummary orgSlug={serverSlug} />
|
|
341
346
|
</form>
|
|
342
347
|
);
|
|
343
348
|
}
|
|
344
349
|
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// IdentityProvidersSummary — shows linked IDPs on the org profile
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
function IdentityProvidersSummary({ orgSlug }: { orgSlug: string }) {
|
|
355
|
+
const idpAvailable = useResourceAvailable(ApiResourceKind.identity_provider);
|
|
356
|
+
const { identityProviders, isLoading } = useIdentityProviderList(
|
|
357
|
+
idpAvailable && orgSlug ? orgSlug : null,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
if (!idpAvailable || !orgSlug) return null;
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<>
|
|
364
|
+
<hr className="border-border" />
|
|
365
|
+
<div className="space-y-2">
|
|
366
|
+
<p className="text-[0.65rem] font-medium text-muted-foreground uppercase tracking-wider">
|
|
367
|
+
Identity Providers
|
|
368
|
+
</p>
|
|
369
|
+
|
|
370
|
+
{isLoading ? (
|
|
371
|
+
<div className="bg-muted-subtle h-8 animate-pulse rounded" />
|
|
372
|
+
) : identityProviders.length === 0 ? (
|
|
373
|
+
<p className="text-xs text-muted-foreground">
|
|
374
|
+
No identity providers configured.{" "}
|
|
375
|
+
<a
|
|
376
|
+
href="/settings/identity-providers"
|
|
377
|
+
className="text-primary hover:text-primary-hover underline underline-offset-2"
|
|
378
|
+
>
|
|
379
|
+
Set up federated authentication
|
|
380
|
+
</a>
|
|
381
|
+
</p>
|
|
382
|
+
) : (
|
|
383
|
+
<div className="space-y-1.5">
|
|
384
|
+
{identityProviders.map((idp) => {
|
|
385
|
+
const spec = idp.spec;
|
|
386
|
+
const displayName =
|
|
387
|
+
spec?.displayName || idp.metadata?.name || "Unnamed";
|
|
388
|
+
const isSso = spec?.isSsoProvider;
|
|
389
|
+
const isJit = !isSso && spec?.autoProvisionAccounts;
|
|
390
|
+
|
|
391
|
+
return (
|
|
392
|
+
<div
|
|
393
|
+
key={idp.metadata?.id}
|
|
394
|
+
className="flex items-center gap-2 text-xs"
|
|
395
|
+
>
|
|
396
|
+
<ShieldSmallIcon />
|
|
397
|
+
<span className="text-foreground truncate">{displayName}</span>
|
|
398
|
+
{isSso && (
|
|
399
|
+
<span className="rounded-full border border-primary/30 bg-primary-subtle px-1.5 py-px text-[0.6rem] font-medium text-primary">
|
|
400
|
+
SSO
|
|
401
|
+
</span>
|
|
402
|
+
)}
|
|
403
|
+
{isJit && (
|
|
404
|
+
<span className="rounded-full border border-primary/30 bg-primary-subtle px-1.5 py-px text-[0.6rem] font-medium text-primary">
|
|
405
|
+
JIT
|
|
406
|
+
</span>
|
|
407
|
+
)}
|
|
408
|
+
</div>
|
|
409
|
+
);
|
|
410
|
+
})}
|
|
411
|
+
<a
|
|
412
|
+
href="/settings/identity-providers"
|
|
413
|
+
className="inline-block text-[0.65rem] text-primary hover:text-primary-hover underline underline-offset-2"
|
|
414
|
+
>
|
|
415
|
+
Manage
|
|
416
|
+
</a>
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
</div>
|
|
420
|
+
</>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function ShieldSmallIcon() {
|
|
425
|
+
return (
|
|
426
|
+
<svg
|
|
427
|
+
width="12"
|
|
428
|
+
height="12"
|
|
429
|
+
viewBox="0 0 16 16"
|
|
430
|
+
fill="none"
|
|
431
|
+
stroke="currentColor"
|
|
432
|
+
strokeWidth="1.5"
|
|
433
|
+
strokeLinecap="round"
|
|
434
|
+
strokeLinejoin="round"
|
|
435
|
+
aria-hidden="true"
|
|
436
|
+
className="shrink-0 text-muted-foreground"
|
|
437
|
+
>
|
|
438
|
+
<path d="M8 1.5L2 4v4c0 3.5 2.5 5.5 6 7 3.5-1.5 6-3.5 6-7V4L8 1.5z" />
|
|
439
|
+
</svg>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
345
443
|
// ---------------------------------------------------------------------------
|
|
346
444
|
// ReadOnlyField — copiable label/value pair
|
|
347
445
|
// ---------------------------------------------------------------------------
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useMemo } from "react";
|
|
3
|
+
import { useCallback, useMemo, useState, useRef, type KeyboardEvent } from "react";
|
|
4
4
|
import { getUserMessage } from "@stigmer/sdk";
|
|
5
5
|
import { useRunnerFileBrowser } from "./useRunnerFileBrowser";
|
|
6
|
+
import { useRecentWorkspaces, type RecentWorkspace } from "../workspace/useRecentWorkspaces";
|
|
6
7
|
|
|
7
8
|
/** Props for {@link RunnerFileBrowser}. */
|
|
8
9
|
export interface RunnerFileBrowserProps {
|
|
@@ -14,6 +15,16 @@ export interface RunnerFileBrowserProps {
|
|
|
14
15
|
readonly onCancel: () => void;
|
|
15
16
|
/** Additional CSS class names for the root container. */
|
|
16
17
|
readonly className?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Display name of the runner (e.g. "dev-macbook").
|
|
20
|
+
* Shown in the context header so users know which machine they're browsing.
|
|
21
|
+
*/
|
|
22
|
+
readonly runnerName?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Hostname of the runner's machine (e.g. "Alice's MacBook Pro").
|
|
25
|
+
* Shown alongside the runner name in the context header.
|
|
26
|
+
*/
|
|
27
|
+
readonly runnerHostname?: string;
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
/**
|
|
@@ -41,8 +52,54 @@ export function RunnerFileBrowser({
|
|
|
41
52
|
onSelect,
|
|
42
53
|
onCancel,
|
|
43
54
|
className,
|
|
55
|
+
runnerName,
|
|
56
|
+
runnerHostname,
|
|
44
57
|
}: RunnerFileBrowserProps) {
|
|
45
58
|
const browser = useRunnerFileBrowser(runnerId);
|
|
59
|
+
const recents = useRecentWorkspaces(runnerId);
|
|
60
|
+
|
|
61
|
+
const handleSelect = useCallback(
|
|
62
|
+
(path: string) => {
|
|
63
|
+
recents.recordSelection(path);
|
|
64
|
+
onSelect(path);
|
|
65
|
+
},
|
|
66
|
+
[recents, onSelect],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const [isEditingPath, setIsEditingPath] = useState(false);
|
|
70
|
+
const [editPathValue, setEditPathValue] = useState("");
|
|
71
|
+
const pathInputRef = useRef<HTMLInputElement>(null);
|
|
72
|
+
|
|
73
|
+
const startEditingPath = useCallback(() => {
|
|
74
|
+
setEditPathValue(browser.currentPath);
|
|
75
|
+
setIsEditingPath(true);
|
|
76
|
+
requestAnimationFrame(() => pathInputRef.current?.select());
|
|
77
|
+
}, [browser.currentPath]);
|
|
78
|
+
|
|
79
|
+
const commitPath = useCallback(() => {
|
|
80
|
+
const trimmed = editPathValue.trim();
|
|
81
|
+
if (trimmed && trimmed !== browser.currentPath) {
|
|
82
|
+
browser.navigateToPath(trimmed);
|
|
83
|
+
}
|
|
84
|
+
setIsEditingPath(false);
|
|
85
|
+
}, [editPathValue, browser]);
|
|
86
|
+
|
|
87
|
+
const cancelEditing = useCallback(() => {
|
|
88
|
+
setIsEditingPath(false);
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
const handlePathKeyDown = useCallback(
|
|
92
|
+
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
93
|
+
if (e.key === "Enter") {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
commitPath();
|
|
96
|
+
} else if (e.key === "Escape") {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
cancelEditing();
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
[commitPath, cancelEditing],
|
|
102
|
+
);
|
|
46
103
|
|
|
47
104
|
const visibleEntries = useMemo(
|
|
48
105
|
() =>
|
|
@@ -77,6 +134,33 @@ export function RunnerFileBrowser({
|
|
|
77
134
|
|
|
78
135
|
return (
|
|
79
136
|
<div className={["space-y-2", className].filter(Boolean).join(" ")}>
|
|
137
|
+
{/* Runner context header */}
|
|
138
|
+
{(runnerName || runnerHostname) && (
|
|
139
|
+
<div className="flex items-center gap-1.5 text-[0.65rem] text-muted-foreground">
|
|
140
|
+
<ServerIcon />
|
|
141
|
+
<span className="truncate">
|
|
142
|
+
{runnerName ?? runnerHostname}
|
|
143
|
+
{runnerName && runnerHostname && (
|
|
144
|
+
<span className="text-border"> · {runnerHostname}</span>
|
|
145
|
+
)}
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{/* Recent / favorite paths */}
|
|
151
|
+
{recents.entries.length > 0 && (
|
|
152
|
+
<RecentPathsList
|
|
153
|
+
entries={recents.entries}
|
|
154
|
+
onSelect={(path) => {
|
|
155
|
+
recents.recordSelection(path);
|
|
156
|
+
onSelect(path);
|
|
157
|
+
}}
|
|
158
|
+
onTogglePin={recents.togglePin}
|
|
159
|
+
onRemove={recents.remove}
|
|
160
|
+
onNavigate={browser.navigateToPath}
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
163
|
+
|
|
80
164
|
{/* Navigation bar: shortcuts + breadcrumb */}
|
|
81
165
|
<div className="space-y-1.5">
|
|
82
166
|
{/* Shortcut buttons */}
|
|
@@ -121,16 +205,26 @@ export function RunnerFileBrowser({
|
|
|
121
205
|
</div>
|
|
122
206
|
</div>
|
|
123
207
|
|
|
124
|
-
{/*
|
|
125
|
-
{
|
|
208
|
+
{/* Path bar — editable input or clickable breadcrumb */}
|
|
209
|
+
{isEditingPath ? (
|
|
210
|
+
<input
|
|
211
|
+
ref={pathInputRef}
|
|
212
|
+
type="text"
|
|
213
|
+
value={editPathValue}
|
|
214
|
+
onChange={(e) => setEditPathValue(e.target.value)}
|
|
215
|
+
onKeyDown={handlePathKeyDown}
|
|
216
|
+
onBlur={commitPath}
|
|
217
|
+
className="w-full rounded-md border border-input bg-background px-2 py-1 text-[0.65rem] text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
218
|
+
placeholder="/path/to/directory or ~/relative"
|
|
219
|
+
autoFocus
|
|
220
|
+
/>
|
|
221
|
+
) : browser.segments.length > 0 ? (
|
|
126
222
|
<div className="flex items-center gap-0.5 overflow-x-auto text-[0.65rem] scrollbar-none">
|
|
127
223
|
{browser.segments.map((seg, i) => {
|
|
128
224
|
const isLast = i === browser.segments.length - 1;
|
|
129
225
|
return (
|
|
130
226
|
<span key={seg.path} className="flex shrink-0 items-center gap-0.5">
|
|
131
|
-
{i > 0 &&
|
|
132
|
-
<ChevronRightIcon />
|
|
133
|
-
)}
|
|
227
|
+
{i > 0 && <ChevronRightIcon />}
|
|
134
228
|
{isLast ? (
|
|
135
229
|
<span className="rounded px-1 py-0.5 font-medium text-foreground">
|
|
136
230
|
{seg.name}
|
|
@@ -148,8 +242,17 @@ export function RunnerFileBrowser({
|
|
|
148
242
|
</span>
|
|
149
243
|
);
|
|
150
244
|
})}
|
|
245
|
+
<button
|
|
246
|
+
type="button"
|
|
247
|
+
onClick={startEditingPath}
|
|
248
|
+
className="ml-1 shrink-0 rounded p-0.5 text-muted-foreground hover:text-foreground hover:bg-accent-hover transition-colors"
|
|
249
|
+
title="Type a path directly"
|
|
250
|
+
aria-label="Edit path"
|
|
251
|
+
>
|
|
252
|
+
<EditIcon />
|
|
253
|
+
</button>
|
|
151
254
|
</div>
|
|
152
|
-
)}
|
|
255
|
+
) : null}
|
|
153
256
|
</div>
|
|
154
257
|
|
|
155
258
|
{/* Inline error banner (when we already have a current path) */}
|
|
@@ -226,7 +329,7 @@ export function RunnerFileBrowser({
|
|
|
226
329
|
</button>
|
|
227
330
|
<button
|
|
228
331
|
type="button"
|
|
229
|
-
onClick={() =>
|
|
332
|
+
onClick={() => handleSelect(browser.currentPath)}
|
|
230
333
|
disabled={!browser.currentPath || browser.isLoading}
|
|
231
334
|
className="rounded-md bg-primary px-2.5 py-1 text-xs text-primary-foreground hover:bg-primary-hover transition-colors disabled:opacity-40"
|
|
232
335
|
>
|
|
@@ -281,6 +384,87 @@ function ErrorDisplay({
|
|
|
281
384
|
);
|
|
282
385
|
}
|
|
283
386
|
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// Recent paths list
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
|
|
391
|
+
function RecentPathsList({
|
|
392
|
+
entries,
|
|
393
|
+
onSelect,
|
|
394
|
+
onTogglePin,
|
|
395
|
+
onRemove,
|
|
396
|
+
onNavigate,
|
|
397
|
+
}: {
|
|
398
|
+
entries: readonly RecentWorkspace[];
|
|
399
|
+
onSelect: (path: string) => void;
|
|
400
|
+
onTogglePin: (path: string) => void;
|
|
401
|
+
onRemove: (path: string) => void;
|
|
402
|
+
onNavigate: (path: string) => void;
|
|
403
|
+
}) {
|
|
404
|
+
return (
|
|
405
|
+
<div className="space-y-1">
|
|
406
|
+
<span className="text-[0.6rem] font-medium text-muted-foreground">
|
|
407
|
+
Recent
|
|
408
|
+
</span>
|
|
409
|
+
<div className="max-h-24 overflow-y-auto rounded-md border border-border">
|
|
410
|
+
{entries.map((entry) => {
|
|
411
|
+
const dirName = entry.path.split("/").filter(Boolean).pop() ?? entry.path;
|
|
412
|
+
return (
|
|
413
|
+
<div
|
|
414
|
+
key={entry.path}
|
|
415
|
+
className="group flex items-center gap-1.5 px-2 py-1 text-xs transition-colors hover:bg-accent-hover"
|
|
416
|
+
>
|
|
417
|
+
<button
|
|
418
|
+
type="button"
|
|
419
|
+
onClick={() => onTogglePin(entry.path)}
|
|
420
|
+
className={[
|
|
421
|
+
"shrink-0 transition-colors",
|
|
422
|
+
entry.pinned
|
|
423
|
+
? "text-foreground"
|
|
424
|
+
: "text-transparent group-hover:text-muted-foreground",
|
|
425
|
+
].join(" ")}
|
|
426
|
+
title={entry.pinned ? "Unpin" : "Pin"}
|
|
427
|
+
aria-label={entry.pinned ? `Unpin ${dirName}` : `Pin ${dirName}`}
|
|
428
|
+
>
|
|
429
|
+
<PinIcon />
|
|
430
|
+
</button>
|
|
431
|
+
<button
|
|
432
|
+
type="button"
|
|
433
|
+
onClick={() => onSelect(entry.path)}
|
|
434
|
+
className="flex min-w-0 flex-1 items-center gap-1.5 text-left text-foreground"
|
|
435
|
+
title={`Select ${entry.path}`}
|
|
436
|
+
>
|
|
437
|
+
<FolderIcon />
|
|
438
|
+
<span className="truncate [direction:rtl] text-left">
|
|
439
|
+
<bdi>{entry.path}</bdi>
|
|
440
|
+
</span>
|
|
441
|
+
</button>
|
|
442
|
+
<button
|
|
443
|
+
type="button"
|
|
444
|
+
onClick={() => onNavigate(entry.path)}
|
|
445
|
+
className="shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 hover:text-foreground"
|
|
446
|
+
title="Browse this directory"
|
|
447
|
+
aria-label={`Browse ${dirName}`}
|
|
448
|
+
>
|
|
449
|
+
<ChevronRightIcon />
|
|
450
|
+
</button>
|
|
451
|
+
<button
|
|
452
|
+
type="button"
|
|
453
|
+
onClick={() => onRemove(entry.path)}
|
|
454
|
+
className="shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 hover:text-destructive"
|
|
455
|
+
title="Remove from recent"
|
|
456
|
+
aria-label={`Remove ${dirName} from recent`}
|
|
457
|
+
>
|
|
458
|
+
<XSmallIcon />
|
|
459
|
+
</button>
|
|
460
|
+
</div>
|
|
461
|
+
);
|
|
462
|
+
})}
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
284
468
|
// ---------------------------------------------------------------------------
|
|
285
469
|
// Shortcut button
|
|
286
470
|
// ---------------------------------------------------------------------------
|
|
@@ -334,6 +518,41 @@ function LoadingSkeleton() {
|
|
|
334
518
|
// Icons (inline SVG, consistent with existing SDK icon patterns)
|
|
335
519
|
// ---------------------------------------------------------------------------
|
|
336
520
|
|
|
521
|
+
function EditIcon() {
|
|
522
|
+
return (
|
|
523
|
+
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
524
|
+
<path d="M8.5 1.5L10.5 3.5L4 10H2V8L8.5 1.5Z" />
|
|
525
|
+
</svg>
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function PinIcon() {
|
|
530
|
+
return (
|
|
531
|
+
<svg width="10" height="10" viewBox="0 0 12 12" fill="currentColor" stroke="none">
|
|
532
|
+
<path d="M7.5 1.5L10.5 4.5L8 7L9 10L6 7L2 11L5 3L4.5 1.5L7.5 1.5Z" />
|
|
533
|
+
</svg>
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function XSmallIcon() {
|
|
538
|
+
return (
|
|
539
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
540
|
+
<path d="M2.5 2.5L7.5 7.5M7.5 2.5L2.5 7.5" />
|
|
541
|
+
</svg>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function ServerIcon() {
|
|
546
|
+
return (
|
|
547
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="shrink-0">
|
|
548
|
+
<rect x="1.5" y="1.5" width="9" height="3.5" rx="0.5" />
|
|
549
|
+
<rect x="1.5" y="7" width="9" height="3.5" rx="0.5" />
|
|
550
|
+
<circle cx="3.5" cy="3.25" r="0.5" fill="currentColor" stroke="none" />
|
|
551
|
+
<circle cx="3.5" cy="8.75" r="0.5" fill="currentColor" stroke="none" />
|
|
552
|
+
</svg>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
337
556
|
function HomeIcon() {
|
|
338
557
|
return (
|
|
339
558
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
@@ -576,13 +576,21 @@ function ActionMenu({
|
|
|
576
576
|
// ---------------------------------------------------------------------------
|
|
577
577
|
|
|
578
578
|
function PhaseBadge({ phase }: { phase: RunnerPhase }) {
|
|
579
|
+
const starting = phase === RunnerPhase.STARTING;
|
|
579
580
|
return (
|
|
580
581
|
<span className="inline-flex shrink-0 items-center gap-1">
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
582
|
+
{starting ? (
|
|
583
|
+
<span
|
|
584
|
+
className="inline-block h-2.5 w-2.5 animate-spin rounded-full border border-primary border-t-transparent"
|
|
585
|
+
aria-hidden="true"
|
|
586
|
+
/>
|
|
587
|
+
) : (
|
|
588
|
+
<span
|
|
589
|
+
className={`inline-block h-1.5 w-1.5 rounded-full ${phaseDotColor(phase)}`}
|
|
590
|
+
aria-hidden="true"
|
|
591
|
+
/>
|
|
592
|
+
)}
|
|
593
|
+
<span className={cn("text-[0.65rem]", starting ? "text-primary" : "text-muted-foreground")}>
|
|
586
594
|
{phaseLabel(phase)}
|
|
587
595
|
</span>
|
|
588
596
|
</span>
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import type { Runner } from "@stigmer/protos/ai/stigmer/agentic/runner/v1/api_pb";
|
|
6
|
+
import { RunnerPhase } from "@stigmer/protos/ai/stigmer/agentic/runner/v1/enum_pb";
|
|
7
|
+
import { useRunnerList } from "./useRunnerList";
|
|
8
|
+
import { isActivePhase, phaseDotColor, PHASE_SORT_ORDER } from "./phase";
|
|
9
|
+
|
|
10
|
+
/** Props for {@link WorkspaceRunnerSelector}. */
|
|
11
|
+
export interface WorkspaceRunnerSelectorProps {
|
|
12
|
+
/** Organization slug to scope the runner list. */
|
|
13
|
+
readonly org: string;
|
|
14
|
+
/** Currently selected runner ID, or `null` for "Auto". */
|
|
15
|
+
readonly value: string | null;
|
|
16
|
+
/** Called when the user selects a runner. `null` means "Auto". */
|
|
17
|
+
readonly onChange: (runnerId: string | null) => void;
|
|
18
|
+
/** Disables all interactions. */
|
|
19
|
+
readonly disabled?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Set of runner IDs that are known to be running on the local machine.
|
|
22
|
+
*
|
|
23
|
+
* When provided, matching runners are labeled "This machine" instead
|
|
24
|
+
* of their name/hostname. This is a host-provided signal — the desktop
|
|
25
|
+
* app determines local runners from `~/.stigmer/runners/` state files;
|
|
26
|
+
* the web console does not pass this prop.
|
|
27
|
+
*/
|
|
28
|
+
readonly localRunnerIds?: ReadonlySet<string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Cursor-inspired "Run On" section for the workspace popover.
|
|
33
|
+
*
|
|
34
|
+
* Renders a compact, sectioned list of available runners so users can
|
|
35
|
+
* pick where their session runs without leaving the workspace context.
|
|
36
|
+
* Designed to be composed alongside {@link WorkspaceEditor} inside the
|
|
37
|
+
* workspace popover — NOT as a standalone picker.
|
|
38
|
+
*
|
|
39
|
+
* Label strategy (SDK-agnostic, no platform branding):
|
|
40
|
+
* - "Auto" for null selection (platform assigns a runner)
|
|
41
|
+
* - "Cloud" for system-managed runners
|
|
42
|
+
* - "This machine" when the host provides `localRunnerIds`
|
|
43
|
+
* - Runner name / hostname for everything else
|
|
44
|
+
*/
|
|
45
|
+
export function WorkspaceRunnerSelector({
|
|
46
|
+
org,
|
|
47
|
+
value,
|
|
48
|
+
onChange,
|
|
49
|
+
disabled,
|
|
50
|
+
localRunnerIds,
|
|
51
|
+
}: WorkspaceRunnerSelectorProps) {
|
|
52
|
+
const { runners, isLoading } = useRunnerList(org);
|
|
53
|
+
|
|
54
|
+
const active = useMemo(() => {
|
|
55
|
+
return runners
|
|
56
|
+
.filter((r) => isActivePhase(r.status?.phase ?? RunnerPhase.UNSPECIFIED))
|
|
57
|
+
.sort((a, b) => {
|
|
58
|
+
const pa = a.status?.phase ?? RunnerPhase.UNSPECIFIED;
|
|
59
|
+
const pb = b.status?.phase ?? RunnerPhase.UNSPECIFIED;
|
|
60
|
+
const po = PHASE_SORT_ORDER[pa] - PHASE_SORT_ORDER[pb];
|
|
61
|
+
if (po !== 0) return po;
|
|
62
|
+
return (a.metadata?.name ?? "").localeCompare(b.metadata?.name ?? "");
|
|
63
|
+
});
|
|
64
|
+
}, [runners]);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="space-y-1" role="listbox" aria-label="Run on">
|
|
68
|
+
<div className="px-1 text-[0.6rem] font-medium uppercase tracking-wider text-muted-foreground">
|
|
69
|
+
Run On
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{/* Auto option */}
|
|
73
|
+
<RunOnOption
|
|
74
|
+
selected={value === null}
|
|
75
|
+
onClick={() => onChange(null)}
|
|
76
|
+
disabled={disabled}
|
|
77
|
+
label="Auto"
|
|
78
|
+
hint="platform assigns"
|
|
79
|
+
dotClass="bg-primary"
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
{/* Available runners */}
|
|
83
|
+
{active.map((r) => {
|
|
84
|
+
const id = r.metadata!.id;
|
|
85
|
+
return (
|
|
86
|
+
<RunOnOption
|
|
87
|
+
key={id}
|
|
88
|
+
selected={value === id}
|
|
89
|
+
onClick={() => onChange(id)}
|
|
90
|
+
disabled={disabled}
|
|
91
|
+
label={runnerLabel(r, localRunnerIds)}
|
|
92
|
+
hint={runnerHint(r, localRunnerIds)}
|
|
93
|
+
dotClass={phaseDotColor(r.status?.phase ?? RunnerPhase.UNSPECIFIED)}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
})}
|
|
97
|
+
|
|
98
|
+
{/* Loading */}
|
|
99
|
+
{isLoading && active.length === 0 && (
|
|
100
|
+
<div className="px-1 py-2 text-[0.65rem] text-muted-foreground">
|
|
101
|
+
Loading runners...
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{/* Empty */}
|
|
106
|
+
{!isLoading && active.length === 0 && (
|
|
107
|
+
<div className="px-1 py-2 text-[0.65rem] text-muted-foreground">
|
|
108
|
+
No runners available
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function RunOnOption({
|
|
116
|
+
selected,
|
|
117
|
+
onClick,
|
|
118
|
+
disabled,
|
|
119
|
+
label,
|
|
120
|
+
hint,
|
|
121
|
+
dotClass,
|
|
122
|
+
}: {
|
|
123
|
+
selected: boolean;
|
|
124
|
+
onClick: () => void;
|
|
125
|
+
disabled?: boolean;
|
|
126
|
+
label: string;
|
|
127
|
+
hint?: string;
|
|
128
|
+
dotClass: string;
|
|
129
|
+
}) {
|
|
130
|
+
return (
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
onClick={onClick}
|
|
134
|
+
disabled={disabled}
|
|
135
|
+
className={cn(
|
|
136
|
+
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs transition-colors",
|
|
137
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
138
|
+
selected
|
|
139
|
+
? "bg-accent font-medium text-foreground"
|
|
140
|
+
: "text-foreground hover:bg-accent-hover",
|
|
141
|
+
)}
|
|
142
|
+
role="option"
|
|
143
|
+
aria-selected={selected}
|
|
144
|
+
>
|
|
145
|
+
<span
|
|
146
|
+
className={cn("inline-block h-1.5 w-1.5 shrink-0 rounded-full", dotClass)}
|
|
147
|
+
aria-hidden="true"
|
|
148
|
+
/>
|
|
149
|
+
<span className="min-w-0 flex-1 truncate">{label}</span>
|
|
150
|
+
{hint && (
|
|
151
|
+
<span className="shrink-0 text-[0.6rem] text-muted-foreground">
|
|
152
|
+
{hint}
|
|
153
|
+
</span>
|
|
154
|
+
)}
|
|
155
|
+
</button>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function runnerLabel(
|
|
160
|
+
runner: Runner,
|
|
161
|
+
localRunnerIds?: ReadonlySet<string>,
|
|
162
|
+
): string {
|
|
163
|
+
const id = runner.metadata?.id ?? "";
|
|
164
|
+
if (localRunnerIds?.has(id)) return "This machine";
|
|
165
|
+
return runner.metadata?.name ?? runner.status?.connectionInfo?.hostname ?? "Unnamed";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function runnerHint(
|
|
169
|
+
runner: Runner,
|
|
170
|
+
localRunnerIds?: ReadonlySet<string>,
|
|
171
|
+
): string | undefined {
|
|
172
|
+
const id = runner.metadata?.id ?? "";
|
|
173
|
+
if (localRunnerIds?.has(id)) {
|
|
174
|
+
return runner.status?.connectionInfo?.hostname;
|
|
175
|
+
}
|
|
176
|
+
const name = runner.metadata?.name;
|
|
177
|
+
const hostname = runner.status?.connectionInfo?.hostname;
|
|
178
|
+
if (name && hostname && name !== hostname) return hostname;
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
@@ -3,8 +3,11 @@ import { RunnerPhase } from "@stigmer/protos/ai/stigmer/agentic/runner/v1/enum_p
|
|
|
3
3
|
import { isTransitionalPhase, isActivePhase } from "../phase";
|
|
4
4
|
|
|
5
5
|
describe("isTransitionalPhase", () => {
|
|
6
|
-
it(
|
|
7
|
-
|
|
6
|
+
it.each([
|
|
7
|
+
["PENDING", RunnerPhase.PENDING],
|
|
8
|
+
["STARTING", RunnerPhase.STARTING],
|
|
9
|
+
] as const)("returns true for %s", (_label, phase) => {
|
|
10
|
+
expect(isTransitionalPhase(phase)).toBe(true);
|
|
8
11
|
});
|
|
9
12
|
|
|
10
13
|
it.each([
|
|
@@ -21,6 +24,7 @@ describe("isTransitionalPhase", () => {
|
|
|
21
24
|
const allPhases = [
|
|
22
25
|
RunnerPhase.READY,
|
|
23
26
|
RunnerPhase.BUSY,
|
|
27
|
+
RunnerPhase.STARTING,
|
|
24
28
|
RunnerPhase.PENDING,
|
|
25
29
|
RunnerPhase.STOPPED,
|
|
26
30
|
RunnerPhase.FAILED,
|
package/src/runner/index.ts
CHANGED
|
@@ -41,6 +41,9 @@ export type {
|
|
|
41
41
|
export { RunnerListPanel } from "./RunnerListPanel";
|
|
42
42
|
export type { RunnerListPanelProps } from "./RunnerListPanel";
|
|
43
43
|
|
|
44
|
+
export { WorkspaceRunnerSelector } from "./WorkspaceRunnerSelector";
|
|
45
|
+
export type { WorkspaceRunnerSelectorProps } from "./WorkspaceRunnerSelector";
|
|
46
|
+
|
|
44
47
|
export {
|
|
45
48
|
phaseLabel,
|
|
46
49
|
phaseDotColor,
|