@stigmer/react 3.4.0 → 3.4.1
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/agent/useAgentCount.d.ts +2 -0
- package/agent/useAgentCount.d.ts.map +1 -1
- package/agent/useAgentCount.js.map +1 -1
- package/api-key/CreateApiKeyForm.d.ts +7 -1
- package/api-key/CreateApiKeyForm.d.ts.map +1 -1
- package/api-key/CreateApiKeyForm.js +2 -2
- package/api-key/CreateApiKeyForm.js.map +1 -1
- package/datastore/useDatastoreCount.d.ts +2 -0
- package/datastore/useDatastoreCount.d.ts.map +1 -1
- package/datastore/useDatastoreCount.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/manifest/ApplyManifestDialog.d.ts +10 -5
- package/manifest/ApplyManifestDialog.d.ts.map +1 -1
- package/manifest/ApplyManifestDialog.js +30 -13
- package/manifest/ApplyManifestDialog.js.map +1 -1
- package/manifest/useApplyManifest.d.ts +6 -2
- package/manifest/useApplyManifest.d.ts.map +1 -1
- package/manifest/useApplyManifest.js +5 -5
- package/manifest/useApplyManifest.js.map +1 -1
- package/mcp-server/McpServerDetailView.d.ts +38 -2
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +21 -5
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/mcp-server/index.d.ts +7 -0
- package/mcp-server/index.d.ts.map +1 -1
- package/mcp-server/index.js +9 -0
- package/mcp-server/index.js.map +1 -1
- package/mcp-server/steps/EnvironmentAuthStep.d.ts +4 -0
- package/mcp-server/steps/EnvironmentAuthStep.d.ts.map +1 -1
- package/mcp-server/steps/EnvironmentAuthStep.js +4 -0
- package/mcp-server/steps/EnvironmentAuthStep.js.map +1 -1
- package/mcp-server/steps/IdentityTransportStep.d.ts +6 -0
- package/mcp-server/steps/IdentityTransportStep.d.ts.map +1 -1
- package/mcp-server/steps/IdentityTransportStep.js +7 -1
- package/mcp-server/steps/IdentityTransportStep.js.map +1 -1
- package/mcp-server/steps/ReviewStep.d.ts +5 -0
- package/mcp-server/steps/ReviewStep.d.ts.map +1 -1
- package/mcp-server/steps/ReviewStep.js +16 -2
- package/mcp-server/steps/ReviewStep.js.map +1 -1
- package/mcp-server/useMcpServerCount.d.ts +2 -0
- package/mcp-server/useMcpServerCount.d.ts.map +1 -1
- package/mcp-server/useMcpServerCount.js.map +1 -1
- package/package.json +4 -4
- package/resource-creation/CreationPicker.d.ts.map +1 -1
- package/resource-creation/CreationPicker.js +3 -3
- package/resource-creation/CreationPicker.js.map +1 -1
- package/resource-creation/WizardNav.d.ts.map +1 -1
- package/resource-creation/WizardNav.js +1 -1
- package/resource-creation/WizardNav.js.map +1 -1
- package/resource-detail/Section.d.ts +8 -1
- package/resource-detail/Section.d.ts.map +1 -1
- package/resource-detail/Section.js +2 -2
- package/resource-detail/Section.js.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts +9 -1
- package/resource-workbench/components/ResourceWorkbench.d.ts.map +1 -1
- package/resource-workbench/components/ResourceWorkbench.js +2 -1
- package/resource-workbench/components/ResourceWorkbench.js.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.d.ts +9 -0
- package/resource-workbench/hooks/useResourceCollection.d.ts.map +1 -1
- package/resource-workbench/hooks/useResourceCollection.js +2 -2
- package/resource-workbench/hooks/useResourceCollection.js.map +1 -1
- package/search/useResourceCount.d.ts +7 -0
- package/search/useResourceCount.d.ts.map +1 -1
- package/search/useResourceCount.js +2 -1
- package/search/useResourceCount.js.map +1 -1
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +17 -3
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +2 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +19 -11
- package/session/SessionViewer.js.map +1 -1
- package/session/SessionViewerLayout.d.ts +90 -0
- package/session/SessionViewerLayout.d.ts.map +1 -0
- package/session/SessionViewerLayout.js +32 -0
- package/session/SessionViewerLayout.js.map +1 -0
- package/session/index.d.ts +4 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js +10 -0
- package/session/index.js.map +1 -1
- package/session/useSessionRailViews.d.ts +2 -2
- package/skill/useSkillCount.d.ts +2 -0
- package/skill/useSkillCount.d.ts.map +1 -1
- package/skill/useSkillCount.js.map +1 -1
- package/src/agent/useAgentCount.ts +2 -0
- package/src/api-key/CreateApiKeyForm.tsx +8 -1
- package/src/api-key/__tests__/CreateApiKeyForm.test.tsx +58 -0
- package/src/datastore/useDatastoreCount.ts +2 -0
- package/src/index.ts +11 -0
- package/src/manifest/ApplyManifestDialog.tsx +40 -16
- package/src/manifest/__tests__/ApplyManifestDialog.test.tsx +147 -0
- package/src/manifest/__tests__/useApplyManifest.test.tsx +6 -6
- package/src/manifest/useApplyManifest.ts +12 -8
- package/src/mcp-server/McpServerDetailView.tsx +52 -4
- package/src/mcp-server/__tests__/McpServerDetailView.test.tsx +345 -0
- package/src/mcp-server/index.ts +13 -0
- package/src/mcp-server/steps/EnvironmentAuthStep.tsx +4 -0
- package/src/mcp-server/steps/IdentityTransportStep.tsx +9 -1
- package/src/mcp-server/steps/ReviewStep.tsx +18 -1
- package/src/mcp-server/useMcpServerCount.ts +2 -0
- package/src/resource-creation/CreationPicker.tsx +7 -0
- package/src/resource-creation/WizardNav.tsx +1 -0
- package/src/resource-detail/Section.tsx +9 -1
- package/src/resource-workbench/components/ResourceWorkbench.tsx +13 -0
- package/src/resource-workbench/hooks/useResourceCollection.ts +11 -1
- package/src/search/useResourceCount.ts +9 -1
- package/src/session/NewSessionViewer.tsx +48 -61
- package/src/session/SessionViewer.tsx +71 -86
- package/src/session/SessionViewerLayout.tsx +131 -0
- package/src/session/__tests__/SessionViewerLayout.test.tsx +140 -0
- package/src/session/index.ts +14 -0
- package/src/session/useSessionRailViews.tsx +2 -2
- package/src/skill/useSkillCount.ts +2 -0
- package/src/test/__tests__/samples.test.ts +159 -2
- package/src/test/index.ts +1 -1
- package/src/test/samples.ts +109 -14
- package/src/workflow/useWorkflowCount.ts +2 -0
- package/test/index.d.ts +1 -1
- package/test/index.d.ts.map +1 -1
- package/test/index.js +1 -1
- package/test/index.js.map +1 -1
- package/test/samples.d.ts +79 -7
- package/test/samples.d.ts.map +1 -1
- package/test/samples.js +101 -14
- package/test/samples.js.map +1 -1
- package/workflow/useWorkflowCount.d.ts +2 -0
- package/workflow/useWorkflowCount.d.ts.map +1 -1
- package/workflow/useWorkflowCount.js.map +1 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
|
|
4
|
-
import { cn } from "@stigmer/theme";
|
|
5
4
|
import type { ResourceRef } from "@stigmer/sdk";
|
|
6
5
|
import type { UseGitHubConnectionReturn } from "../github/useGitHubConnection.js";
|
|
7
6
|
import type { WorkspaceFileLister } from "../workspace/WorkspaceFileLister.js";
|
|
@@ -10,7 +9,7 @@ import type { WorkspaceContentSearcher } from "../workspace/WorkspaceContentSear
|
|
|
10
9
|
import type { InteractionModeOption } from "../composer/index.js";
|
|
11
10
|
import { SessionComposer } from "../composer/index.js";
|
|
12
11
|
import type { HarnessOption } from "../models/harness.js";
|
|
13
|
-
import {
|
|
12
|
+
import { SessionViewerLayout } from "./SessionViewerLayout.js";
|
|
14
13
|
import { useWorkspaceEditors } from "../internal/store/index.js";
|
|
15
14
|
import { WorkspaceSurface } from "../workspace/WorkspaceSurface.js";
|
|
16
15
|
import type { SetupTabProps } from "./facets/SetupTab.js";
|
|
@@ -383,68 +382,56 @@ export function NewSessionViewer({
|
|
|
383
382
|
);
|
|
384
383
|
|
|
385
384
|
return (
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
385
|
+
// The launcher renders the same SessionViewerLayout as SessionViewer
|
|
386
|
+
// (DD-016) — the shared frame is what makes "the two viewers behave
|
|
387
|
+
// identically" structural rather than hand-synchronized.
|
|
388
|
+
<SessionViewerLayout
|
|
389
|
+
className={className}
|
|
390
|
+
resizeAriaLabel="Resize composer panel"
|
|
391
|
+
splitStorageKey="stgm-new-session-chat-width"
|
|
392
|
+
// Persistent chrome: the chip is always mounted, matching SessionViewer
|
|
393
|
+
// (DD-016) rather than the launcher's earlier progressively-revealed
|
|
394
|
+
// inspector. A toggle that appears/disappears with attached context
|
|
395
|
+
// reads as instability and, worse, unmounts the open panel's only
|
|
396
|
+
// collapse control when the last context item is removed. Always-on is
|
|
397
|
+
// the predictable, discoverable shape; opening homes on the Config
|
|
398
|
+
// facet, which carries useful defaults (harness/model) pre-session.
|
|
399
|
+
// Guests are the exception: the panel exposes configuration a visitor
|
|
400
|
+
// has no business with, so the chip (the panel's only toggle) is
|
|
401
|
+
// simply absent and the panel can never open.
|
|
402
|
+
chip={
|
|
403
|
+
!isGuest ? (
|
|
399
404
|
<SessionPanelChip
|
|
400
405
|
isOpen={panel.isOpen}
|
|
401
406
|
onToggle={panel.isOpen ? panel.closePanel : panel.openPanel}
|
|
402
407
|
/>
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
{
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
onViewChange={panel.setView}
|
|
432
|
-
extraViews={railViews}
|
|
433
|
-
onRemoveEntry={flow.workspace.remove}
|
|
434
|
-
onAddLocalFolder={canAddLocalFolder ? handleAddLocalFolder : undefined}
|
|
435
|
-
editors={editors}
|
|
436
|
-
selectedFile={activeFile}
|
|
437
|
-
onOpenFile={panel.openFile}
|
|
438
|
-
onActivateEditor={panel.activateEditor}
|
|
439
|
-
onPinEditor={panel.pinEditor}
|
|
440
|
-
onCloseEditor={panel.closeEditor}
|
|
441
|
-
onCollapse={panel.closePanel}
|
|
442
|
-
reveal={activeReveal}
|
|
443
|
-
className="h-full"
|
|
444
|
-
/>
|
|
445
|
-
) : null
|
|
446
|
-
}
|
|
447
|
-
/>
|
|
448
|
-
</div>
|
|
408
|
+
) : undefined
|
|
409
|
+
}
|
|
410
|
+
conversation={composerNode}
|
|
411
|
+
panel={
|
|
412
|
+
panel.isOpen ? (
|
|
413
|
+
<WorkspaceSurface
|
|
414
|
+
entries={flow.workspace.entries}
|
|
415
|
+
lister={workspaceFileLister}
|
|
416
|
+
reader={workspaceFileReader}
|
|
417
|
+
searcher={workspaceContentSearcher}
|
|
418
|
+
view={panel.view}
|
|
419
|
+
onViewChange={panel.setView}
|
|
420
|
+
extraViews={railViews}
|
|
421
|
+
onRemoveEntry={flow.workspace.remove}
|
|
422
|
+
onAddLocalFolder={canAddLocalFolder ? handleAddLocalFolder : undefined}
|
|
423
|
+
editors={editors}
|
|
424
|
+
selectedFile={activeFile}
|
|
425
|
+
onOpenFile={panel.openFile}
|
|
426
|
+
onActivateEditor={panel.activateEditor}
|
|
427
|
+
onPinEditor={panel.pinEditor}
|
|
428
|
+
onCloseEditor={panel.closeEditor}
|
|
429
|
+
onCollapse={panel.closePanel}
|
|
430
|
+
reveal={activeReveal}
|
|
431
|
+
className="h-full"
|
|
432
|
+
/>
|
|
433
|
+
) : null
|
|
434
|
+
}
|
|
435
|
+
/>
|
|
449
436
|
);
|
|
450
437
|
}
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "../workspace/WorkspaceSurface.js";
|
|
20
20
|
import type { InteractionModeOption, SessionComposerHandle, SessionComposerSubmitContext } from "../composer/index.js";
|
|
21
21
|
import type { ApplyResourceResult } from "../library/useApplyResource.js";
|
|
22
|
-
import {
|
|
22
|
+
import { SessionViewerLayout } from "./SessionViewerLayout.js";
|
|
23
23
|
import { useWorkspaceEditors, isVirtualEntryId } from "../internal/store/index.js";
|
|
24
24
|
import { isTerminalPhase } from "../execution/execution-phases.js";
|
|
25
25
|
import { MessageThread, type MessageThreadSlots } from "../execution/MessageThread.js";
|
|
@@ -267,7 +267,8 @@ export interface SessionViewerProps {
|
|
|
267
267
|
* to a top-right chip; opening a file (tree, search, or a transcript path)
|
|
268
268
|
* expands it.
|
|
269
269
|
*
|
|
270
|
-
*
|
|
270
|
+
* Both regions render inside {@link SessionViewerLayout} — the frame shared
|
|
271
|
+
* with {@link NewSessionViewer} — with the chat width persisted per viewer.
|
|
271
272
|
*
|
|
272
273
|
* Framework-agnostic — no Next.js, no Tauri, no routing deps. Host
|
|
273
274
|
* apps inject platform-specific values via props (DD-004/DD-016).
|
|
@@ -552,101 +553,85 @@ export function SessionViewer({
|
|
|
552
553
|
}
|
|
553
554
|
|
|
554
555
|
return (
|
|
555
|
-
<
|
|
556
|
-
{
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
556
|
+
<SessionViewerLayout
|
|
557
|
+
className={className}
|
|
558
|
+
// Top-right controls: host actions + the panel chip. The chip is the
|
|
559
|
+
// panel's always-mounted toggle; while collapsed it carries only the
|
|
560
|
+
// pending-item count. Execution status is never surfaced as header
|
|
561
|
+
// chrome — the thread itself communicates run state. Guests get no
|
|
562
|
+
// chip: the panel exposes session configuration a visitor has no
|
|
563
|
+
// business with, so its only toggle is simply absent.
|
|
564
|
+
headerActions={headerActions}
|
|
565
|
+
chip={
|
|
566
|
+
!isGuest ? (
|
|
565
567
|
<SessionPanelChip
|
|
566
568
|
isOpen={panel.isOpen}
|
|
567
569
|
onToggle={panel.isOpen ? panel.closePanel : panel.openPanel}
|
|
568
570
|
badgeCount={writeBackCount + artifactCount}
|
|
569
571
|
/>
|
|
570
|
-
)
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
{
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
572
|
+
) : undefined
|
|
573
|
+
}
|
|
574
|
+
splitStorageKey="stgm-session-chat-width"
|
|
575
|
+
conversation={
|
|
576
|
+
<ConversationColumn
|
|
577
|
+
flow={flow}
|
|
578
|
+
modelId={modelId}
|
|
579
|
+
setModelId={setModelId}
|
|
580
|
+
interactionMode={interactionMode}
|
|
581
|
+
setInteractionMode={setInteractionMode}
|
|
582
|
+
composerRef={composerRef}
|
|
583
|
+
org={org}
|
|
584
|
+
gitHubConnection={gitHubConnection}
|
|
585
|
+
enableGitHub={enableGitHub}
|
|
586
|
+
enableLocal={enableLocal}
|
|
587
|
+
onBrowseLocalFolder={onBrowseLocalFolder}
|
|
588
|
+
onBuildFromPlan={handleBuildFromPlan}
|
|
589
|
+
onOpenPlan={handleOpenPlan}
|
|
590
|
+
isBuildingFromPlan={isBuildingFromPlan}
|
|
591
|
+
planAttachFailed={planAttachFailed}
|
|
592
|
+
onDismissPlanAttachFailed={() => setPlanAttachFailed(false)}
|
|
593
|
+
onFilePathClick={handleTranscriptFilePathClick}
|
|
594
|
+
isCurated={isCurated}
|
|
595
|
+
isGuest={isGuest}
|
|
596
|
+
isObserver={isObserver}
|
|
597
|
+
threadSlots={threadSlots}
|
|
598
|
+
/>
|
|
599
|
+
}
|
|
600
|
+
panel={
|
|
601
|
+
panel.isOpen && !isGuest ? (
|
|
602
|
+
<SessionPanelRegion
|
|
591
603
|
flow={flow}
|
|
592
|
-
modelId={modelId}
|
|
593
|
-
setModelId={setModelId}
|
|
594
|
-
interactionMode={interactionMode}
|
|
595
|
-
setInteractionMode={setInteractionMode}
|
|
596
|
-
composerRef={composerRef}
|
|
597
604
|
org={org}
|
|
598
|
-
|
|
599
|
-
|
|
605
|
+
panel={panel}
|
|
606
|
+
// Observers cannot manage access (no can_grant_access /
|
|
607
|
+
// can_view_access on a channel session) — the control is
|
|
608
|
+
// withheld rather than left to fail server-side.
|
|
609
|
+
accessSlot={isObserver ? undefined : accessSlot}
|
|
610
|
+
onApplied={onApplied}
|
|
611
|
+
// Implementing a plan submits an execution — a write an
|
|
612
|
+
// observer cannot make (can_create_execution_in is
|
|
613
|
+
// owner/viewer-only).
|
|
614
|
+
onImplementPlan={isObserver ? undefined : handleBuildFromPlan}
|
|
615
|
+
implementPlanDisabled={
|
|
616
|
+
isObserver || !conv.canSendFollowUp || isBuildingFromPlan
|
|
617
|
+
}
|
|
618
|
+
sessionPlan={sessionPlan}
|
|
619
|
+
streamingPlan={streamingPlan}
|
|
620
|
+
planDraft={planDraft}
|
|
621
|
+
openPlanExecutionId={openPlanExecutionId}
|
|
622
|
+
onOpenPlan={handleOpenPlan}
|
|
623
|
+
onOpenArtifact={handleOpenArtifact}
|
|
624
|
+
onActivateArtifact={handleActivateArtifact}
|
|
600
625
|
enableLocal={enableLocal}
|
|
601
626
|
onBrowseLocalFolder={onBrowseLocalFolder}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
planAttachFailed={planAttachFailed}
|
|
606
|
-
onDismissPlanAttachFailed={() => setPlanAttachFailed(false)}
|
|
607
|
-
onFilePathClick={handleTranscriptFilePathClick}
|
|
627
|
+
workspaceFileLister={workspaceFileLister}
|
|
628
|
+
workspaceFileReader={workspaceFileReader}
|
|
629
|
+
workspaceContentSearcher={workspaceContentSearcher}
|
|
608
630
|
isCurated={isCurated}
|
|
609
|
-
isGuest={isGuest}
|
|
610
|
-
isObserver={isObserver}
|
|
611
|
-
threadSlots={threadSlots}
|
|
612
631
|
/>
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
<SessionPanelRegion
|
|
617
|
-
flow={flow}
|
|
618
|
-
org={org}
|
|
619
|
-
panel={panel}
|
|
620
|
-
// Observers cannot manage access (no can_grant_access /
|
|
621
|
-
// can_view_access on a channel session) — the control is
|
|
622
|
-
// withheld rather than left to fail server-side.
|
|
623
|
-
accessSlot={isObserver ? undefined : accessSlot}
|
|
624
|
-
onApplied={onApplied}
|
|
625
|
-
// Implementing a plan submits an execution — a write an
|
|
626
|
-
// observer cannot make (can_create_execution_in is
|
|
627
|
-
// owner/viewer-only).
|
|
628
|
-
onImplementPlan={isObserver ? undefined : handleBuildFromPlan}
|
|
629
|
-
implementPlanDisabled={
|
|
630
|
-
isObserver || !conv.canSendFollowUp || isBuildingFromPlan
|
|
631
|
-
}
|
|
632
|
-
sessionPlan={sessionPlan}
|
|
633
|
-
streamingPlan={streamingPlan}
|
|
634
|
-
planDraft={planDraft}
|
|
635
|
-
openPlanExecutionId={openPlanExecutionId}
|
|
636
|
-
onOpenPlan={handleOpenPlan}
|
|
637
|
-
onOpenArtifact={handleOpenArtifact}
|
|
638
|
-
onActivateArtifact={handleActivateArtifact}
|
|
639
|
-
enableLocal={enableLocal}
|
|
640
|
-
onBrowseLocalFolder={onBrowseLocalFolder}
|
|
641
|
-
workspaceFileLister={workspaceFileLister}
|
|
642
|
-
workspaceFileReader={workspaceFileReader}
|
|
643
|
-
workspaceContentSearcher={workspaceContentSearcher}
|
|
644
|
-
isCurated={isCurated}
|
|
645
|
-
/>
|
|
646
|
-
) : null
|
|
647
|
-
}
|
|
648
|
-
/>
|
|
649
|
-
</div>
|
|
632
|
+
) : null
|
|
633
|
+
}
|
|
634
|
+
/>
|
|
650
635
|
);
|
|
651
636
|
}
|
|
652
637
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { ResizableSplit } from "../internal/ResizableSplit.js";
|
|
6
|
+
|
|
7
|
+
/** Props for {@link SessionViewerLayout}. */
|
|
8
|
+
export interface SessionViewerLayoutProps {
|
|
9
|
+
/**
|
|
10
|
+
* The conversation column — the layout's primary (left) pane. Stays
|
|
11
|
+
* mounted at a stable tree position across every panel open/close, so
|
|
12
|
+
* streaming and scroll state survive the toggle.
|
|
13
|
+
*/
|
|
14
|
+
readonly conversation: ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* The session panel content, or `null` while the panel is collapsed.
|
|
17
|
+
* This is the single source of truth for the collapsed state: passing
|
|
18
|
+
* `null` collapses the split (the conversation fills the row) and hides
|
|
19
|
+
* the drag separator; passing content expands the panel into the
|
|
20
|
+
* flexible region. Hosts derive it from their panel controller, e.g.
|
|
21
|
+
* `panel.isOpen ? <WorkspaceSurface … /> : null`.
|
|
22
|
+
*/
|
|
23
|
+
readonly panel: ReactNode | null;
|
|
24
|
+
/**
|
|
25
|
+
* The panel's toggle chip, rendered in the layout's top-right corner
|
|
26
|
+
* (typically the session panel chip, {@link PanelChip}). Omit it to
|
|
27
|
+
* render no toggle — how hosts express audiences that must never open
|
|
28
|
+
* the panel (the guest case in {@link SessionViewer} and
|
|
29
|
+
* {@link NewSessionViewer}).
|
|
30
|
+
*/
|
|
31
|
+
readonly chip?: ReactNode;
|
|
32
|
+
/**
|
|
33
|
+
* Host-injected header actions, rendered beside the chip in the
|
|
34
|
+
* top-right corner (slot injection, as {@link SessionViewerProps.headerActions}).
|
|
35
|
+
*/
|
|
36
|
+
readonly headerActions?: ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* `localStorage` key persisting the chat pane's drag-resized width
|
|
39
|
+
* across sessions. Each host passes its own key so the two shipped
|
|
40
|
+
* viewers keep distinct widths. **Omit it in deterministic hosts**
|
|
41
|
+
* (documentation embeds, video export): with no key the width always
|
|
42
|
+
* starts at the default, so every render of the same input is
|
|
43
|
+
* pixel-identical instead of depending on previously persisted state.
|
|
44
|
+
*/
|
|
45
|
+
readonly splitStorageKey?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Whether the conversation pane collapses below the `lg` breakpoint
|
|
48
|
+
* while the panel is open, leaving the panel full-width — the shipped
|
|
49
|
+
* console behavior on narrow windows.
|
|
50
|
+
*
|
|
51
|
+
* Pass `false` in fixed-canvas hosts, where the media query is
|
|
52
|
+
* meaningless or actively wrong: a documentation embed renders in an
|
|
53
|
+
* iframe whose viewport is the docs column (below `lg` on most pages),
|
|
54
|
+
* so the default would hide the conversation on exactly the beats that
|
|
55
|
+
* open the panel.
|
|
56
|
+
*
|
|
57
|
+
* @default true
|
|
58
|
+
*/
|
|
59
|
+
readonly responsive?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Accessible label for the split's drag separator. Name the pane being
|
|
62
|
+
* resized in the host's vocabulary (the session viewer's chat pane vs
|
|
63
|
+
* the launcher's composer pane).
|
|
64
|
+
*
|
|
65
|
+
* @default "Resize chat panel"
|
|
66
|
+
*/
|
|
67
|
+
readonly resizeAriaLabel?: string;
|
|
68
|
+
/** Additional CSS classes for the root container. */
|
|
69
|
+
readonly className?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The one conversation-plus-panel frame every session viewer shares —
|
|
74
|
+
* {@link SessionViewer} and {@link NewSessionViewer} both render it, so the
|
|
75
|
+
* two surfaces cannot drift apart (their behave-identically contract), and
|
|
76
|
+
* platform builders composing a custom session surface from
|
|
77
|
+
* {@link WorkspaceSurface}, {@link PanelChip}, and the session facets get
|
|
78
|
+
* the same geometry the shipped console uses.
|
|
79
|
+
*
|
|
80
|
+
* The layout owns exactly three things:
|
|
81
|
+
*
|
|
82
|
+
* - **The top-right control corner** — the host's `headerActions` beside
|
|
83
|
+
* the panel toggle `chip`, absolutely positioned over the conversation.
|
|
84
|
+
* - **The split geometry** — chat fills the row while the panel is
|
|
85
|
+
* collapsed; opening makes the chat the fixed, drag-resizable pane
|
|
86
|
+
* (420px default, 320–640px) and hands the flexible region to the panel.
|
|
87
|
+
* - **The collapse mechanics** — collapse goes through the split's
|
|
88
|
+
* `collapsedPane` (CSS, not conditional structure), so the conversation
|
|
89
|
+
* is always the same first child and an open/close toggle never
|
|
90
|
+
* remounts it.
|
|
91
|
+
*
|
|
92
|
+
* Everything rendered inside the panes belongs to the host: the layout
|
|
93
|
+
* takes opaque `ReactNode`s and never inspects them.
|
|
94
|
+
*/
|
|
95
|
+
export function SessionViewerLayout({
|
|
96
|
+
conversation,
|
|
97
|
+
panel,
|
|
98
|
+
chip,
|
|
99
|
+
headerActions,
|
|
100
|
+
splitStorageKey,
|
|
101
|
+
responsive = true,
|
|
102
|
+
resizeAriaLabel = "Resize chat panel",
|
|
103
|
+
className,
|
|
104
|
+
}: SessionViewerLayoutProps) {
|
|
105
|
+
const isPanelOpen = panel != null;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className={cn("relative flex h-full w-full flex-col", className)}>
|
|
109
|
+
{(headerActions != null || chip != null) && (
|
|
110
|
+
<div className="absolute top-2 right-6 z-10 flex items-center gap-2">
|
|
111
|
+
{headerActions}
|
|
112
|
+
{chip}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
<ResizableSplit
|
|
117
|
+
resizablePane="primary"
|
|
118
|
+
collapsedPane={isPanelOpen ? "none" : "secondary"}
|
|
119
|
+
defaultSize={420}
|
|
120
|
+
minSize={320}
|
|
121
|
+
maxSize={640}
|
|
122
|
+
storageKey={splitStorageKey}
|
|
123
|
+
responsiveCollapse={responsive && isPanelOpen ? "primary" : "none"}
|
|
124
|
+
ariaLabel={resizeAriaLabel}
|
|
125
|
+
className="min-h-0 flex-1"
|
|
126
|
+
primary={conversation}
|
|
127
|
+
secondary={panel}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
3
|
+
import { SessionViewerLayout } from "../SessionViewerLayout";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// The one conversation-plus-panel frame both shipped viewers render
|
|
7
|
+
// (SessionViewer + NewSessionViewer), so the two surfaces cannot drift.
|
|
8
|
+
// The contract under test:
|
|
9
|
+
// - `panel` is the single source of truth for collapsed state
|
|
10
|
+
// - the conversation never remounts across an open/close toggle
|
|
11
|
+
// - `responsive={false}` disables the below-`lg` conversation collapse
|
|
12
|
+
// (fixed-canvas hosts: docs embeds, video export)
|
|
13
|
+
// - no `splitStorageKey` means no localStorage read (deterministic hosts)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
cleanup();
|
|
18
|
+
vi.restoreAllMocks();
|
|
19
|
+
localStorage.clear();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function conversation() {
|
|
23
|
+
return <div data-testid="conversation">chat</div>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function panel() {
|
|
27
|
+
return <div data-testid="panel">panel</div>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("SessionViewerLayout", () => {
|
|
31
|
+
it("collapsed (panel null): conversation renders, panel absent, no separator exposed", () => {
|
|
32
|
+
render(<SessionViewerLayout conversation={conversation()} panel={null} />);
|
|
33
|
+
expect(screen.getByTestId("conversation").textContent).toBe("chat");
|
|
34
|
+
expect(screen.queryByTestId("panel")).toBeNull();
|
|
35
|
+
// The drag separator is CSS-hidden and aria-hidden while collapsed.
|
|
36
|
+
expect(screen.queryByRole("separator")).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("open (panel provided): panel renders and the separator is operable", () => {
|
|
40
|
+
render(<SessionViewerLayout conversation={conversation()} panel={panel()} />);
|
|
41
|
+
expect(screen.getByTestId("panel").textContent).toBe("panel");
|
|
42
|
+
const separator = screen.getByRole("separator", { name: "Resize chat panel" });
|
|
43
|
+
expect(separator.getAttribute("tabindex")).toBe("0");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("resizeAriaLabel names the separator in the host's vocabulary", () => {
|
|
47
|
+
render(
|
|
48
|
+
<SessionViewerLayout
|
|
49
|
+
conversation={conversation()}
|
|
50
|
+
panel={panel()}
|
|
51
|
+
resizeAriaLabel="Resize composer panel"
|
|
52
|
+
/>,
|
|
53
|
+
);
|
|
54
|
+
expect(
|
|
55
|
+
screen.queryByRole("separator", { name: "Resize composer panel" }),
|
|
56
|
+
).not.toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("never remounts the conversation across an open/close toggle", () => {
|
|
60
|
+
const { rerender } = render(
|
|
61
|
+
<SessionViewerLayout conversation={conversation()} panel={null} />,
|
|
62
|
+
);
|
|
63
|
+
const node = screen.getByTestId("conversation");
|
|
64
|
+
|
|
65
|
+
rerender(<SessionViewerLayout conversation={conversation()} panel={panel()} />);
|
|
66
|
+
expect(screen.getByTestId("conversation")).toBe(node);
|
|
67
|
+
|
|
68
|
+
rerender(<SessionViewerLayout conversation={conversation()} panel={null} />);
|
|
69
|
+
expect(screen.getByTestId("conversation")).toBe(node);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("open panel sizes the conversation pane at the shared default width", () => {
|
|
73
|
+
render(<SessionViewerLayout conversation={conversation()} panel={panel()} />);
|
|
74
|
+
const pane = screen.getByTestId("conversation").parentElement!;
|
|
75
|
+
expect(pane.style.width).toBe("420px");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("responsive (default): the open-panel conversation pane collapses below lg", () => {
|
|
79
|
+
const { container } = render(
|
|
80
|
+
<SessionViewerLayout conversation={conversation()} panel={panel()} />,
|
|
81
|
+
);
|
|
82
|
+
const pane = screen.getByTestId("conversation").parentElement!;
|
|
83
|
+
expect(pane.className).toContain("max-lg:hidden");
|
|
84
|
+
// The separator hides with it, so the panel is the full row below lg.
|
|
85
|
+
expect(container.querySelectorAll('[class*="max-lg:hidden"]').length).toBe(2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("responsive={false}: no below-lg collapse anywhere (fixed-canvas hosts)", () => {
|
|
89
|
+
const { container } = render(
|
|
90
|
+
<SessionViewerLayout
|
|
91
|
+
conversation={conversation()}
|
|
92
|
+
panel={panel()}
|
|
93
|
+
responsive={false}
|
|
94
|
+
/>,
|
|
95
|
+
);
|
|
96
|
+
expect(container.querySelector('[class*="max-lg:hidden"]')).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("reads no localStorage without a splitStorageKey (deterministic hosts)", () => {
|
|
100
|
+
const getItem = vi.spyOn(Storage.prototype, "getItem");
|
|
101
|
+
render(<SessionViewerLayout conversation={conversation()} panel={panel()} />);
|
|
102
|
+
expect(getItem).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("seeds the pane width from the host's splitStorageKey when given", () => {
|
|
106
|
+
localStorage.setItem("stgm-test-chat-width", "512");
|
|
107
|
+
render(
|
|
108
|
+
<SessionViewerLayout
|
|
109
|
+
conversation={conversation()}
|
|
110
|
+
panel={panel()}
|
|
111
|
+
splitStorageKey="stgm-test-chat-width"
|
|
112
|
+
/>,
|
|
113
|
+
);
|
|
114
|
+
const pane = screen.getByTestId("conversation").parentElement!;
|
|
115
|
+
expect(pane.style.width).toBe("512px");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("renders headerActions beside the chip in the top-right corner", () => {
|
|
119
|
+
render(
|
|
120
|
+
<SessionViewerLayout
|
|
121
|
+
conversation={conversation()}
|
|
122
|
+
panel={null}
|
|
123
|
+
headerActions={<button type="button">Share</button>}
|
|
124
|
+
chip={<button type="button">Toggle panel</button>}
|
|
125
|
+
/>,
|
|
126
|
+
);
|
|
127
|
+
const actions = screen.getByRole("button", { name: "Share" });
|
|
128
|
+
const chip = screen.getByRole("button", { name: "Toggle panel" });
|
|
129
|
+
expect(actions.parentElement).toBe(chip.parentElement);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("renders no control corner when both chip and headerActions are omitted", () => {
|
|
133
|
+
const { container } = render(
|
|
134
|
+
<SessionViewerLayout conversation={conversation()} panel={null} />,
|
|
135
|
+
);
|
|
136
|
+
// The corner is the only `top-2`-positioned element in the layout (the
|
|
137
|
+
// separator's inner hit target is also `absolute`, so match the corner).
|
|
138
|
+
expect(container.querySelector('[class*="top-2"]')).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
});
|
package/src/session/index.ts
CHANGED
|
@@ -109,6 +109,12 @@ export type { SessionViewerProps } from "./SessionViewer.js";
|
|
|
109
109
|
export { NewSessionViewer } from "./NewSessionViewer.js";
|
|
110
110
|
export type { NewSessionViewerProps } from "./NewSessionViewer.js";
|
|
111
111
|
|
|
112
|
+
// SessionViewerLayout — the conversation-plus-panel frame both viewers
|
|
113
|
+
// share, exported for platform builders composing a custom session surface
|
|
114
|
+
// at the shipped console's geometry.
|
|
115
|
+
export { SessionViewerLayout } from "./SessionViewerLayout.js";
|
|
116
|
+
export type { SessionViewerLayoutProps } from "./SessionViewerLayout.js";
|
|
117
|
+
|
|
112
118
|
// Unified session-panel controller — drives the WorkspaceSurface open-editor
|
|
113
119
|
// group, the collapsed/open state, and the rail-view FSM (for platform
|
|
114
120
|
// builders embedding the surface directly).
|
|
@@ -118,6 +124,14 @@ export type {
|
|
|
118
124
|
UseSessionPanelOptions,
|
|
119
125
|
} from "./useSessionPanel.js";
|
|
120
126
|
|
|
127
|
+
// Session rail views — composes the session facets (Config / Changes /
|
|
128
|
+
// Artifacts / Usage) as the panel's rail, with the console's own labels,
|
|
129
|
+
// icons, badges, and contextual-visibility rules. Exported so custom
|
|
130
|
+
// session surfaces inherit the rail by construction instead of
|
|
131
|
+
// re-declaring it.
|
|
132
|
+
export { useSessionRailViews } from "./useSessionRailViews.js";
|
|
133
|
+
export type { UseSessionRailViewsOptions } from "./useSessionRailViews.js";
|
|
134
|
+
|
|
121
135
|
// Plan draft — viewer-owned in-place edit of the session's current plan
|
|
122
136
|
// (edit-as-input; the published plan.md artifact is never mutated).
|
|
123
137
|
export { usePlanDraft, planDraftKey } from "./usePlanDraft.js";
|
|
@@ -55,13 +55,13 @@ export interface UseSessionRailViewsOptions {
|
|
|
55
55
|
* (a cloud session with git workspace entries). When `true` the Changes
|
|
56
56
|
* facet is offered from the start — with an honest pre-push state — instead
|
|
57
57
|
* of only materializing once a write-back exists, so "where does my work
|
|
58
|
-
* go?" always has an answer
|
|
58
|
+
* go?" always has an answer (the facet's `expectsWriteBack` prop).
|
|
59
59
|
* @default false
|
|
60
60
|
*/
|
|
61
61
|
readonly expectsWriteBack?: boolean;
|
|
62
62
|
/**
|
|
63
63
|
* Whether the session's latest execution is settled (terminal) — forwarded
|
|
64
|
-
* to the Changes facet's pre-push states
|
|
64
|
+
* to the Changes facet's pre-push states (its `isSettled` prop).
|
|
65
65
|
* @default false
|
|
66
66
|
*/
|
|
67
67
|
readonly changesSettled?: boolean;
|
|
@@ -17,6 +17,8 @@ export interface UseSkillCountOptions {
|
|
|
17
17
|
* @default "org"
|
|
18
18
|
*/
|
|
19
19
|
readonly scope?: ResourceListScope;
|
|
20
|
+
/** Opaque token that forces a recount when its value changes. */
|
|
21
|
+
readonly refetchToken?: unknown;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/** Return value of {@link useSkillCount}. */
|