@stigmer/react 3.12.3 → 3.12.4
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/steps/IdentityStep.d.ts.map +1 -1
- package/agent/steps/IdentityStep.js +2 -1
- package/agent/steps/IdentityStep.js.map +1 -1
- package/agent-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
- package/agent-instance/AgentInstanceDetailPanel.js +2 -1
- package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
- package/api-key/CreateApiKeyForm.d.ts.map +1 -1
- package/api-key/CreateApiKeyForm.js +2 -1
- package/api-key/CreateApiKeyForm.js.map +1 -1
- package/channel/ChannelConversationsDialog.d.ts.map +1 -1
- package/channel/ChannelConversationsDialog.js +2 -1
- package/channel/ChannelConversationsDialog.js.map +1 -1
- package/channel/ChannelTemplatesDialog.d.ts.map +1 -1
- package/channel/ChannelTemplatesDialog.js +2 -1
- package/channel/ChannelTemplatesDialog.js.map +1 -1
- package/channel/ConnectSlackDialog.d.ts.map +1 -1
- package/channel/ConnectSlackDialog.js +2 -1
- package/channel/ConnectSlackDialog.js.map +1 -1
- package/channel/connect/ServingAppSection.d.ts.map +1 -1
- package/channel/connect/ServingAppSection.js +2 -1
- package/channel/connect/ServingAppSection.js.map +1 -1
- package/channel-app/CreateChannelAppForm.d.ts.map +1 -1
- package/channel-app/CreateChannelAppForm.js +2 -1
- package/channel-app/CreateChannelAppForm.js.map +1 -1
- package/conversation/ConversationListPane.d.ts.map +1 -1
- package/conversation/ConversationListPane.js +2 -1
- package/conversation/ConversationListPane.js.map +1 -1
- package/conversation/ConversationTimelineView.d.ts.map +1 -1
- package/conversation/ConversationTimelineView.js +2 -1
- package/conversation/ConversationTimelineView.js.map +1 -1
- package/cursor-accounts/CursorAccountsConsole.d.ts.map +1 -1
- package/cursor-accounts/CursorAccountsConsole.js +2 -1
- package/cursor-accounts/CursorAccountsConsole.js.map +1 -1
- package/cursor-accounts/MemberKeysPanel.d.ts.map +1 -1
- package/cursor-accounts/MemberKeysPanel.js +2 -1
- package/cursor-accounts/MemberKeysPanel.js.map +1 -1
- package/dashboard/DashboardFailedRuns.d.ts.map +1 -1
- package/dashboard/DashboardFailedRuns.js +2 -1
- package/dashboard/DashboardFailedRuns.js.map +1 -1
- package/dependency-graph/DependencyGraph.d.ts.map +1 -1
- package/dependency-graph/DependencyGraph.js +2 -1
- package/dependency-graph/DependencyGraph.js.map +1 -1
- package/dependency-graph/DependencyTreeNode.d.ts.map +1 -1
- package/dependency-graph/DependencyTreeNode.js +2 -1
- package/dependency-graph/DependencyTreeNode.js.map +1 -1
- package/environment/EnvironmentPicker.d.ts.map +1 -1
- package/environment/EnvironmentPicker.js +2 -1
- package/environment/EnvironmentPicker.js.map +1 -1
- package/error/SecretFlowErrorGuide.d.ts.map +1 -1
- package/error/SecretFlowErrorGuide.js +2 -1
- package/error/SecretFlowErrorGuide.js.map +1 -1
- package/execution/ArtifactContentBody.d.ts.map +1 -1
- package/execution/ArtifactContentBody.js +2 -1
- package/execution/ArtifactContentBody.js.map +1 -1
- package/execution/ArtifactsWidget.d.ts.map +1 -1
- package/execution/ArtifactsWidget.js +2 -1
- package/execution/ArtifactsWidget.js.map +1 -1
- package/execution/FileChangeProgressBar.d.ts.map +1 -1
- package/execution/FileChangeProgressBar.js +2 -1
- package/execution/FileChangeProgressBar.js.map +1 -1
- package/execution/TodoList.d.ts.map +1 -1
- package/execution/TodoList.js +2 -1
- package/execution/TodoList.js.map +1 -1
- package/execution/WriteBacksWidget.d.ts.map +1 -1
- package/execution/WriteBacksWidget.js +2 -1
- package/execution/WriteBacksWidget.js.map +1 -1
- package/execution/tool-categories.d.ts +5 -22
- package/execution/tool-categories.d.ts.map +1 -1
- package/execution/tool-categories.js +8 -38
- package/execution/tool-categories.js.map +1 -1
- package/iam-policy/PeopleWithAccess.d.ts.map +1 -1
- package/iam-policy/PeopleWithAccess.js +2 -1
- package/iam-policy/PeopleWithAccess.js.map +1 -1
- package/iam-policy/PrincipalPicker.d.ts.map +1 -1
- package/iam-policy/PrincipalPicker.js +2 -1
- package/iam-policy/PrincipalPicker.js.map +1 -1
- package/iam-policy/RoleSelector.d.ts.map +1 -1
- package/iam-policy/RoleSelector.js +2 -1
- package/iam-policy/RoleSelector.js.map +1 -1
- package/identity-provider/CreateIdentityProviderForm.d.ts.map +1 -1
- package/identity-provider/CreateIdentityProviderForm.js +2 -1
- package/identity-provider/CreateIdentityProviderForm.js.map +1 -1
- package/identity-provider/IdentityProviderDetailPanel.d.ts.map +1 -1
- package/identity-provider/IdentityProviderDetailPanel.js +2 -1
- package/identity-provider/IdentityProviderDetailPanel.js.map +1 -1
- package/identity-provider/IdentityProviderWizard.d.ts.map +1 -1
- package/identity-provider/IdentityProviderWizard.js +2 -1
- package/identity-provider/IdentityProviderWizard.js.map +1 -1
- package/index.d.ts +3 -1
- package/index.d.ts.map +1 -1
- package/index.js +3 -0
- package/index.js.map +1 -1
- package/internal/element-resets.d.ts +17 -0
- package/internal/element-resets.d.ts.map +1 -0
- package/internal/element-resets.js +32 -0
- package/internal/element-resets.js.map +1 -0
- package/internal/file-tree/FileTreeNode.d.ts.map +1 -1
- package/internal/file-tree/FileTreeNode.js +2 -1
- package/internal/file-tree/FileTreeNode.js.map +1 -1
- package/internal/markdown-components.d.ts +7 -0
- package/internal/markdown-components.d.ts.map +1 -1
- package/internal/markdown-components.js +15 -5
- package/internal/markdown-components.js.map +1 -1
- package/invitation/InvitationManager.d.ts.map +1 -1
- package/invitation/InvitationManager.js +2 -1
- package/invitation/InvitationManager.js.map +1 -1
- package/manifest/ApplyManifestDialog.d.ts.map +1 -1
- package/manifest/ApplyManifestDialog.js +2 -1
- package/manifest/ApplyManifestDialog.js.map +1 -1
- package/mcp-server/steps/IdentityTransportStep.d.ts.map +1 -1
- package/mcp-server/steps/IdentityTransportStep.js +2 -1
- package/mcp-server/steps/IdentityTransportStep.js.map +1 -1
- package/package.json +4 -4
- package/platform-client/CreatePlatformClientForm.d.ts.map +1 -1
- package/platform-client/CreatePlatformClientForm.js +2 -1
- package/platform-client/CreatePlatformClientForm.js.map +1 -1
- package/platform-client/PlatformClientDetailPanel.d.ts.map +1 -1
- package/platform-client/PlatformClientDetailPanel.js +2 -1
- package/platform-client/PlatformClientDetailPanel.js.map +1 -1
- package/pricing-governance/BaselineEditor.d.ts.map +1 -1
- package/pricing-governance/BaselineEditor.js +2 -1
- package/pricing-governance/BaselineEditor.js.map +1 -1
- package/pricing-governance/PricingGovernanceConsole.d.ts.map +1 -1
- package/pricing-governance/PricingGovernanceConsole.js +2 -1
- package/pricing-governance/PricingGovernanceConsole.js.map +1 -1
- package/provider-standing/ProviderStandingAccessNotice.d.ts +11 -0
- package/provider-standing/ProviderStandingAccessNotice.d.ts.map +1 -0
- package/provider-standing/ProviderStandingAccessNotice.js +14 -0
- package/provider-standing/ProviderStandingAccessNotice.js.map +1 -0
- package/provider-standing/ProviderStandingConsole.d.ts +24 -0
- package/provider-standing/ProviderStandingConsole.d.ts.map +1 -0
- package/provider-standing/ProviderStandingConsole.js +98 -0
- package/provider-standing/ProviderStandingConsole.js.map +1 -0
- package/provider-standing/index.d.ts +6 -0
- package/provider-standing/index.d.ts.map +1 -0
- package/provider-standing/index.js +6 -0
- package/provider-standing/index.js.map +1 -0
- package/provider-standing/useProviderStanding.d.ts +34 -0
- package/provider-standing/useProviderStanding.d.ts.map +1 -0
- package/provider-standing/useProviderStanding.js +26 -0
- package/provider-standing/useProviderStanding.js.map +1 -0
- package/provider.d.ts +12 -0
- package/provider.d.ts.map +1 -1
- package/provider.js +12 -0
- package/provider.js.map +1 -1
- package/resource-creation/StepIndicator.d.ts.map +1 -1
- package/resource-creation/StepIndicator.js +2 -1
- package/resource-creation/StepIndicator.js.map +1 -1
- package/schedule/ScheduleDetailView.d.ts.map +1 -1
- package/schedule/ScheduleDetailView.js +3 -2
- package/schedule/ScheduleDetailView.js.map +1 -1
- package/schedule/ScheduleRunsTable.d.ts.map +1 -1
- package/schedule/ScheduleRunsTable.js +2 -1
- package/schedule/ScheduleRunsTable.js.map +1 -1
- package/session/facets/ArtifactsTab.d.ts.map +1 -1
- package/session/facets/ArtifactsTab.js +2 -1
- package/session/facets/ArtifactsTab.js.map +1 -1
- package/session/facets/ChangesTab.d.ts.map +1 -1
- package/session/facets/ChangesTab.js +2 -1
- package/session/facets/ChangesTab.js.map +1 -1
- package/session/facets/UsageTab.d.ts +10 -3
- package/session/facets/UsageTab.d.ts.map +1 -1
- package/session/facets/UsageTab.js +42 -6
- package/session/facets/UsageTab.js.map +1 -1
- package/session/index.d.ts +1 -1
- package/session/index.d.ts.map +1 -1
- package/session/index.js.map +1 -1
- package/session/useSessionUsage.d.ts +25 -0
- package/session/useSessionUsage.d.ts.map +1 -1
- package/session/useSessionUsage.js +11 -0
- package/session/useSessionUsage.js.map +1 -1
- package/settings/settings-nav.d.ts.map +1 -1
- package/settings/settings-nav.js +7 -1
- package/settings/settings-nav.js.map +1 -1
- package/settings/useSettingsNavGroups.d.ts.map +1 -1
- package/settings/useSettingsNavGroups.js +4 -1
- package/settings/useSettingsNavGroups.js.map +1 -1
- package/sharing/ShareAgentDialog.d.ts.map +1 -1
- package/sharing/ShareAgentDialog.js +2 -1
- package/sharing/ShareAgentDialog.js.map +1 -1
- package/sidebar/WorkspaceSidebar.d.ts.map +1 -1
- package/sidebar/WorkspaceSidebar.js +2 -1
- package/sidebar/WorkspaceSidebar.js.map +1 -1
- package/skill/SkillFileBrowser.d.ts.map +1 -1
- package/skill/SkillFileBrowser.js +2 -1
- package/skill/SkillFileBrowser.js.map +1 -1
- package/skill/internal/fetchAndUnpackArtifact.d.ts +1 -1
- package/skill/internal/fetchAndUnpackArtifact.d.ts.map +1 -1
- package/skill/internal/fetchAndUnpackArtifact.js +47 -3
- package/skill/internal/fetchAndUnpackArtifact.js.map +1 -1
- package/skill/useSkillDiff.d.ts.map +1 -1
- package/skill/useSkillDiff.js +7 -1
- package/skill/useSkillDiff.js.map +1 -1
- package/skill/useSkillUpload.d.ts.map +1 -1
- package/skill/useSkillUpload.js +22 -4
- package/skill/useSkillUpload.js.map +1 -1
- package/src/__tests__/bare-host-resets.layout.test.tsx +184 -0
- package/src/agent/steps/IdentityStep.tsx +2 -1
- package/src/agent-instance/AgentInstanceDetailPanel.tsx +2 -1
- package/src/api-key/CreateApiKeyForm.tsx +2 -1
- package/src/channel/ChannelConversationsDialog.tsx +2 -1
- package/src/channel/ChannelTemplatesDialog.tsx +2 -1
- package/src/channel/ConnectSlackDialog.tsx +2 -1
- package/src/channel/connect/ServingAppSection.tsx +2 -1
- package/src/channel-app/CreateChannelAppForm.tsx +2 -1
- package/src/conversation/ConversationListPane.tsx +2 -5
- package/src/conversation/ConversationTimelineView.tsx +2 -1
- package/src/cursor-accounts/CursorAccountsConsole.tsx +2 -1
- package/src/cursor-accounts/MemberKeysPanel.tsx +2 -1
- package/src/dashboard/DashboardFailedRuns.tsx +2 -1
- package/src/dependency-graph/DependencyGraph.tsx +2 -1
- package/src/dependency-graph/DependencyTreeNode.tsx +2 -1
- package/src/environment/EnvironmentPicker.tsx +2 -1
- package/src/error/SecretFlowErrorGuide.tsx +3 -2
- package/src/execution/ArtifactContentBody.tsx +2 -1
- package/src/execution/ArtifactsWidget.tsx +2 -1
- package/src/execution/FileChangeProgressBar.tsx +2 -1
- package/src/execution/TodoList.tsx +2 -1
- package/src/execution/WriteBacksWidget.tsx +2 -1
- package/src/execution/tool-categories.ts +17 -41
- package/src/iam-policy/PeopleWithAccess.tsx +2 -1
- package/src/iam-policy/PrincipalPicker.tsx +2 -0
- package/src/iam-policy/RoleSelector.tsx +2 -1
- package/src/identity-provider/CreateIdentityProviderForm.tsx +2 -1
- package/src/identity-provider/IdentityProviderDetailPanel.tsx +2 -1
- package/src/identity-provider/IdentityProviderWizard.tsx +2 -1
- package/src/index.ts +13 -0
- package/src/internal/element-resets.ts +33 -0
- package/src/internal/file-tree/FileTreeNode.tsx +2 -1
- package/src/internal/markdown-components.tsx +15 -5
- package/src/invitation/InvitationManager.tsx +3 -2
- package/src/manifest/ApplyManifestDialog.tsx +2 -1
- package/src/mcp-server/steps/IdentityTransportStep.tsx +3 -2
- package/src/platform-client/CreatePlatformClientForm.tsx +4 -3
- package/src/platform-client/PlatformClientDetailPanel.tsx +4 -3
- package/src/pricing-governance/BaselineEditor.tsx +3 -2
- package/src/pricing-governance/PricingGovernanceConsole.tsx +2 -1
- package/src/provider-standing/ProviderStandingAccessNotice.tsx +36 -0
- package/src/provider-standing/ProviderStandingConsole.tsx +216 -0
- package/src/provider-standing/__tests__/ProviderStandingConsole.test.tsx +143 -0
- package/src/provider-standing/index.ts +8 -0
- package/src/provider-standing/useProviderStanding.ts +50 -0
- package/src/provider.tsx +12 -0
- package/src/resource-creation/StepIndicator.tsx +2 -1
- package/src/schedule/ScheduleDetailView.tsx +3 -2
- package/src/schedule/ScheduleRunsTable.tsx +2 -1
- package/src/session/__tests__/UsageTab.test.tsx +136 -0
- package/src/session/__tests__/useSessionUsage.test.ts +19 -0
- package/src/session/facets/ArtifactsTab.tsx +2 -1
- package/src/session/facets/ChangesTab.tsx +2 -1
- package/src/session/facets/UsageTab.tsx +98 -6
- package/src/session/index.ts +1 -0
- package/src/session/useSessionUsage.ts +37 -0
- package/src/settings/__tests__/useSettingsNavGroups.test.tsx +11 -1
- package/src/settings/settings-nav.ts +7 -0
- package/src/settings/useSettingsNavGroups.ts +8 -1
- package/src/sharing/ShareAgentDialog.tsx +2 -1
- package/src/sidebar/WorkspaceSidebar.tsx +2 -1
- package/src/skill/SkillFileBrowser.tsx +2 -1
- package/src/skill/__tests__/SkillFileBrowser.test.tsx +10 -2
- package/src/skill/__tests__/fetchAndUnpackArtifact.test.ts +90 -0
- package/src/skill/__tests__/useSkillUpload.test.ts +69 -0
- package/src/skill/internal/fetchAndUnpackArtifact.ts +54 -4
- package/src/skill/useSkillDiff.ts +7 -1
- package/src/skill/useSkillUpload.ts +25 -6
- package/src/workflow/FailedRunsWidget.tsx +2 -1
- package/src/workflow/PendingApprovalsWidget.tsx +2 -1
- package/src/workflow/facets/WorkflowArtifactsTab.tsx +2 -1
- package/src/workflow/facets/WorkflowUsageTab.tsx +2 -1
- package/src/workflow/inspector/WorkflowSummaryPanel.tsx +2 -1
- package/src/workflow/inspector/tabs/BranchesTab.tsx +2 -1
- package/src/workflow/inspector/tabs/IterationTab.tsx +2 -1
- package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +2 -1
- package/src/workspace/ExplorerTree.tsx +2 -1
- package/src/workspace/WorkspaceContentSearch.tsx +2 -1
- package/src/workspace/WorkspaceFileSearch.tsx +2 -1
- package/src/workspace/WorkspaceSummary.tsx +2 -1
- package/styles.css +1 -1
- package/workflow/FailedRunsWidget.d.ts.map +1 -1
- package/workflow/FailedRunsWidget.js +2 -1
- package/workflow/FailedRunsWidget.js.map +1 -1
- package/workflow/PendingApprovalsWidget.d.ts.map +1 -1
- package/workflow/PendingApprovalsWidget.js +2 -1
- package/workflow/PendingApprovalsWidget.js.map +1 -1
- package/workflow/facets/WorkflowArtifactsTab.d.ts.map +1 -1
- package/workflow/facets/WorkflowArtifactsTab.js +2 -1
- package/workflow/facets/WorkflowArtifactsTab.js.map +1 -1
- package/workflow/facets/WorkflowUsageTab.d.ts.map +1 -1
- package/workflow/facets/WorkflowUsageTab.js +2 -1
- package/workflow/facets/WorkflowUsageTab.js.map +1 -1
- package/workflow/inspector/WorkflowSummaryPanel.d.ts.map +1 -1
- package/workflow/inspector/WorkflowSummaryPanel.js +2 -1
- package/workflow/inspector/WorkflowSummaryPanel.js.map +1 -1
- package/workflow/inspector/tabs/BranchesTab.d.ts.map +1 -1
- package/workflow/inspector/tabs/BranchesTab.js +2 -1
- package/workflow/inspector/tabs/BranchesTab.js.map +1 -1
- package/workflow/inspector/tabs/IterationTab.d.ts.map +1 -1
- package/workflow/inspector/tabs/IterationTab.js +2 -1
- package/workflow/inspector/tabs/IterationTab.js.map +1 -1
- package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceDetailPanel.js +2 -1
- package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
- package/workspace/ExplorerTree.d.ts.map +1 -1
- package/workspace/ExplorerTree.js +2 -1
- package/workspace/ExplorerTree.js.map +1 -1
- package/workspace/WorkspaceContentSearch.d.ts.map +1 -1
- package/workspace/WorkspaceContentSearch.js +2 -1
- package/workspace/WorkspaceContentSearch.js.map +1 -1
- package/workspace/WorkspaceFileSearch.d.ts.map +1 -1
- package/workspace/WorkspaceFileSearch.js +2 -1
- package/workspace/WorkspaceFileSearch.js.map +1 -1
- package/workspace/WorkspaceSummary.d.ts.map +1 -1
- package/workspace/WorkspaceSummary.js +2 -1
- package/workspace/WorkspaceSummary.js.map +1 -1
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
artifactKey,
|
|
8
8
|
type SessionArtifactEntry,
|
|
9
9
|
} from "../useSessionArtifacts.js";
|
|
10
|
+
import { UNSTYLED_LIST } from "../../internal/element-resets.js";
|
|
10
11
|
import { ArtifactPreviewModal } from "../../execution/ArtifactPreviewModal.js";
|
|
11
12
|
import { ArtifactRow } from "../../execution/ArtifactRow.js";
|
|
12
13
|
import { isPlanArtifact } from "../../library/detect-plan-artifact.js";
|
|
@@ -123,7 +124,7 @@ export function ArtifactsTab({
|
|
|
123
124
|
|
|
124
125
|
return (
|
|
125
126
|
<>
|
|
126
|
-
<ul role="list" className=
|
|
127
|
+
<ul role="list" className={`${UNSTYLED_LIST} stg:flex stg:flex-col`}>
|
|
127
128
|
{artifacts.map((entry) => (
|
|
128
129
|
<ArtifactRow
|
|
129
130
|
key={artifactKey(entry.artifact)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
4
|
+
import { UNSTYLED_LIST } from "../../internal/element-resets.js";
|
|
4
5
|
import { useSessionWriteBacks } from "../useSessionWriteBacks.js";
|
|
5
6
|
import { WriteBackCard } from "../../execution/WriteBackCard.js";
|
|
6
7
|
|
|
@@ -48,7 +49,7 @@ export function ChangesTab({
|
|
|
48
49
|
// Dense row groups matching the Artifacts facet's list — gap separates
|
|
49
50
|
// entries only in multi-repo sessions (single-entry lists stay seamless).
|
|
50
51
|
return (
|
|
51
|
-
<ul role="list" className=
|
|
52
|
+
<ul role="list" className={`${UNSTYLED_LIST} stg:flex stg:flex-col stg:gap-3`}>
|
|
52
53
|
{writeBacks.map((entry) => (
|
|
53
54
|
<li key={entry.writeBack.workspaceEntryName}>
|
|
54
55
|
<WriteBackCard writeBack={entry.writeBack} />
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
5
|
+
import { useSessionUsage, type ExecutionUsageEntry } from "../useSessionUsage.js";
|
|
6
|
+
import { UsageWidget, formatCost } from "../../execution/UsageWidget.js";
|
|
6
7
|
|
|
7
8
|
export interface UsageTabProps {
|
|
8
9
|
readonly executions: readonly AgentExecution[];
|
|
@@ -11,9 +12,16 @@ export interface UsageTabProps {
|
|
|
11
12
|
/**
|
|
12
13
|
* Usage facet for the session panel (a `useSessionRailViews` rail view).
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Renders the session totals ({@link UsageWidget}) plus the #362 model
|
|
16
|
+
* provenance list: per execution, the billing-RESOLVED model with its
|
|
17
|
+
* cost — for a Cursor Auto run, the only honest answer to "which model
|
|
18
|
+
* actually ran and what did it cost", since the requested model is empty
|
|
19
|
+
* by definition there — paired with the tier the runner REQUESTED (the
|
|
20
|
+
* streaming summary's audit record that the account default was never
|
|
21
|
+
* left in control). Requested-vs-billed divergence is a platform alarm
|
|
22
|
+
* (`stigmer.billing.service_tier.mismatch`), not a per-row UI concern.
|
|
23
|
+
*
|
|
24
|
+
* Shows an empty state when no usage data is available.
|
|
17
25
|
*/
|
|
18
26
|
export function UsageTab({ executions }: UsageTabProps) {
|
|
19
27
|
const usage = useSessionUsage(executions);
|
|
@@ -28,5 +36,89 @@ export function UsageTab({ executions }: UsageTabProps) {
|
|
|
28
36
|
);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
return
|
|
39
|
+
return (
|
|
40
|
+
<div className="stg:flex stg:flex-col stg:gap-4">
|
|
41
|
+
<UsageWidget executions={executions} />
|
|
42
|
+
{usage.executionBreakdown.length > 0 && (
|
|
43
|
+
<ExecutionModelList
|
|
44
|
+
entries={usage.executionBreakdown}
|
|
45
|
+
executions={executions}
|
|
46
|
+
/>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Per-execution model provenance rows, in the report's chronological
|
|
54
|
+
* order: `#N resolved-model [tier requested] [Estimated] $cost`.
|
|
55
|
+
*/
|
|
56
|
+
function ExecutionModelList({
|
|
57
|
+
entries,
|
|
58
|
+
executions,
|
|
59
|
+
}: {
|
|
60
|
+
readonly entries: readonly ExecutionUsageEntry[];
|
|
61
|
+
readonly executions: readonly AgentExecution[];
|
|
62
|
+
}) {
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
className="stg:flex stg:flex-col stg:gap-1"
|
|
66
|
+
role="list"
|
|
67
|
+
aria-label="Per-execution model and tier"
|
|
68
|
+
>
|
|
69
|
+
<div className="stg:text-xs stg:font-medium stg:text-foreground">
|
|
70
|
+
Models per run
|
|
71
|
+
</div>
|
|
72
|
+
{entries.map((entry, index) => {
|
|
73
|
+
const requestedTier = requestedTierLabel(executions, entry.executionId);
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
key={entry.executionId}
|
|
77
|
+
className="stg:flex stg:items-baseline stg:justify-between stg:gap-2 stg:text-xs stg:text-muted-foreground"
|
|
78
|
+
role="listitem"
|
|
79
|
+
>
|
|
80
|
+
<span className="stg:truncate">
|
|
81
|
+
<span className="stg:tabular-nums">#{index + 1}</span>{" "}
|
|
82
|
+
{entry.resolvedModel || "—"}
|
|
83
|
+
{requestedTier && (
|
|
84
|
+
<span className="stg:ml-1 stg:rounded stg:bg-muted stg:px-1 stg:py-0.5 stg:text-[10px] stg:font-medium stg:leading-none">
|
|
85
|
+
{requestedTier} requested
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
{entry.isEstimated && (
|
|
89
|
+
<span className="stg:ml-1 stg:rounded stg:bg-muted stg:px-1 stg:py-0.5 stg:text-[10px] stg:font-medium stg:leading-none">
|
|
90
|
+
Estimated
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
</span>
|
|
94
|
+
<span className="stg:shrink-0 stg:tabular-nums">
|
|
95
|
+
{formatCost(entry.billableCostUsd)}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The tier the runner requested for one execution, from its streaming
|
|
106
|
+
* usage summary — present once the runner has translated the execution
|
|
107
|
+
* config, absent for executions that predate the tier attribute. Never
|
|
108
|
+
* "unspecified": the runner records the RESOLVED tier (#357's audit
|
|
109
|
+
* contract).
|
|
110
|
+
*/
|
|
111
|
+
function requestedTierLabel(
|
|
112
|
+
executions: readonly AgentExecution[],
|
|
113
|
+
executionId: string,
|
|
114
|
+
): string | null {
|
|
115
|
+
const match = executions.find((e) => e.metadata?.id === executionId);
|
|
116
|
+
switch (match?.status?.streamingUsage?.requestedServiceTier) {
|
|
117
|
+
case ServiceTier.FAST:
|
|
118
|
+
return "fast";
|
|
119
|
+
case ServiceTier.STANDARD:
|
|
120
|
+
return "standard";
|
|
121
|
+
default:
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
32
124
|
}
|
package/src/session/index.ts
CHANGED
|
@@ -41,6 +41,25 @@ export interface ModelCostEntry {
|
|
|
41
41
|
readonly callCount: number;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Per-execution model provenance from billing records (stigmer/stigmer#362).
|
|
46
|
+
*/
|
|
47
|
+
export interface ExecutionUsageEntry {
|
|
48
|
+
/** Execution identifier (`metadata.id`). */
|
|
49
|
+
readonly executionId: string;
|
|
50
|
+
/**
|
|
51
|
+
* Billing-resolved primary model — what the provider actually served.
|
|
52
|
+
* For a Cursor Auto execution this is the after-the-fact answer to
|
|
53
|
+
* "which model did Auto pick", which the requested model (empty by
|
|
54
|
+
* definition there) can never give.
|
|
55
|
+
*/
|
|
56
|
+
readonly resolvedModel: string;
|
|
57
|
+
/** Billable cost in USD for this execution. */
|
|
58
|
+
readonly billableCostUsd: number;
|
|
59
|
+
/** `true` while this execution's records are still provisional. */
|
|
60
|
+
readonly isEstimated: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
44
63
|
/** Return value of {@link useSessionUsage}. */
|
|
45
64
|
export interface UseSessionUsageReturn {
|
|
46
65
|
/** Aggregate estimated cost in USD across all models. */
|
|
@@ -59,6 +78,13 @@ export interface UseSessionUsageReturn {
|
|
|
59
78
|
readonly llmCallCount: number;
|
|
60
79
|
/** Per-model cost and token breakdown. */
|
|
61
80
|
readonly modelBreakdown: readonly ModelCostEntry[];
|
|
81
|
+
/**
|
|
82
|
+
* Per-execution resolved model + cost, chronological, from billing
|
|
83
|
+
* records (#362). Empty until the first authoritative record lands —
|
|
84
|
+
* the streaming fallback deliberately contributes none, because the
|
|
85
|
+
* runner only knows what it REQUESTED, never what the provider served.
|
|
86
|
+
*/
|
|
87
|
+
readonly executionBreakdown: readonly ExecutionUsageEntry[];
|
|
62
88
|
/** Model with the highest usage in this session. */
|
|
63
89
|
readonly primaryModel: string;
|
|
64
90
|
/** Provider of the primary model. */
|
|
@@ -82,6 +108,7 @@ const EMPTY: UseSessionUsageReturn = {
|
|
|
82
108
|
cacheCreationTokens: 0,
|
|
83
109
|
llmCallCount: 0,
|
|
84
110
|
modelBreakdown: [],
|
|
111
|
+
executionBreakdown: [],
|
|
85
112
|
primaryModel: "",
|
|
86
113
|
primaryProvider: "",
|
|
87
114
|
hasUsage: false,
|
|
@@ -122,6 +149,12 @@ function mapReport(report: GetSessionUsageReportOutput): UseSessionUsageReturn {
|
|
|
122
149
|
cacheCreationTokens: Number(agg.cacheCreationInputTokens),
|
|
123
150
|
llmCallCount,
|
|
124
151
|
modelBreakdown: report.modelBreakdown.map(mapModelUsage),
|
|
152
|
+
executionBreakdown: report.executions.map((e) => ({
|
|
153
|
+
executionId: e.executionId,
|
|
154
|
+
resolvedModel: e.primaryModel,
|
|
155
|
+
billableCostUsd: microsToUsd(e.billableCostMicros),
|
|
156
|
+
isEstimated: e.isEstimated,
|
|
157
|
+
})),
|
|
125
158
|
primaryModel: agg.primaryModel,
|
|
126
159
|
primaryProvider: agg.primaryProvider,
|
|
127
160
|
hasUsage: llmCallCount > 0 || totalTokens > 0 || billableCost > 0,
|
|
@@ -169,6 +202,10 @@ function aggregateStreamingUsage(
|
|
|
169
202
|
cacheReadTokens,
|
|
170
203
|
cacheCreationTokens,
|
|
171
204
|
llmCallCount: turnCount,
|
|
205
|
+
// No executionBreakdown here on purpose: the runner only knows the
|
|
206
|
+
// REQUESTED model; resolved-model rows exist only once billing
|
|
207
|
+
// records land (#362).
|
|
208
|
+
executionBreakdown: [],
|
|
172
209
|
modelBreakdown: model
|
|
173
210
|
? [
|
|
174
211
|
{
|
|
@@ -48,6 +48,7 @@ describe("useSettingsNavGroups", () => {
|
|
|
48
48
|
verdicts = {
|
|
49
49
|
can_manage_model_pricing: true,
|
|
50
50
|
can_manage_cursor_accounts: true,
|
|
51
|
+
can_view_provider_standing: true,
|
|
51
52
|
};
|
|
52
53
|
render(<GroupsProbe />);
|
|
53
54
|
|
|
@@ -56,7 +57,7 @@ describe("useSettingsNavGroups", () => {
|
|
|
56
57
|
`${BASE_LABELS},${PLATFORM_SETTINGS_NAV_GROUP.label}`,
|
|
57
58
|
);
|
|
58
59
|
expect(el.getAttribute("data-platform-items")).toBe(
|
|
59
|
-
"Pricing Governance,Cursor Accounts",
|
|
60
|
+
"Pricing Governance,Cursor Accounts,Provider Standing",
|
|
60
61
|
);
|
|
61
62
|
});
|
|
62
63
|
|
|
@@ -78,6 +79,15 @@ describe("useSettingsNavGroups", () => {
|
|
|
78
79
|
);
|
|
79
80
|
});
|
|
80
81
|
|
|
82
|
+
it("filters per item: standing-only operator sees only Provider Standing", () => {
|
|
83
|
+
verdicts = { can_view_provider_standing: true };
|
|
84
|
+
render(<GroupsProbe />);
|
|
85
|
+
|
|
86
|
+
expect(screen.getByTestId("groups").getAttribute("data-platform-items")).toBe(
|
|
87
|
+
"Provider Standing",
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
81
91
|
it("returns exactly the base groups when not authorized", () => {
|
|
82
92
|
function IdentityProbe() {
|
|
83
93
|
const groups = useSettingsNavGroups();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentType } from "react";
|
|
2
2
|
import {
|
|
3
|
+
Activity,
|
|
3
4
|
AppWindow,
|
|
4
5
|
BarChart3,
|
|
5
6
|
Box,
|
|
@@ -123,5 +124,11 @@ export const PLATFORM_SETTINGS_NAV_GROUP: SettingsNavGroup = {
|
|
|
123
124
|
icon: MousePointerClick,
|
|
124
125
|
requiredPermission: "can_manage_cursor_accounts",
|
|
125
126
|
},
|
|
127
|
+
{
|
|
128
|
+
href: "/settings/provider-standing",
|
|
129
|
+
label: "Provider Standing",
|
|
130
|
+
icon: Activity,
|
|
131
|
+
requiredPermission: "can_view_provider_standing",
|
|
132
|
+
},
|
|
126
133
|
],
|
|
127
134
|
};
|
|
@@ -52,14 +52,21 @@ export function useSettingsNavGroups(): readonly SettingsNavGroup[] {
|
|
|
52
52
|
"can_manage_cursor_accounts",
|
|
53
53
|
{ fail: "closed" },
|
|
54
54
|
);
|
|
55
|
+
const providerStanding = useCheckPermission(
|
|
56
|
+
PLATFORM_RESOURCE,
|
|
57
|
+
"can_view_provider_standing",
|
|
58
|
+
{ fail: "closed" },
|
|
59
|
+
);
|
|
55
60
|
|
|
56
61
|
const pricingAllowed = pricing.allowed;
|
|
57
62
|
const cursorAccountsAllowed = cursorAccounts.allowed;
|
|
63
|
+
const providerStandingAllowed = providerStanding.allowed;
|
|
58
64
|
|
|
59
65
|
return useMemo(() => {
|
|
60
66
|
const verdicts: Record<string, boolean> = {
|
|
61
67
|
can_manage_model_pricing: pricingAllowed,
|
|
62
68
|
can_manage_cursor_accounts: cursorAccountsAllowed,
|
|
69
|
+
can_view_provider_standing: providerStandingAllowed,
|
|
63
70
|
};
|
|
64
71
|
|
|
65
72
|
const visibleItems = PLATFORM_SETTINGS_NAV_GROUP.items.filter(
|
|
@@ -77,5 +84,5 @@ export function useSettingsNavGroups(): readonly SettingsNavGroup[] {
|
|
|
77
84
|
...SETTINGS_NAV_GROUPS,
|
|
78
85
|
{ ...PLATFORM_SETTINGS_NAV_GROUP, items: visibleItems },
|
|
79
86
|
];
|
|
80
|
-
}, [pricingAllowed, cursorAccountsAllowed]);
|
|
87
|
+
}, [pricingAllowed, cursorAccountsAllowed, providerStandingAllowed]);
|
|
81
88
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useCallback, useId, useMemo, useRef, useState, type FormEvent } from "react";
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
5
|
import { DialogShell } from "../internal/DialogShell.js";
|
|
6
|
+
import { UNSTYLED_LIST } from "../internal/element-resets.js";
|
|
6
7
|
import {
|
|
7
8
|
MAX_ALLOWED_ORIGINS,
|
|
8
9
|
StigmerError,
|
|
@@ -1316,7 +1317,7 @@ function OriginsEditor({
|
|
|
1316
1317
|
</p>
|
|
1317
1318
|
|
|
1318
1319
|
{draft.allowedOrigins.length > 0 && (
|
|
1319
|
-
<ul className="stg:mt-2 stg:flex stg:flex-col stg:divide-y stg:divide-border stg:rounded-md stg:border stg:border-border">
|
|
1320
|
+
<ul className={cn(UNSTYLED_LIST, "stg:mt-2 stg:flex stg:flex-col stg:divide-y stg:divide-border stg:rounded-md stg:border stg:border-border")}>
|
|
1320
1321
|
{draft.allowedOrigins.map((origin) => (
|
|
1321
1322
|
<li
|
|
1322
1323
|
key={origin}
|
|
@@ -15,6 +15,7 @@ import type { RecentActivityEntry, RecentActivityGroup } from "../activity/types
|
|
|
15
15
|
import { groupRecentActivityByTime } from "../activity/group-activity.js";
|
|
16
16
|
import { formatRelativeTime } from "../activity/format-relative-time.js";
|
|
17
17
|
import { recentActivityStatusBadge } from "../activity/entry-status-badge.js";
|
|
18
|
+
import { UNSTYLED_LIST } from "../internal/element-resets.js";
|
|
18
19
|
import { ScrollArea } from "../internal/scroll-area.js";
|
|
19
20
|
import {
|
|
20
21
|
Tooltip,
|
|
@@ -313,7 +314,7 @@ function ActivityGroupList({
|
|
|
313
314
|
<p className="stg:text-sidebar-muted-foreground stg:mb-1 stg:px-2 stg:text-[10px] stg:font-medium stg:tracking-wider stg:uppercase">
|
|
314
315
|
{group.label}
|
|
315
316
|
</p>
|
|
316
|
-
<ul className=
|
|
317
|
+
<ul className={`${UNSTYLED_LIST} stg:space-y-0.5`} role="list">
|
|
317
318
|
{group.entries.map((entry) => (
|
|
318
319
|
<ActivityEntry
|
|
319
320
|
key={entry.id}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useMemo, useState } from "react";
|
|
4
4
|
import Markdown from "react-markdown";
|
|
5
5
|
import { cn } from "@stigmer/theme";
|
|
6
|
+
import { UNSTYLED_LIST } from "../internal/element-resets.js";
|
|
6
7
|
import { MARKDOWN_COMPONENTS, REMARK_PLUGINS, stripFrontmatter } from "../internal/markdown-components.js";
|
|
7
8
|
import { buildFileTree, FileTreeNode } from "../internal/file-tree/index.js";
|
|
8
9
|
import { useSkillArtifact } from "./useSkillArtifact.js";
|
|
@@ -132,7 +133,7 @@ export function SkillFileBrowser({
|
|
|
132
133
|
className="stg:border-b stg:border-border stg:md:border-b-0 stg:md:border-r stg:overflow-y-auto"
|
|
133
134
|
aria-label="Skill package file tree"
|
|
134
135
|
>
|
|
135
|
-
<ul className="stg:py-1" role="tree">
|
|
136
|
+
<ul className={cn(UNSTYLED_LIST, "stg:py-1")} role="tree">
|
|
136
137
|
{treeNodes.map((node) => (
|
|
137
138
|
<FileTreeNode
|
|
138
139
|
key={node.path}
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, vi, afterEach } from "vitest";
|
|
|
2
2
|
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
4
|
import { zipSync, strToU8 } from "fflate";
|
|
5
|
-
import type
|
|
5
|
+
import { StigmerError, type Stigmer } from "@stigmer/sdk";
|
|
6
6
|
import { StigmerContext } from "../../context";
|
|
7
7
|
import { SkillFileBrowser } from "../SkillFileBrowser";
|
|
8
8
|
|
|
@@ -12,14 +12,22 @@ function wrapperFor(stigmer: Stigmer) {
|
|
|
12
12
|
);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** Connect's Unimplemented code — what a pre-transfer-lane server answers. */
|
|
16
|
+
const CODE_UNIMPLEMENTED = 12;
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
19
|
* Builds a real ZIP (via fflate, the same library the unpacker uses) and a
|
|
17
20
|
* Stigmer client whose `skill.getArtifact` serves it — so tests exercise the
|
|
18
21
|
* genuine fetch → unzip → browse path instead of stubbing the unpacker.
|
|
22
|
+
* The download-URL mint answers Unimplemented (a pre-transfer-lane server),
|
|
23
|
+
* pinning that the browser degrades to the unary lane it always used.
|
|
19
24
|
*/
|
|
20
25
|
function makeStigmerServing(files: Record<string, Uint8Array>) {
|
|
21
26
|
const getArtifact = vi.fn().mockResolvedValue({ artifact: zipSync(files) });
|
|
22
|
-
|
|
27
|
+
const getArtifactDownloadUrl = vi
|
|
28
|
+
.fn()
|
|
29
|
+
.mockRejectedValue(new StigmerError("unknown", "unimplemented", CODE_UNIMPLEMENTED));
|
|
30
|
+
return { skill: { getArtifact, getArtifactDownloadUrl } } as unknown as Stigmer;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
function renderBrowser(stigmer: Stigmer) {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Pins for the artifact fetch lane selection (stigmer#701): URL-first via
|
|
2
|
+
// getArtifactDownloadUrl, with the two deliberate browser degrade arms —
|
|
3
|
+
// Unimplemented mint (pre-lane server) and a failed URL fetch (the shape a
|
|
4
|
+
// missing bucket CORS policy takes in a browser).
|
|
5
|
+
import { describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { strToU8, zipSync } from "fflate";
|
|
7
|
+
import { StigmerError, type Stigmer } from "@stigmer/sdk";
|
|
8
|
+
import { fetchAndUnpackArtifact } from "../internal/fetchAndUnpackArtifact.js";
|
|
9
|
+
|
|
10
|
+
const CODE_UNIMPLEMENTED = 12;
|
|
11
|
+
const ZIP = zipSync({ "SKILL.md": strToU8("# hi") });
|
|
12
|
+
|
|
13
|
+
function stigmerWith(overrides: {
|
|
14
|
+
mint?: ReturnType<typeof vi.fn>;
|
|
15
|
+
unary?: ReturnType<typeof vi.fn>;
|
|
16
|
+
fetch?: unknown;
|
|
17
|
+
}): Stigmer {
|
|
18
|
+
return {
|
|
19
|
+
skill: {
|
|
20
|
+
getArtifactDownloadUrl: overrides.mint,
|
|
21
|
+
getArtifact: overrides.unary,
|
|
22
|
+
},
|
|
23
|
+
fetch: overrides.fetch,
|
|
24
|
+
} as unknown as Stigmer;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("fetchAndUnpackArtifact lane selection", () => {
|
|
28
|
+
it("fetches via the minted download URL, never the unary lane", async () => {
|
|
29
|
+
const mint = vi.fn().mockResolvedValue({ url: "https://r2.example/skills/get" });
|
|
30
|
+
const unary = vi.fn();
|
|
31
|
+
const fetchImpl = vi.fn(async () => new Response(new Uint8Array(ZIP).buffer, { status: 200 }));
|
|
32
|
+
|
|
33
|
+
const result = await fetchAndUnpackArtifact(
|
|
34
|
+
stigmerWith({ mint, unary, fetch: fetchImpl }),
|
|
35
|
+
"skills/org/skill/hash.zip",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(fetchImpl).toHaveBeenCalledWith("https://r2.example/skills/get");
|
|
39
|
+
expect(unary).not.toHaveBeenCalled();
|
|
40
|
+
expect(result.contentMap.get("SKILL.md")).toBe("# hi");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("degrades to the unary lane when the mint answers Unimplemented (pre-lane server)", async () => {
|
|
44
|
+
const mint = vi
|
|
45
|
+
.fn()
|
|
46
|
+
.mockRejectedValue(new StigmerError("unknown", "unimplemented", CODE_UNIMPLEMENTED));
|
|
47
|
+
const unary = vi.fn().mockResolvedValue({ artifact: ZIP });
|
|
48
|
+
|
|
49
|
+
const result = await fetchAndUnpackArtifact(
|
|
50
|
+
stigmerWith({ mint, unary }),
|
|
51
|
+
"skills/org/skill/hash.zip",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(unary).toHaveBeenCalledTimes(1);
|
|
55
|
+
expect(result.contentMap.get("SKILL.md")).toBe("# hi");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("degrades to the unary lane when the URL fetch itself fails (browser CORS shape), warning once", async () => {
|
|
59
|
+
const mint = vi.fn().mockResolvedValue({ url: "https://r2.example/skills/get" });
|
|
60
|
+
const unary = vi.fn().mockResolvedValue({ artifact: ZIP });
|
|
61
|
+
const fetchImpl = vi.fn(async () => {
|
|
62
|
+
throw new TypeError("Failed to fetch");
|
|
63
|
+
});
|
|
64
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const result = await fetchAndUnpackArtifact(
|
|
68
|
+
stigmerWith({ mint, unary, fetch: fetchImpl }),
|
|
69
|
+
"skills/org/skill/hash.zip",
|
|
70
|
+
);
|
|
71
|
+
expect(unary).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
73
|
+
expect(result.contentMap.get("SKILL.md")).toBe("# hi");
|
|
74
|
+
} finally {
|
|
75
|
+
warn.mockRestore();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("propagates a non-Unimplemented mint failure — a real error must not be masked", async () => {
|
|
80
|
+
const mint = vi
|
|
81
|
+
.fn()
|
|
82
|
+
.mockRejectedValue(new StigmerError("not-found", "no such artifact", 5));
|
|
83
|
+
const unary = vi.fn();
|
|
84
|
+
|
|
85
|
+
await expect(
|
|
86
|
+
fetchAndUnpackArtifact(stigmerWith({ mint, unary }), "skills/org/skill/hash.zip"),
|
|
87
|
+
).rejects.toThrow(/no such artifact/);
|
|
88
|
+
expect(unary).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { renderHook, act } from "@testing-library/react";
|
|
3
|
+
import { zipSync, strToU8 } from "fflate";
|
|
4
|
+
import { useSkillUpload } from "../useSkillUpload";
|
|
5
|
+
|
|
6
|
+
const SKILL_MD = "---\nname: my-skill\ndescription: A test skill\n---\n# My Skill\n";
|
|
7
|
+
|
|
8
|
+
/** Build a real ZIP (fflate, the same library the hook unpacks with) as a File. */
|
|
9
|
+
function zipFile(files: Record<string, Uint8Array>): File {
|
|
10
|
+
return new File([zipSync(files)], "skill.zip", { type: "application/zip" });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function process(files: Record<string, Uint8Array>) {
|
|
14
|
+
const { result } = renderHook(() => useSkillUpload());
|
|
15
|
+
await act(() => result.current.processFile(zipFile(files)));
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("useSkillUpload", () => {
|
|
20
|
+
it("accepts a root SKILL.md and produces a preview", async () => {
|
|
21
|
+
const result = await process({
|
|
22
|
+
"SKILL.md": strToU8(SKILL_MD),
|
|
23
|
+
"references/schema.md": strToU8("# Schema"),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(result.current.validationError).toBeNull();
|
|
27
|
+
expect(result.current.preview?.name).toBe("my-skill");
|
|
28
|
+
expect(result.current.preview?.description).toBe("A test skill");
|
|
29
|
+
expect(result.current.artifact).not.toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// The layout contract is root-only on both editions (DD-018, #452). The
|
|
33
|
+
// preview exists to catch the "zipped the folder instead of its contents"
|
|
34
|
+
// mistake BEFORE upload — accepting nested here and failing at push is the
|
|
35
|
+
// exact confusion issue #684 pins.
|
|
36
|
+
it("rejects a nested-only SKILL.md with the zip-the-contents hint", async () => {
|
|
37
|
+
const result = await process({
|
|
38
|
+
"my-skill/SKILL.md": strToU8(SKILL_MD),
|
|
39
|
+
"my-skill/references/schema.md": strToU8("# Schema"),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(result.current.preview).toBeNull();
|
|
43
|
+
expect(result.current.artifact).toBeNull();
|
|
44
|
+
expect(result.current.validationError).toBe(
|
|
45
|
+
"Found my-skill/SKILL.md — SKILL.md must be at the archive root. " +
|
|
46
|
+
"Zip the skill folder's contents, not the folder itself.",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("detects a deeply nested SKILL.md for the hint (any depth, server parity)", async () => {
|
|
51
|
+
const result = await process({
|
|
52
|
+
"wrapper/my-skill/SKILL.md": strToU8(SKILL_MD),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(result.current.preview).toBeNull();
|
|
56
|
+
expect(result.current.validationError).toContain("Found wrapper/my-skill/SKILL.md");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("rejects an archive with no SKILL.md anywhere with the plain root-level message", async () => {
|
|
60
|
+
const result = await process({
|
|
61
|
+
"README.md": strToU8("# Not a skill"),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result.current.preview).toBeNull();
|
|
65
|
+
expect(result.current.validationError).toBe(
|
|
66
|
+
"ZIP must contain a SKILL.md file at the root level",
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { create } from "@bufbuild/protobuf";
|
|
2
2
|
import { GetArtifactRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/io_pb";
|
|
3
|
-
import type
|
|
3
|
+
import { isUnimplemented, type Stigmer } from "@stigmer/sdk";
|
|
4
4
|
import type { SkillFileEntry } from "../useSkillUpload.js";
|
|
5
5
|
|
|
6
6
|
/** Unpacked artifact: file entries for browsing + content map for reading. */
|
|
@@ -20,11 +20,10 @@ export async function fetchAndUnpackArtifact(
|
|
|
20
20
|
stigmer: Stigmer,
|
|
21
21
|
artifactStorageKey: string,
|
|
22
22
|
): Promise<UnpackedArtifact> {
|
|
23
|
-
const
|
|
24
|
-
const response = await stigmer.skill.getArtifact(request);
|
|
23
|
+
const artifact = await fetchArtifactBytes(stigmer, artifactStorageKey);
|
|
25
24
|
|
|
26
25
|
const { unzipSync, strFromU8 } = await import("fflate");
|
|
27
|
-
const unzipped = unzipSync(
|
|
26
|
+
const unzipped = unzipSync(artifact);
|
|
28
27
|
|
|
29
28
|
const entries = Object.entries(unzipped);
|
|
30
29
|
const files: SkillFileEntry[] = entries.map(([path, data]) => ({
|
|
@@ -49,3 +48,54 @@ export async function fetchAndUnpackArtifact(
|
|
|
49
48
|
|
|
50
49
|
return { files, contentMap };
|
|
51
50
|
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fetch the artifact ZIP bytes, URL-first (the transfer lane, stigmer#675):
|
|
54
|
+
* the bytes ride HTTP, so any valid skill (up to the 100MB limit) is
|
|
55
|
+
* fetchable — the unary `getArtifact` RPC is capped at the server's 10MB
|
|
56
|
+
* gRPC message limit.
|
|
57
|
+
*
|
|
58
|
+
* Fallbacks to the unary lane, deliberately wider than the runner's
|
|
59
|
+
* mint-Unimplemented-only contract because this code runs in a browser:
|
|
60
|
+
*
|
|
61
|
+
* - `Unimplemented` from the mint — a pre-transfer-lane server (the code
|
|
62
|
+
* clients key their fallback on, same as every other lane client).
|
|
63
|
+
* - A failed HTTP fetch of the minted URL — in a browser this is how a
|
|
64
|
+
* missing bucket CORS policy surfaces (an opaque `TypeError`,
|
|
65
|
+
* indistinguishable from a network failure). The unary lane was the
|
|
66
|
+
* status quo and still serves everything under the message cap, so
|
|
67
|
+
* degrading beats breaking the console; the warn keeps it observable.
|
|
68
|
+
*/
|
|
69
|
+
async function fetchArtifactBytes(
|
|
70
|
+
stigmer: Stigmer,
|
|
71
|
+
artifactStorageKey: string,
|
|
72
|
+
): Promise<Uint8Array> {
|
|
73
|
+
const request = create(GetArtifactRequestSchema, { artifactStorageKey });
|
|
74
|
+
|
|
75
|
+
let minted;
|
|
76
|
+
try {
|
|
77
|
+
minted = await stigmer.skill.getArtifactDownloadUrl(request);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (isUnimplemented(err)) {
|
|
80
|
+
const response = await stigmer.skill.getArtifact(request);
|
|
81
|
+
return response.artifact;
|
|
82
|
+
}
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const doFetch = stigmer.fetch ?? globalThis.fetch;
|
|
88
|
+
const resp = await doFetch(minted.url);
|
|
89
|
+
if (!resp.ok) {
|
|
90
|
+
throw new Error(`artifact download failed with HTTP ${resp.status}`);
|
|
91
|
+
}
|
|
92
|
+
return new Uint8Array(await resp.arrayBuffer());
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.warn(
|
|
95
|
+
"[stigmer] skill artifact download URL fetch failed (bucket CORS not provisioned for this origin?) — falling back to the unary lane",
|
|
96
|
+
err,
|
|
97
|
+
);
|
|
98
|
+
const response = await stigmer.skill.getArtifact(request);
|
|
99
|
+
return response.artifact;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -21,7 +21,13 @@ export interface UseSkillDiffReturn {
|
|
|
21
21
|
readonly error: Error | null;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Maximum COMBINED unpacked text of both artifacts (10 MB) — a browser
|
|
26
|
+
* memory guard on holding and diffing two extracted file trees, NOT a
|
|
27
|
+
* transport limit. The transfer lane (stigmer#675) removed the download
|
|
28
|
+
* cap, so arbitrarily large valid skills can be *fetched*; what stays
|
|
29
|
+
* bounded is what this hook is willing to diff in tab memory.
|
|
30
|
+
*/
|
|
25
31
|
const MAX_ARTIFACT_BYTES = 10 * 1024 * 1024;
|
|
26
32
|
|
|
27
33
|
// ---------------------------------------------------------------------------
|