@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
|
@@ -6,6 +6,8 @@ import { getUserMessage, type AttachmentInput, type EnvVarInput, type McpServerU
|
|
|
6
6
|
import { useComposer } from "./useComposer";
|
|
7
7
|
import { ComposerToolbar } from "./ComposerToolbar";
|
|
8
8
|
import { type ConfigureMenuItem } from "./ConfigureMenu";
|
|
9
|
+
import type { HarnessOption } from "../models/harness";
|
|
10
|
+
import { parseModelKey } from "../models/registry";
|
|
9
11
|
import { ContextChip, type ChipItem } from "./ContextChip";
|
|
10
12
|
import { WorkspaceEditor } from "../workspace/WorkspaceEditor";
|
|
11
13
|
import { AgentPicker } from "../agent/AgentPicker";
|
|
@@ -21,6 +23,7 @@ import type { UseSessionVariablesReturn } from "../execution/useSessionVariables
|
|
|
21
23
|
import type { UseWorkspaceEntriesReturn } from "../workspace/useWorkspaceEntries";
|
|
22
24
|
import type { UseGitHubConnectionReturn } from "../github/useGitHubConnection";
|
|
23
25
|
import { useRunnerList } from "../runner/useRunnerList";
|
|
26
|
+
import { WorkspaceRunnerSelector } from "../runner/WorkspaceRunnerSelector";
|
|
24
27
|
import { useAttachments } from "../attachment/useAttachments";
|
|
25
28
|
import { AttachmentChipList } from "../attachment/AttachmentChipList";
|
|
26
29
|
import { useSessionEnvPool } from "../environment/useSessionEnvPool";
|
|
@@ -100,6 +103,22 @@ export interface SessionComposerProps {
|
|
|
100
103
|
/** Disables the entire composer (e.g., while an execution streams). */
|
|
101
104
|
readonly disabled?: boolean;
|
|
102
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Currently selected execution harness.
|
|
108
|
+
*
|
|
109
|
+
* Controls which models appear in the model selector and flows
|
|
110
|
+
* through to session creation. When omitted, defaults to `"native"`.
|
|
111
|
+
*/
|
|
112
|
+
readonly harness?: HarnessOption;
|
|
113
|
+
/**
|
|
114
|
+
* Called when the user switches the harness.
|
|
115
|
+
*
|
|
116
|
+
* Providing this callback enables the harness selector in the toolbar.
|
|
117
|
+
*/
|
|
118
|
+
readonly onHarnessChange?: (harness: HarnessOption) => void;
|
|
119
|
+
/** Show the harness selector in the toolbar. @default false */
|
|
120
|
+
readonly showHarnessSelector?: boolean;
|
|
121
|
+
|
|
103
122
|
/** Initial model ID for the model selector. */
|
|
104
123
|
readonly defaultModelId?: string;
|
|
105
124
|
/** Called when the user changes the selected model. */
|
|
@@ -356,6 +375,9 @@ export function SessionComposer({
|
|
|
356
375
|
onSubmit,
|
|
357
376
|
isSubmitting = false,
|
|
358
377
|
disabled = false,
|
|
378
|
+
harness,
|
|
379
|
+
onHarnessChange,
|
|
380
|
+
showHarnessSelector = false,
|
|
359
381
|
defaultModelId,
|
|
360
382
|
onModelChange,
|
|
361
383
|
showModelSelector = true,
|
|
@@ -411,20 +433,31 @@ export function SessionComposer({
|
|
|
411
433
|
needsRunnerList ? (org ?? null) : null,
|
|
412
434
|
);
|
|
413
435
|
|
|
414
|
-
const
|
|
436
|
+
const selectedRunner = useMemo(() => {
|
|
415
437
|
if (!runnerId) return undefined;
|
|
416
|
-
|
|
417
|
-
return runner?.metadata?.name;
|
|
438
|
+
return runnerListForBrowse.find((r) => r.metadata?.id === runnerId);
|
|
418
439
|
}, [runnerId, runnerListForBrowse]);
|
|
419
440
|
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
441
|
+
const selectedRunnerName = selectedRunner?.metadata?.name;
|
|
442
|
+
const selectedRunnerHostname = selectedRunner?.status?.connectionInfo?.hostname;
|
|
443
|
+
|
|
444
|
+
const browseRunnerId = runnerId ?? null;
|
|
445
|
+
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
// Runner-switch safety — clear local workspace entries when runner changes
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
const prevRunnerIdRef = useRef(runnerId);
|
|
451
|
+
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
if (prevRunnerIdRef.current !== runnerId) {
|
|
454
|
+
const hadLocal = workspace?.entries.some((e) => e.type === "local");
|
|
455
|
+
prevRunnerIdRef.current = runnerId;
|
|
456
|
+
if (hadLocal) {
|
|
457
|
+
workspace?.clearLocal();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}, [runnerId, workspace]);
|
|
428
461
|
|
|
429
462
|
// ---------------------------------------------------------------------------
|
|
430
463
|
// Configure menu state — drives the Tier 2 drill-down popover
|
|
@@ -600,7 +633,10 @@ export function SessionComposer({
|
|
|
600
633
|
}
|
|
601
634
|
: undefined;
|
|
602
635
|
|
|
603
|
-
|
|
636
|
+
const resolvedModelId = modelId
|
|
637
|
+
? (parseModelKey(modelId)?.modelId ?? modelId)
|
|
638
|
+
: undefined;
|
|
639
|
+
onSubmit(message, resolvedModelId, context);
|
|
604
640
|
|
|
605
641
|
if (enableAttachments) {
|
|
606
642
|
attachments.clear();
|
|
@@ -622,6 +658,13 @@ export function SessionComposer({
|
|
|
622
658
|
[onModelChange],
|
|
623
659
|
);
|
|
624
660
|
|
|
661
|
+
const handleHarnessChange = useCallback(
|
|
662
|
+
(h: HarnessOption) => {
|
|
663
|
+
onHarnessChange?.(h);
|
|
664
|
+
},
|
|
665
|
+
[onHarnessChange],
|
|
666
|
+
);
|
|
667
|
+
|
|
625
668
|
const handleDisplayNameResolved = useCallback(
|
|
626
669
|
(key: string, name: string) => {
|
|
627
670
|
setDisplayNames((prev) => {
|
|
@@ -883,17 +926,6 @@ export function SessionComposer({
|
|
|
883
926
|
});
|
|
884
927
|
}
|
|
885
928
|
|
|
886
|
-
if (workspace) {
|
|
887
|
-
for (const entry of workspace.entries) {
|
|
888
|
-
items.push({
|
|
889
|
-
key: `ws:${entry.id}`,
|
|
890
|
-
label: entry.name,
|
|
891
|
-
type: "workspace",
|
|
892
|
-
onRemove: () => workspace.remove(entry.id),
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
929
|
if (showMcp) {
|
|
898
930
|
for (const [key, entry] of Object.entries(mcpSetup.entries)) {
|
|
899
931
|
const slug = key.slice(key.indexOf("/") + 1);
|
|
@@ -946,15 +978,6 @@ export function SessionComposer({
|
|
|
946
978
|
}
|
|
947
979
|
}
|
|
948
980
|
|
|
949
|
-
if (showRunner && runnerId) {
|
|
950
|
-
items.push({
|
|
951
|
-
key: `runner:${runnerId}`,
|
|
952
|
-
label: selectedRunnerName ?? "Runner",
|
|
953
|
-
type: "runner",
|
|
954
|
-
onRemove: () => onRunnerIdChange?.(null),
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
|
|
958
981
|
if (sessionVariables) {
|
|
959
982
|
for (const entry of sessionVariables.entries) {
|
|
960
983
|
const k = entry.key.trim();
|
|
@@ -1057,7 +1080,7 @@ export function SessionComposer({
|
|
|
1057
1080
|
count: skillCount,
|
|
1058
1081
|
});
|
|
1059
1082
|
}
|
|
1060
|
-
if (showRunner) {
|
|
1083
|
+
if (showRunner && !showWorkspace) {
|
|
1061
1084
|
items.push({
|
|
1062
1085
|
id: "runner",
|
|
1063
1086
|
icon: <RunnerIcon />,
|
|
@@ -1074,7 +1097,7 @@ export function SessionComposer({
|
|
|
1074
1097
|
});
|
|
1075
1098
|
}
|
|
1076
1099
|
return items;
|
|
1077
|
-
}, [showAgent, agentRef, agentSetup.state, showMcp, mcpCount, mcpSetup.needsSetupCount, showSkills, skillCount, showRunner, runnerId, showSessionVars, sessionVarCount]);
|
|
1100
|
+
}, [showAgent, agentRef, agentSetup.state, showMcp, mcpCount, mcpSetup.needsSetupCount, showSkills, skillCount, showRunner, showWorkspace, runnerId, showSessionVars, sessionVarCount]);
|
|
1078
1101
|
|
|
1079
1102
|
const renderConfigPanel = useCallback(
|
|
1080
1103
|
(panelId: string): React.ReactNode => {
|
|
@@ -1356,16 +1379,27 @@ export function SessionComposer({
|
|
|
1356
1379
|
workspaceCount={workspaceCount}
|
|
1357
1380
|
workspaceContent={
|
|
1358
1381
|
workspace
|
|
1359
|
-
? <
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1382
|
+
? <div className="space-y-3">
|
|
1383
|
+
{showRunner && org && (
|
|
1384
|
+
<WorkspaceRunnerSelector
|
|
1385
|
+
org={org}
|
|
1386
|
+
value={runnerId ?? null}
|
|
1387
|
+
onChange={(id) => onRunnerIdChange?.(id)}
|
|
1388
|
+
disabled={isDisabled}
|
|
1389
|
+
/>
|
|
1390
|
+
)}
|
|
1391
|
+
<WorkspaceEditor
|
|
1392
|
+
workspace={workspace}
|
|
1393
|
+
disabled={isDisabled}
|
|
1394
|
+
gitHubConnection={gitHubConnection}
|
|
1395
|
+
enableGitHub={enableGitHub}
|
|
1396
|
+
enableLocal={enableLocal}
|
|
1397
|
+
runnerId={browseRunnerId}
|
|
1398
|
+
onBrowseLocalFolder={runnerId ? onBrowseLocalFolder : undefined}
|
|
1399
|
+
runnerName={selectedRunnerName}
|
|
1400
|
+
runnerHostname={selectedRunnerHostname}
|
|
1401
|
+
/>
|
|
1402
|
+
</div>
|
|
1369
1403
|
: null
|
|
1370
1404
|
}
|
|
1371
1405
|
configureItems={configureItems}
|
|
@@ -1374,6 +1408,9 @@ export function SessionComposer({
|
|
|
1374
1408
|
configActivePanel={configActivePanel}
|
|
1375
1409
|
onConfigActivePanelChange={handleConfigActivePanelChange}
|
|
1376
1410
|
renderConfigPanel={renderConfigPanel}
|
|
1411
|
+
showHarnessSelector={showHarnessSelector}
|
|
1412
|
+
harness={harness}
|
|
1413
|
+
onHarnessChange={handleHarnessChange}
|
|
1377
1414
|
showModelSelector={showModelSelector}
|
|
1378
1415
|
modelId={modelId}
|
|
1379
1416
|
onModelChange={handleModelChange}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useState } from "react";
|
|
3
4
|
import Markdown from "react-markdown";
|
|
4
5
|
import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
5
6
|
import { MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
@@ -20,9 +21,11 @@ export interface MessageEntryProps {
|
|
|
20
21
|
* - `MESSAGE_HUMAN` — plain text with muted background
|
|
21
22
|
* - `MESSAGE_AI` — markdown-rendered via `react-markdown` + `remark-gfm`,
|
|
22
23
|
* with a blinking cursor while streaming
|
|
24
|
+
* - `MESSAGE_THINKING` — collapsible thinking block with subdued styling,
|
|
25
|
+
* collapsed by default showing a brief summary
|
|
23
26
|
* - `MESSAGE_SYSTEM` — small muted text
|
|
24
27
|
* - `MESSAGE_TOOL` / `UNSPECIFIED` — renders nothing (tool results are
|
|
25
|
-
* consumed by {@link ToolCallGroup}
|
|
28
|
+
* consumed by {@link ToolCallGroup})
|
|
26
29
|
*
|
|
27
30
|
* Purely presentational — no data fetching, no state.
|
|
28
31
|
* All visual properties flow through `--stgm-*` tokens.
|
|
@@ -44,6 +47,14 @@ export function MessageEntry({ message, className }: MessageEntryProps) {
|
|
|
44
47
|
className={className}
|
|
45
48
|
/>
|
|
46
49
|
);
|
|
50
|
+
case MessageType.MESSAGE_THINKING:
|
|
51
|
+
return (
|
|
52
|
+
<ThinkingMessage
|
|
53
|
+
content={message.content}
|
|
54
|
+
isStreaming={message.isStreaming}
|
|
55
|
+
className={className}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
47
58
|
case MessageType.MESSAGE_SYSTEM:
|
|
48
59
|
return <SystemMessage content={message.content} className={className} />;
|
|
49
60
|
default:
|
|
@@ -103,6 +114,69 @@ function AiMessage({
|
|
|
103
114
|
);
|
|
104
115
|
}
|
|
105
116
|
|
|
117
|
+
const THINKING_PREVIEW_LENGTH = 80;
|
|
118
|
+
|
|
119
|
+
function ThinkingMessage({
|
|
120
|
+
content,
|
|
121
|
+
isStreaming,
|
|
122
|
+
className,
|
|
123
|
+
}: {
|
|
124
|
+
content: string;
|
|
125
|
+
isStreaming: boolean;
|
|
126
|
+
className?: string;
|
|
127
|
+
}) {
|
|
128
|
+
const [expanded, setExpanded] = useState(false);
|
|
129
|
+
const hasContent = content.trim().length > 0;
|
|
130
|
+
|
|
131
|
+
if (!hasContent && !isStreaming) return null;
|
|
132
|
+
|
|
133
|
+
const preview = content.length > THINKING_PREVIEW_LENGTH
|
|
134
|
+
? content.slice(0, THINKING_PREVIEW_LENGTH).trimEnd() + "..."
|
|
135
|
+
: content;
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
role="article"
|
|
140
|
+
aria-label="Model thinking"
|
|
141
|
+
className={cn("px-4 py-1.5", className)}
|
|
142
|
+
>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
aria-expanded={expanded}
|
|
146
|
+
onClick={() => setExpanded((v) => !v)}
|
|
147
|
+
className={cn(
|
|
148
|
+
"flex items-center gap-1.5 text-xs text-muted-foreground transition-colors",
|
|
149
|
+
"hover:text-foreground cursor-pointer",
|
|
150
|
+
)}
|
|
151
|
+
>
|
|
152
|
+
<ThinkingIcon isStreaming={isStreaming} />
|
|
153
|
+
<span className="min-w-0 truncate">
|
|
154
|
+
{isStreaming && !hasContent
|
|
155
|
+
? "Thinking..."
|
|
156
|
+
: expanded
|
|
157
|
+
? "Thinking"
|
|
158
|
+
: preview}
|
|
159
|
+
</span>
|
|
160
|
+
{hasContent && <ChevronIcon expanded={expanded} />}
|
|
161
|
+
</button>
|
|
162
|
+
|
|
163
|
+
{expanded && hasContent && (
|
|
164
|
+
<div className="mt-1.5 border-l-2 border-muted-foreground/20 pl-3">
|
|
165
|
+
<p className="text-xs text-muted-foreground whitespace-pre-wrap leading-relaxed">
|
|
166
|
+
{content}
|
|
167
|
+
{isStreaming && (
|
|
168
|
+
<span
|
|
169
|
+
className="inline-block w-[2px] h-[0.8em] bg-muted-foreground align-text-bottom animate-pulse ml-0.5"
|
|
170
|
+
aria-hidden="true"
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
173
|
+
</p>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
106
180
|
function SystemMessage({
|
|
107
181
|
content,
|
|
108
182
|
className,
|
|
@@ -121,3 +195,62 @@ function SystemMessage({
|
|
|
121
195
|
);
|
|
122
196
|
}
|
|
123
197
|
|
|
198
|
+
function ThinkingIcon({ isStreaming }: { isStreaming: boolean }) {
|
|
199
|
+
if (isStreaming) {
|
|
200
|
+
return (
|
|
201
|
+
<svg
|
|
202
|
+
width="12"
|
|
203
|
+
height="12"
|
|
204
|
+
viewBox="0 0 12 12"
|
|
205
|
+
fill="none"
|
|
206
|
+
stroke="currentColor"
|
|
207
|
+
strokeWidth="1.5"
|
|
208
|
+
className="shrink-0 animate-spin"
|
|
209
|
+
aria-hidden="true"
|
|
210
|
+
>
|
|
211
|
+
<path d="M6 1.5A4.5 4.5 0 1 1 1.5 6" strokeLinecap="round" />
|
|
212
|
+
</svg>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return (
|
|
216
|
+
<svg
|
|
217
|
+
width="12"
|
|
218
|
+
height="12"
|
|
219
|
+
viewBox="0 0 12 12"
|
|
220
|
+
fill="none"
|
|
221
|
+
stroke="currentColor"
|
|
222
|
+
strokeWidth="1.5"
|
|
223
|
+
strokeLinecap="round"
|
|
224
|
+
strokeLinejoin="round"
|
|
225
|
+
className="shrink-0"
|
|
226
|
+
aria-hidden="true"
|
|
227
|
+
>
|
|
228
|
+
<circle cx="6" cy="5" r="3.5" />
|
|
229
|
+
<path d="M4.5 9.5C4.5 8.5 5 8 6 8s1.5.5 1.5 1.5" />
|
|
230
|
+
<circle cx="5" cy="4.5" r="0.5" fill="currentColor" />
|
|
231
|
+
<circle cx="7" cy="4.5" r="0.5" fill="currentColor" />
|
|
232
|
+
</svg>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function ChevronIcon({ expanded }: { expanded: boolean }) {
|
|
237
|
+
return (
|
|
238
|
+
<svg
|
|
239
|
+
width="10"
|
|
240
|
+
height="10"
|
|
241
|
+
viewBox="0 0 10 10"
|
|
242
|
+
fill="none"
|
|
243
|
+
stroke="currentColor"
|
|
244
|
+
strokeWidth="1.5"
|
|
245
|
+
strokeLinecap="round"
|
|
246
|
+
strokeLinejoin="round"
|
|
247
|
+
className={cn(
|
|
248
|
+
"shrink-0 transition-transform duration-150",
|
|
249
|
+
expanded && "rotate-90",
|
|
250
|
+
)}
|
|
251
|
+
aria-hidden="true"
|
|
252
|
+
>
|
|
253
|
+
<path d="M3.5 2L6.5 5L3.5 8" />
|
|
254
|
+
</svg>
|
|
255
|
+
);
|
|
256
|
+
}
|
package/src/github/index.ts
CHANGED
|
@@ -42,6 +42,52 @@ export interface GitHubConnectOptions {
|
|
|
42
42
|
readonly popup?: boolean;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Optional configuration for {@link useGitHubConnection}.
|
|
47
|
+
*
|
|
48
|
+
* Enables desktop and non-browser environments to participate in the
|
|
49
|
+
* GitHub OAuth flow without relying on `window.open()` popups.
|
|
50
|
+
*/
|
|
51
|
+
export interface UseGitHubConnectionConfig {
|
|
52
|
+
/**
|
|
53
|
+
* Custom function to open the authorization URL in a browser.
|
|
54
|
+
*
|
|
55
|
+
* When provided, the hook calls this instead of `window.open()` during
|
|
56
|
+
* popup-mode `connect()` flows. This enables desktop environments
|
|
57
|
+
* (e.g. Tauri, Electron) where webview popups are blocked but the
|
|
58
|
+
* system browser is available.
|
|
59
|
+
*
|
|
60
|
+
* When used with {@link callbackUrl}, the callback page processes the
|
|
61
|
+
* token exchange and the consumer calls {@link UseGitHubConnectionReturn.reconcile}
|
|
62
|
+
* to pick up the token from the personal environment.
|
|
63
|
+
*
|
|
64
|
+
* When used without {@link callbackUrl} (e.g. localhost callback server),
|
|
65
|
+
* the consumer calls {@link UseGitHubConnectionReturn.handleCallback}
|
|
66
|
+
* with the code, state, and redirect URI to complete the exchange.
|
|
67
|
+
*/
|
|
68
|
+
readonly openUrl?: (url: string) => void | Promise<void>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Override the OAuth callback URL.
|
|
72
|
+
*
|
|
73
|
+
* When provided, this replaces the `redirectUri` parameter passed to
|
|
74
|
+
* `connect()` by UI components like `WorkspaceEditor`. Use this to
|
|
75
|
+
* route the GitHub callback to a specific URL — for example, the
|
|
76
|
+
* Stigmer web console's callback page for desktop flows, or a
|
|
77
|
+
* localhost server for local development.
|
|
78
|
+
*
|
|
79
|
+
* When the callback page handles the token exchange itself (cloud
|
|
80
|
+
* desktop flows), the consumer triggers re-reconciliation via
|
|
81
|
+
* {@link UseGitHubConnectionReturn.reconcile} after the callback
|
|
82
|
+
* completes externally.
|
|
83
|
+
*
|
|
84
|
+
* When the consumer handles the exchange (localhost flows), the same
|
|
85
|
+
* URL must be passed to `handleCallback()` — GitHub requires an
|
|
86
|
+
* exact match between the authorize and exchange redirect URIs.
|
|
87
|
+
*/
|
|
88
|
+
readonly callbackUrl?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
45
91
|
/** Return value of {@link useGitHubConnection}. */
|
|
46
92
|
export interface UseGitHubConnectionReturn {
|
|
47
93
|
/** Whether a valid GitHub token exists. */
|
|
@@ -72,6 +118,16 @@ export interface UseGitHubConnectionReturn {
|
|
|
72
118
|
state: string,
|
|
73
119
|
redirectUri: string,
|
|
74
120
|
) => Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Trigger re-reconciliation from the personal environment.
|
|
123
|
+
*
|
|
124
|
+
* Call this when the token exchange was handled externally (e.g. by
|
|
125
|
+
* the Stigmer web callback page during a desktop OAuth flow) and the
|
|
126
|
+
* token is already stored server-side. The hook will refetch the
|
|
127
|
+
* personal environment, reveal the token, and update
|
|
128
|
+
* {@link isConnected}.
|
|
129
|
+
*/
|
|
130
|
+
readonly reconcile: () => void;
|
|
75
131
|
/** Clear the stored token and user info. */
|
|
76
132
|
readonly disconnect: () => void;
|
|
77
133
|
}
|
|
@@ -93,6 +149,39 @@ async function fetchGitHubUser(token: string): Promise<GitHubUser | null> {
|
|
|
93
149
|
}
|
|
94
150
|
}
|
|
95
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Reads the OAuth state from the opener window's sessionStorage when
|
|
154
|
+
* running inside a popup (same-origin). Falls back to the current
|
|
155
|
+
* window's sessionStorage for redirect-based flows or when the
|
|
156
|
+
* opener is unavailable.
|
|
157
|
+
*/
|
|
158
|
+
function getSavedOAuthState(): string | null {
|
|
159
|
+
try {
|
|
160
|
+
if (window.opener && !window.opener.closed) {
|
|
161
|
+
const openerState = window.opener.sessionStorage.getItem(
|
|
162
|
+
STORAGE_KEY_STATE,
|
|
163
|
+
);
|
|
164
|
+
if (openerState) return openerState;
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// Cross-origin or closed opener — fall through to local storage.
|
|
168
|
+
}
|
|
169
|
+
return sessionStorage.getItem(STORAGE_KEY_STATE);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Removes the OAuth state key from both the current window's and the
|
|
174
|
+
* opener's sessionStorage (best-effort).
|
|
175
|
+
*/
|
|
176
|
+
function clearOAuthState(): void {
|
|
177
|
+
sessionStorage.removeItem(STORAGE_KEY_STATE);
|
|
178
|
+
try {
|
|
179
|
+
window.opener?.sessionStorage?.removeItem(STORAGE_KEY_STATE);
|
|
180
|
+
} catch {
|
|
181
|
+
// Cross-origin or closed opener — ignore.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
96
185
|
/**
|
|
97
186
|
* Checks whether the personal environment's redacted data contains a given key.
|
|
98
187
|
* The key is present even when the value is redacted (`***REDACTED***`).
|
|
@@ -127,6 +216,8 @@ function personalEnvHasKey(
|
|
|
127
216
|
*
|
|
128
217
|
* @param org - The active organization slug. Required for server-side
|
|
129
218
|
* token storage. Pass `null` to skip all server operations.
|
|
219
|
+
* @param config - Optional configuration for desktop / non-browser
|
|
220
|
+
* environments. See {@link UseGitHubConnectionConfig}.
|
|
130
221
|
*
|
|
131
222
|
* @example
|
|
132
223
|
* ```tsx
|
|
@@ -155,9 +246,23 @@ function personalEnvHasKey(
|
|
|
155
246
|
* );
|
|
156
247
|
* }
|
|
157
248
|
* ```
|
|
249
|
+
*
|
|
250
|
+
* @example Desktop (Tauri) — open in system browser, callback via deep link
|
|
251
|
+
* ```tsx
|
|
252
|
+
* function DesktopGitHubConnect({ org }: { org: string }) {
|
|
253
|
+
* const gh = useGitHubConnection(org, {
|
|
254
|
+
* openUrl: (url) => invoke("open_auth_in_browser", { authUrl: url }),
|
|
255
|
+
* callbackUrl: "https://app.stigmer.ai/auth/github/callback?source=desktop",
|
|
256
|
+
* });
|
|
257
|
+
* // The web callback page processes the exchange, then redirects to
|
|
258
|
+
* // stigmer://github/callback-done. The desktop deep link handler
|
|
259
|
+
* // calls gh.reconcile() to pick up the token.
|
|
260
|
+
* }
|
|
261
|
+
* ```
|
|
158
262
|
*/
|
|
159
263
|
export function useGitHubConnection(
|
|
160
264
|
org: string | null,
|
|
265
|
+
config?: UseGitHubConnectionConfig,
|
|
161
266
|
): UseGitHubConnectionReturn {
|
|
162
267
|
const stigmer = useStigmer();
|
|
163
268
|
const [token, setToken] = useState<string | null>(null);
|
|
@@ -287,8 +392,12 @@ export function useGitHubConnection(
|
|
|
287
392
|
|
|
288
393
|
const connect = useCallback(
|
|
289
394
|
async (redirectUri: string, options?: GitHubConnectOptions) => {
|
|
395
|
+
const effectiveRedirectUri = config?.callbackUrl ?? redirectUri;
|
|
396
|
+
|
|
290
397
|
const { authorizeUrl, state } =
|
|
291
|
-
await stigmer.github.getOAuthAuthorizeUrl({
|
|
398
|
+
await stigmer.github.getOAuthAuthorizeUrl({
|
|
399
|
+
redirectUri: effectiveRedirectUri,
|
|
400
|
+
});
|
|
292
401
|
|
|
293
402
|
sessionStorage.setItem(STORAGE_KEY_STATE, state);
|
|
294
403
|
|
|
@@ -297,7 +406,18 @@ export function useGitHubConnection(
|
|
|
297
406
|
return;
|
|
298
407
|
}
|
|
299
408
|
|
|
300
|
-
//
|
|
409
|
+
// ── External opener (desktop / non-browser) ─────────────────────
|
|
410
|
+
// When `config.openUrl` is provided, delegate to the consumer
|
|
411
|
+
// instead of using `window.open()`. The consumer is responsible
|
|
412
|
+
// for delivering the callback data via `handleCallback()` or
|
|
413
|
+
// triggering re-reconciliation via `reconcile()`.
|
|
414
|
+
if (config?.openUrl) {
|
|
415
|
+
setIsConnecting(true);
|
|
416
|
+
await config.openUrl(authorizeUrl);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── Browser popup ───────────────────────────────────────────────
|
|
301
421
|
if (popupRef.current && !popupRef.current.closed) {
|
|
302
422
|
popupRef.current.focus();
|
|
303
423
|
return;
|
|
@@ -327,43 +447,62 @@ export function useGitHubConnection(
|
|
|
327
447
|
popupPollRef.current = null;
|
|
328
448
|
popupRef.current = null;
|
|
329
449
|
setIsConnecting(false);
|
|
450
|
+
|
|
451
|
+
// The callback page may have exchanged the code and stored
|
|
452
|
+
// the token server-side (e.g. cross-origin popup flow for
|
|
453
|
+
// platform builders). Re-reconcile from the personal
|
|
454
|
+
// environment to pick up the token.
|
|
455
|
+
setIsLoading(true);
|
|
456
|
+
reconciled.current = false;
|
|
457
|
+
personalEnvRef.current.refetch();
|
|
330
458
|
}
|
|
331
459
|
}, POPUP_CLOSE_POLL_MS);
|
|
332
460
|
popupPollRef.current = pollId;
|
|
333
461
|
},
|
|
334
|
-
[stigmer],
|
|
462
|
+
[stigmer, config?.openUrl, config?.callbackUrl],
|
|
335
463
|
);
|
|
336
464
|
|
|
337
465
|
const handleCallback = useCallback(
|
|
338
466
|
async (code: string, state: string, redirectUri: string) => {
|
|
339
|
-
const savedState =
|
|
467
|
+
const savedState = getSavedOAuthState();
|
|
340
468
|
if (savedState && savedState !== state) {
|
|
341
469
|
throw new Error("OAuth state mismatch — possible CSRF attack");
|
|
342
470
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const { accessToken } = await stigmer.github.exchangeOAuthCode({
|
|
346
|
-
code,
|
|
347
|
-
state,
|
|
348
|
-
redirectUri,
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
const tokenVar = {
|
|
352
|
-
[GITHUB_TOKEN_KEY]: { value: accessToken, isSecret: true },
|
|
353
|
-
};
|
|
354
|
-
const env = await personalEnvRef.current.getOrCreate(tokenVar);
|
|
355
|
-
if (!personalEnvHasKey(env, GITHUB_TOKEN_KEY)) {
|
|
356
|
-
await personalEnvRef.current.addVariables(tokenVar);
|
|
357
|
-
}
|
|
471
|
+
clearOAuthState();
|
|
358
472
|
|
|
359
|
-
|
|
473
|
+
try {
|
|
474
|
+
const { accessToken } = await stigmer.github.exchangeOAuthCode({
|
|
475
|
+
code,
|
|
476
|
+
state,
|
|
477
|
+
redirectUri,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const tokenVar = {
|
|
481
|
+
[GITHUB_TOKEN_KEY]: { value: accessToken, isSecret: true },
|
|
482
|
+
};
|
|
483
|
+
const env = await personalEnvRef.current.getOrCreate(tokenVar);
|
|
484
|
+
if (!personalEnvHasKey(env, GITHUB_TOKEN_KEY)) {
|
|
485
|
+
await personalEnvRef.current.addVariables(tokenVar);
|
|
486
|
+
}
|
|
360
487
|
|
|
361
|
-
|
|
362
|
-
|
|
488
|
+
setToken(accessToken);
|
|
489
|
+
|
|
490
|
+
const u = await fetchGitHubUser(accessToken);
|
|
491
|
+
setUser(u);
|
|
492
|
+
} finally {
|
|
493
|
+
setIsConnecting(false);
|
|
494
|
+
}
|
|
363
495
|
},
|
|
364
496
|
[stigmer],
|
|
365
497
|
);
|
|
366
498
|
|
|
499
|
+
const reconcile = useCallback(() => {
|
|
500
|
+
setIsConnecting(false);
|
|
501
|
+
setIsLoading(true);
|
|
502
|
+
reconciled.current = false;
|
|
503
|
+
personalEnvRef.current.refetch();
|
|
504
|
+
}, []);
|
|
505
|
+
|
|
367
506
|
const disconnect = useCallback(() => {
|
|
368
507
|
sessionStorage.removeItem(STORAGE_KEY_STATE);
|
|
369
508
|
setToken(null);
|
|
@@ -396,6 +535,7 @@ export function useGitHubConnection(
|
|
|
396
535
|
token,
|
|
397
536
|
connect,
|
|
398
537
|
handleCallback,
|
|
538
|
+
reconcile,
|
|
399
539
|
disconnect,
|
|
400
540
|
};
|
|
401
541
|
}
|