@stigmer/react 3.3.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/billing/index.d.ts +0 -18
- package/billing/index.d.ts.map +1 -1
- package/billing/index.js +4 -9
- package/billing/index.js.map +1 -1
- package/cursor-accounts/CursorAccountEditor.d.ts +7 -1
- package/cursor-accounts/CursorAccountEditor.d.ts.map +1 -1
- package/cursor-accounts/CursorAccountEditor.js +21 -5
- package/cursor-accounts/CursorAccountEditor.js.map +1 -1
- package/cursor-accounts/CursorAccountsConsole.d.ts +9 -5
- package/cursor-accounts/CursorAccountsConsole.d.ts.map +1 -1
- package/cursor-accounts/CursorAccountsConsole.js +34 -140
- package/cursor-accounts/CursorAccountsConsole.js.map +1 -1
- package/cursor-accounts/MemberCoverageTable.d.ts +29 -0
- package/cursor-accounts/MemberCoverageTable.d.ts.map +1 -0
- package/cursor-accounts/MemberCoverageTable.js +150 -0
- package/cursor-accounts/MemberCoverageTable.js.map +1 -0
- package/cursor-accounts/MemberKeysPanel.d.ts +25 -0
- package/cursor-accounts/MemberKeysPanel.d.ts.map +1 -0
- package/cursor-accounts/MemberKeysPanel.js +115 -0
- package/cursor-accounts/MemberKeysPanel.js.map +1 -0
- package/cursor-accounts/badges.d.ts +9 -0
- package/cursor-accounts/badges.d.ts.map +1 -0
- package/cursor-accounts/badges.js +10 -0
- package/cursor-accounts/badges.js.map +1 -0
- package/cursor-accounts/cursor-account-coverage.d.ts +41 -0
- package/cursor-accounts/cursor-account-coverage.d.ts.map +1 -0
- package/cursor-accounts/cursor-account-coverage.js +26 -0
- package/cursor-accounts/cursor-account-coverage.js.map +1 -0
- package/cursor-accounts/cursor-account-format.d.ts +13 -0
- package/cursor-accounts/cursor-account-format.d.ts.map +1 -1
- package/cursor-accounts/cursor-account-format.js +16 -1
- package/cursor-accounts/cursor-account-format.js.map +1 -1
- package/datastore/CollectionRecordsBrowser.js +18 -4
- package/datastore/CollectionRecordsBrowser.js.map +1 -1
- package/datastore/RecordFilterBuilder.d.ts +8 -1
- package/datastore/RecordFilterBuilder.d.ts.map +1 -1
- package/datastore/RecordFilterBuilder.js +2 -2
- package/datastore/RecordFilterBuilder.js.map +1 -1
- package/datastore/recordFilter.d.ts +9 -1
- package/datastore/recordFilter.d.ts.map +1 -1
- package/datastore/recordFilter.js +12 -1
- package/datastore/recordFilter.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/execution/ExecutionProgress.d.ts.map +1 -1
- package/execution/ExecutionProgress.js +13 -4
- package/execution/ExecutionProgress.js.map +1 -1
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +10 -2
- package/execution/MessageThread.js.map +1 -1
- package/execution/useCreateAgentExecution.d.ts +31 -1
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +2 -0
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/index.d.ts +8 -6
- package/index.d.ts.map +1 -1
- package/index.js +8 -4
- package/index.js.map +1 -1
- package/internal/form-primitives.d.ts +10 -0
- package/internal/form-primitives.d.ts.map +1 -0
- package/{billing → internal}/form-primitives.js +5 -9
- package/internal/form-primitives.js.map +1 -0
- 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/pricing-governance/BaselineEditor.d.ts.map +1 -0
- package/{billing → pricing-governance}/BaselineEditor.js +19 -2
- package/pricing-governance/BaselineEditor.js.map +1 -0
- package/pricing-governance/ModelCatalogPanel.d.ts.map +1 -0
- package/pricing-governance/ModelCatalogPanel.js.map +1 -0
- package/pricing-governance/ModelGovernanceDetail.d.ts.map +1 -0
- package/pricing-governance/ModelGovernanceDetail.js.map +1 -0
- package/pricing-governance/OperatorAccessNotice.d.ts.map +1 -0
- package/pricing-governance/OperatorAccessNotice.js.map +1 -0
- package/pricing-governance/PricingGovernanceConsole.d.ts.map +1 -0
- package/{billing → pricing-governance}/PricingGovernanceConsole.js +1 -1
- package/pricing-governance/PricingGovernanceConsole.js.map +1 -0
- package/pricing-governance/PricingGovernancePanel.d.ts.map +1 -0
- package/pricing-governance/PricingGovernancePanel.js.map +1 -0
- package/pricing-governance/RetireConfirm.d.ts.map +1 -0
- package/{billing → pricing-governance}/RetireConfirm.js +1 -1
- package/pricing-governance/RetireConfirm.js.map +1 -0
- package/pricing-governance/governance-primitives.d.ts.map +1 -0
- package/pricing-governance/governance-primitives.js.map +1 -0
- package/pricing-governance/index.d.ts +19 -0
- package/pricing-governance/index.d.ts.map +1 -0
- package/pricing-governance/index.js +20 -0
- package/pricing-governance/index.js.map +1 -0
- package/pricing-governance/pricing-format.d.ts.map +1 -0
- package/pricing-governance/pricing-format.js.map +1 -0
- package/pricing-governance/useDecidePricingOverride.d.ts.map +1 -0
- package/pricing-governance/useDecidePricingOverride.js.map +1 -0
- package/pricing-governance/useModelGovernanceView.d.ts.map +1 -0
- package/pricing-governance/useModelGovernanceView.js.map +1 -0
- package/pricing-governance/useModelPricingBaselines.d.ts.map +1 -0
- package/pricing-governance/useModelPricingBaselines.js.map +1 -0
- package/pricing-governance/usePricingGovernance.d.ts.map +1 -0
- package/pricing-governance/usePricingGovernance.js.map +1 -0
- package/pricing-governance/useRetireModelPricingBaseline.d.ts.map +1 -0
- package/pricing-governance/useRetireModelPricingBaseline.js.map +1 -0
- package/pricing-governance/useUpsertModelPricingBaseline.d.ts.map +1 -0
- package/pricing-governance/useUpsertModelPricingBaseline.js.map +1 -0
- 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/useCreateSession.d.ts +28 -0
- package/session/useCreateSession.d.ts.map +1 -1
- package/session/useCreateSession.js +2 -1
- package/session/useCreateSession.js.map +1 -1
- package/session/useNewSessionFlow.d.ts +30 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +7 -1
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionRailViews.d.ts +2 -2
- package/session/useUpdateSession.d.ts +29 -10
- package/session/useUpdateSession.d.ts.map +1 -1
- package/session/useUpdateSession.js +29 -10
- package/session/useUpdateSession.js.map +1 -1
- 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/billing/index.ts +5 -37
- package/src/cursor-accounts/CursorAccountEditor.tsx +61 -13
- package/src/cursor-accounts/CursorAccountsConsole.tsx +44 -400
- package/src/cursor-accounts/MemberCoverageTable.tsx +439 -0
- package/src/cursor-accounts/MemberKeysPanel.tsx +278 -0
- package/src/cursor-accounts/__tests__/CursorAccountsConsole.test.tsx +306 -10
- package/src/cursor-accounts/__tests__/cursor-account-coverage.test.ts +73 -0
- package/src/cursor-accounts/badges.tsx +26 -0
- package/src/cursor-accounts/cursor-account-coverage.ts +71 -0
- package/src/cursor-accounts/cursor-account-format.ts +17 -1
- package/src/datastore/CollectionRecordsBrowser.tsx +29 -3
- package/src/datastore/RecordFilterBuilder.tsx +12 -1
- package/src/datastore/__tests__/CollectionRecordsBrowser.test.tsx +38 -0
- package/src/datastore/__tests__/RecordFilterBuilder.test.tsx +14 -0
- package/src/datastore/__tests__/recordFilter.test.ts +13 -0
- package/src/datastore/recordFilter.ts +15 -1
- package/src/datastore/useDatastoreCount.ts +2 -0
- package/src/execution/ExecutionProgress.tsx +14 -4
- package/src/execution/MessageThread.tsx +10 -2
- package/src/execution/__tests__/ExecutionProgress.test.tsx +67 -0
- package/src/execution/__tests__/MessageThread.test.tsx +44 -0
- package/src/execution/__tests__/useCreateAgentExecution.test.tsx +86 -0
- package/src/execution/useCreateAgentExecution.ts +41 -6
- package/src/index.ts +43 -24
- package/src/{billing → internal}/form-primitives.tsx +4 -32
- 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/{billing → pricing-governance}/BaselineEditor.tsx +36 -3
- package/src/{billing → pricing-governance}/PricingGovernanceConsole.tsx +1 -1
- package/src/{billing → pricing-governance}/RetireConfirm.tsx +1 -1
- package/src/pricing-governance/index.ts +50 -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/__tests__/useCreateSession.test.tsx +94 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +33 -0
- package/src/session/index.ts +14 -0
- package/src/session/useCreateSession.ts +30 -0
- package/src/session/useNewSessionFlow.ts +45 -1
- package/src/session/useSessionRailViews.tsx +2 -2
- package/src/session/useUpdateSession.ts +29 -10
- 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/styles.css +1 -1
- 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
- package/billing/BaselineEditor.d.ts.map +0 -1
- package/billing/BaselineEditor.js.map +0 -1
- package/billing/ModelCatalogPanel.d.ts.map +0 -1
- package/billing/ModelCatalogPanel.js.map +0 -1
- package/billing/ModelGovernanceDetail.d.ts.map +0 -1
- package/billing/ModelGovernanceDetail.js.map +0 -1
- package/billing/OperatorAccessNotice.d.ts.map +0 -1
- package/billing/OperatorAccessNotice.js.map +0 -1
- package/billing/PricingGovernanceConsole.d.ts.map +0 -1
- package/billing/PricingGovernanceConsole.js.map +0 -1
- package/billing/PricingGovernancePanel.d.ts.map +0 -1
- package/billing/PricingGovernancePanel.js.map +0 -1
- package/billing/RetireConfirm.d.ts.map +0 -1
- package/billing/RetireConfirm.js.map +0 -1
- package/billing/form-primitives.d.ts +0 -18
- package/billing/form-primitives.d.ts.map +0 -1
- package/billing/form-primitives.js.map +0 -1
- package/billing/governance-primitives.d.ts.map +0 -1
- package/billing/governance-primitives.js.map +0 -1
- package/billing/pricing-format.d.ts.map +0 -1
- package/billing/pricing-format.js.map +0 -1
- package/billing/useDecidePricingOverride.d.ts.map +0 -1
- package/billing/useDecidePricingOverride.js.map +0 -1
- package/billing/useModelGovernanceView.d.ts.map +0 -1
- package/billing/useModelGovernanceView.js.map +0 -1
- package/billing/useModelPricingBaselines.d.ts.map +0 -1
- package/billing/useModelPricingBaselines.js.map +0 -1
- package/billing/usePricingGovernance.d.ts.map +0 -1
- package/billing/usePricingGovernance.js.map +0 -1
- package/billing/useRetireModelPricingBaseline.d.ts.map +0 -1
- package/billing/useRetireModelPricingBaseline.js.map +0 -1
- package/billing/useUpsertModelPricingBaseline.d.ts.map +0 -1
- package/billing/useUpsertModelPricingBaseline.js.map +0 -1
- /package/{billing → pricing-governance}/BaselineEditor.d.ts +0 -0
- /package/{billing → pricing-governance}/ModelCatalogPanel.d.ts +0 -0
- /package/{billing → pricing-governance}/ModelCatalogPanel.js +0 -0
- /package/{billing → pricing-governance}/ModelGovernanceDetail.d.ts +0 -0
- /package/{billing → pricing-governance}/ModelGovernanceDetail.js +0 -0
- /package/{billing → pricing-governance}/OperatorAccessNotice.d.ts +0 -0
- /package/{billing → pricing-governance}/OperatorAccessNotice.js +0 -0
- /package/{billing → pricing-governance}/PricingGovernanceConsole.d.ts +0 -0
- /package/{billing → pricing-governance}/PricingGovernancePanel.d.ts +0 -0
- /package/{billing → pricing-governance}/PricingGovernancePanel.js +0 -0
- /package/{billing → pricing-governance}/RetireConfirm.d.ts +0 -0
- /package/{billing → pricing-governance}/governance-primitives.d.ts +0 -0
- /package/{billing → pricing-governance}/governance-primitives.js +0 -0
- /package/{billing → pricing-governance}/pricing-format.d.ts +0 -0
- /package/{billing → pricing-governance}/pricing-format.js +0 -0
- /package/{billing → pricing-governance}/useDecidePricingOverride.d.ts +0 -0
- /package/{billing → pricing-governance}/useDecidePricingOverride.js +0 -0
- /package/{billing → pricing-governance}/useModelGovernanceView.d.ts +0 -0
- /package/{billing → pricing-governance}/useModelGovernanceView.js +0 -0
- /package/{billing → pricing-governance}/useModelPricingBaselines.d.ts +0 -0
- /package/{billing → pricing-governance}/useModelPricingBaselines.js +0 -0
- /package/{billing → pricing-governance}/usePricingGovernance.d.ts +0 -0
- /package/{billing → pricing-governance}/usePricingGovernance.js +0 -0
- /package/{billing → pricing-governance}/useRetireModelPricingBaseline.d.ts +0 -0
- /package/{billing → pricing-governance}/useRetireModelPricingBaseline.js +0 -0
- /package/{billing → pricing-governance}/useUpsertModelPricingBaseline.d.ts +0 -0
- /package/{billing → pricing-governance}/useUpsertModelPricingBaseline.js +0 -0
- /package/src/{billing → pricing-governance}/ModelCatalogPanel.tsx +0 -0
- /package/src/{billing → pricing-governance}/ModelGovernanceDetail.tsx +0 -0
- /package/src/{billing → pricing-governance}/OperatorAccessNotice.tsx +0 -0
- /package/src/{billing → pricing-governance}/PricingGovernancePanel.tsx +0 -0
- /package/src/{billing → pricing-governance}/__tests__/ModelCatalogPanel.test.tsx +0 -0
- /package/src/{billing → pricing-governance}/__tests__/PricingGovernanceConsole.test.tsx +0 -0
- /package/src/{billing → pricing-governance}/__tests__/useModelGovernanceView.test.tsx +0 -0
- /package/src/{billing → pricing-governance}/governance-primitives.tsx +0 -0
- /package/src/{billing → pricing-governance}/pricing-format.ts +0 -0
- /package/src/{billing → pricing-governance}/useDecidePricingOverride.ts +0 -0
- /package/src/{billing → pricing-governance}/useModelGovernanceView.ts +0 -0
- /package/src/{billing → pricing-governance}/useModelPricingBaselines.ts +0 -0
- /package/src/{billing → pricing-governance}/usePricingGovernance.ts +0 -0
- /package/src/{billing → pricing-governance}/useRetireModelPricingBaseline.ts +0 -0
- /package/src/{billing → pricing-governance}/useUpsertModelPricingBaseline.ts +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { create } from "@bufbuild/protobuf";
|
|
3
|
+
import {
|
|
4
|
+
CursorAccountSyncSnapshotSchema,
|
|
5
|
+
CursorMemberKeySchema,
|
|
6
|
+
CursorMemberSpendSchema,
|
|
7
|
+
CursorTeamMemberSchema,
|
|
8
|
+
} from "@stigmer/protos/ai/stigmer/platform/cursoraccount/v1/cursor_account_pb";
|
|
9
|
+
import {
|
|
10
|
+
CursorAccountViewSchema,
|
|
11
|
+
CursorMemberKeyState,
|
|
12
|
+
CursorMemberKeyViewSchema,
|
|
13
|
+
CursorTeamMemberViewSchema,
|
|
14
|
+
} from "@stigmer/protos/ai/stigmer/platform/cursoraccount/v1/io_pb";
|
|
15
|
+
import { deriveCoverage } from "../cursor-account-coverage";
|
|
16
|
+
|
|
17
|
+
function keyView(keyId: string, state: CursorMemberKeyState) {
|
|
18
|
+
return create(CursorMemberKeyViewSchema, {
|
|
19
|
+
key: create(CursorMemberKeySchema, { keyId, boundEmail: `${keyId}@x.ai` }),
|
|
20
|
+
state,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("deriveCoverage", () => {
|
|
25
|
+
it("groups keys by server-computed state and passes coverage-gap rows through", () => {
|
|
26
|
+
const active = keyView("k-active", CursorMemberKeyState.member_key_active);
|
|
27
|
+
const removed = keyView("k-removed", CursorMemberKeyState.member_key_owner_removed);
|
|
28
|
+
const stranger = keyView("k-stranger", CursorMemberKeyState.member_key_owner_unknown);
|
|
29
|
+
const gap = create(CursorTeamMemberViewSchema, {
|
|
30
|
+
member: create(CursorTeamMemberSchema, { email: "uncovered@x.ai", role: "member" }),
|
|
31
|
+
spend: create(CursorMemberSpendSchema, { includedSpendUsdMicros: BigInt(42_000) }),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const coverage = deriveCoverage(
|
|
35
|
+
create(CursorAccountViewSchema, {
|
|
36
|
+
snapshot: create(CursorAccountSyncSnapshotSchema, { accountId: "acc-1" }),
|
|
37
|
+
keyViews: [active, removed, stranger],
|
|
38
|
+
membersWithoutKeysViews: [gap],
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(coverage.hasRoster).toBe(true);
|
|
43
|
+
expect(coverage.onTeamWithKey).toEqual([active]);
|
|
44
|
+
expect(coverage.onTeamWithoutKey).toEqual([gap]);
|
|
45
|
+
// Both "left the team" and "never on it" resolve the same way (a
|
|
46
|
+
// Cursor dashboard invite), so they share the category; the per-key
|
|
47
|
+
// state still distinguishes their labels.
|
|
48
|
+
expect(coverage.offTeamWithKey).toEqual([removed, stranger]);
|
|
49
|
+
expect(coverage.unclassified).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("withholds classification before the first sync — owner-unknown is missing data, not off-team", () => {
|
|
53
|
+
const key = keyView("k-1", CursorMemberKeyState.member_key_owner_unknown);
|
|
54
|
+
|
|
55
|
+
const coverage = deriveCoverage(
|
|
56
|
+
create(CursorAccountViewSchema, { keyViews: [key] }),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(coverage.hasRoster).toBe(false);
|
|
60
|
+
expect(coverage.unclassified).toEqual([key]);
|
|
61
|
+
expect(coverage.onTeamWithKey).toEqual([]);
|
|
62
|
+
expect(coverage.onTeamWithoutKey).toEqual([]);
|
|
63
|
+
expect(coverage.offTeamWithKey).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("handles the empty account (no keys, no roster) without inventing rows", () => {
|
|
67
|
+
const coverage = deriveCoverage(create(CursorAccountViewSchema, {}));
|
|
68
|
+
|
|
69
|
+
expect(coverage.hasRoster).toBe(false);
|
|
70
|
+
expect(coverage.unclassified).toEqual([]);
|
|
71
|
+
expect(coverage.onTeamWithKey).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { cn } from "@stigmer/theme";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Small tonal status badge shared by the cursor-accounts surfaces
|
|
5
|
+
* (account rows, coverage table cells). Internal to the module.
|
|
6
|
+
*/
|
|
7
|
+
export function StateBadge({
|
|
8
|
+
tone,
|
|
9
|
+
label,
|
|
10
|
+
}: {
|
|
11
|
+
readonly tone: "ok" | "warn" | "muted";
|
|
12
|
+
readonly label: string;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<span
|
|
16
|
+
className={cn(
|
|
17
|
+
"inline-block rounded px-1.5 py-0.5 text-[10px] font-medium",
|
|
18
|
+
tone === "ok" && "bg-accent text-primary",
|
|
19
|
+
tone === "warn" && "bg-muted-subtle text-destructive",
|
|
20
|
+
tone === "muted" && "bg-muted-subtle text-muted-foreground",
|
|
21
|
+
)}
|
|
22
|
+
>
|
|
23
|
+
{label}
|
|
24
|
+
</span>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CursorMemberKeyState,
|
|
3
|
+
type CursorAccountView,
|
|
4
|
+
type CursorMemberKeyView,
|
|
5
|
+
type CursorTeamMemberView,
|
|
6
|
+
} from "@stigmer/protos/ai/stigmer/platform/cursoraccount/v1/io_pb";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The account's roster coverage, grouped for the operator table. Pure
|
|
10
|
+
* regrouping of server-computed facts — the membership classification
|
|
11
|
+
* (`CursorMemberKeyState`) and the spend joins are computed by the
|
|
12
|
+
* backend on read (the same email rule key selection uses); this module
|
|
13
|
+
* must never re-derive them from emails or role strings.
|
|
14
|
+
*/
|
|
15
|
+
export interface CursorAccountCoverage {
|
|
16
|
+
/**
|
|
17
|
+
* Whether a roster snapshot backs the classification. Before the first
|
|
18
|
+
* sync every key reports "owner unknown" — that is missing data, not
|
|
19
|
+
* "not on the team", so classification is withheld (`unclassified`)
|
|
20
|
+
* until a sync has run.
|
|
21
|
+
*/
|
|
22
|
+
readonly hasRoster: boolean;
|
|
23
|
+
/** On the team, execution key held — the account's routable capacity. */
|
|
24
|
+
readonly onTeamWithKey: readonly CursorMemberKeyView[];
|
|
25
|
+
/**
|
|
26
|
+
* On the team, no execution key — the coverage gap. Sessions can never
|
|
27
|
+
* run under these members' identities or included quotas. Spend comes
|
|
28
|
+
* server-joined so the operator can prioritize whose key to import.
|
|
29
|
+
*/
|
|
30
|
+
readonly onTeamWithoutKey: readonly CursorTeamMemberView[];
|
|
31
|
+
/**
|
|
32
|
+
* Key held, owner not on the team: either left it (roster role
|
|
33
|
+
* "removed") or never appeared in the roster — the per-key state
|
|
34
|
+
* distinguishes the two. Resolving this category means inviting the
|
|
35
|
+
* owner on the Cursor dashboard; the next sync reclassifies.
|
|
36
|
+
*/
|
|
37
|
+
readonly offTeamWithKey: readonly CursorMemberKeyView[];
|
|
38
|
+
/** Every key, only while no roster snapshot exists (empty otherwise). */
|
|
39
|
+
readonly unclassified: readonly CursorMemberKeyView[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Group one account view into the three coverage categories (plus the
|
|
44
|
+
* pre-first-sync "unclassified" holding state). Server order is
|
|
45
|
+
* preserved within each group.
|
|
46
|
+
*/
|
|
47
|
+
export function deriveCoverage(view: CursorAccountView): CursorAccountCoverage {
|
|
48
|
+
const hasRoster = view.snapshot !== undefined;
|
|
49
|
+
|
|
50
|
+
if (!hasRoster) {
|
|
51
|
+
return {
|
|
52
|
+
hasRoster,
|
|
53
|
+
onTeamWithKey: [],
|
|
54
|
+
onTeamWithoutKey: [],
|
|
55
|
+
offTeamWithKey: [],
|
|
56
|
+
unclassified: view.keyViews,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
hasRoster,
|
|
62
|
+
onTeamWithKey: view.keyViews.filter(
|
|
63
|
+
(kv) => kv.state === CursorMemberKeyState.member_key_active,
|
|
64
|
+
),
|
|
65
|
+
onTeamWithoutKey: view.membersWithoutKeysViews,
|
|
66
|
+
offTeamWithKey: view.keyViews.filter(
|
|
67
|
+
(kv) => kv.state !== CursorMemberKeyState.member_key_active,
|
|
68
|
+
),
|
|
69
|
+
unclassified: [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { Timestamp } from "@bufbuild/protobuf/wkt";
|
|
|
2
2
|
|
|
3
3
|
// BigInt literals (0n) require an ES2020 target, which not every consuming
|
|
4
4
|
// app's tsconfig guarantees — the constructor form is target-agnostic (the
|
|
5
|
-
//
|
|
5
|
+
// pricing-governance pricing-format precedent).
|
|
6
6
|
const ZERO = BigInt(0);
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -27,3 +27,19 @@ export function formatSyncTime(ts: Timestamp | undefined): string {
|
|
|
27
27
|
minute: "2-digit",
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Cursor's pool-usage percent (0–100, live-verified) to a display string
|
|
33
|
+
* (`26.97` → `"27%"`, `0` → `"0%"`).
|
|
34
|
+
*
|
|
35
|
+
* Zero renders as "0%", matching Cursor's own Members page. An earlier
|
|
36
|
+
* revision suppressed it on the theory that Cursor only omits percent
|
|
37
|
+
* fields on non-tiered teams (so a stored 0 meant "unreported") — live
|
|
38
|
+
* data falsified that: Cursor also omits them for zero-usage members on
|
|
39
|
+
* tiered Team plans, where its dashboard shows "0%". The "unreported"
|
|
40
|
+
* signal is the absence of the member's whole spend row (the caller's
|
|
41
|
+
* em-dash case), not a zero field inside one.
|
|
42
|
+
*/
|
|
43
|
+
export function formatPoolPercent(percent: number): string {
|
|
44
|
+
return `${Math.round(percent)}%`;
|
|
45
|
+
}
|
|
@@ -209,10 +209,22 @@ function RecordsPane({
|
|
|
209
209
|
const canUpdate = verbs.has(DatastoreVerb.update);
|
|
210
210
|
const canDelete = verbs.has(DatastoreVerb.delete);
|
|
211
211
|
|
|
212
|
+
// The caller's column-level read access (empty means every field): a
|
|
213
|
+
// field-restricted read grant returns only these fields, so the grid
|
|
214
|
+
// and the filter builder narrow to them — no permanently-empty
|
|
215
|
+
// columns, no filter the server would refuse (DD-008 invariant 2).
|
|
216
|
+
const readableFields = useMemo(
|
|
217
|
+
() => access?.access.find((a) => a.verb === DatastoreVerb.read)?.readableFields ?? [],
|
|
218
|
+
[access],
|
|
219
|
+
);
|
|
220
|
+
|
|
212
221
|
const scope = { org, datastore: datastoreSlug, collection: collection.name, partition };
|
|
213
222
|
|
|
214
223
|
const filter = useMemo(() => buildRecordFilter(conditions), [conditions]);
|
|
215
|
-
const columns = useMemo(
|
|
224
|
+
const columns = useMemo(
|
|
225
|
+
() => buildRecordColumns(collection, readableFields),
|
|
226
|
+
[collection, readableFields],
|
|
227
|
+
);
|
|
216
228
|
|
|
217
229
|
// Fetch only once read access is confirmed — the denied panel is the
|
|
218
230
|
// primary render for deny-by-default, not a failed query.
|
|
@@ -297,6 +309,7 @@ function RecordsPane({
|
|
|
297
309
|
<div className="flex flex-wrap items-center gap-2">
|
|
298
310
|
<RecordFilterBuilder
|
|
299
311
|
collection={collection}
|
|
312
|
+
readableFields={readableFields}
|
|
300
313
|
conditions={conditions}
|
|
301
314
|
onChange={onConditionsChange}
|
|
302
315
|
className="min-w-0 flex-1"
|
|
@@ -435,9 +448,22 @@ function DeniedPanel({ message }: { readonly message: string }) {
|
|
|
435
448
|
* (`id`, `updated_at`, `created_by`) stay out of the default grid —
|
|
436
449
|
* they are shown in the record form's read-only summary; a column
|
|
437
450
|
* toggle is recorded growth.
|
|
451
|
+
*
|
|
452
|
+
* `readableFields` narrows the declared columns to the caller's
|
|
453
|
+
* column-level read access (empty means every field): a hidden field's
|
|
454
|
+
* cells would be permanently empty, which reads as missing data rather
|
|
455
|
+
* than restricted access.
|
|
438
456
|
*/
|
|
439
|
-
function buildRecordColumns(
|
|
440
|
-
|
|
457
|
+
function buildRecordColumns(
|
|
458
|
+
collection: CollectionDeclaration,
|
|
459
|
+
readableFields: readonly string[],
|
|
460
|
+
): RecordColumnDef[] {
|
|
461
|
+
const readable = readableFields.length > 0 ? new Set(readableFields) : null;
|
|
462
|
+
const declaredColumns =
|
|
463
|
+
readable === null
|
|
464
|
+
? collection.fields
|
|
465
|
+
: collection.fields.filter((field) => readable.has(field.name));
|
|
466
|
+
const fieldColumns: RecordColumnDef[] = declaredColumns.map((field) => ({
|
|
441
467
|
id: field.name,
|
|
442
468
|
header: field.name,
|
|
443
469
|
sortable: isSortableField(field),
|
|
@@ -29,6 +29,13 @@ import {
|
|
|
29
29
|
export interface RecordFilterBuilderProps {
|
|
30
30
|
/** The collection whose declared schema drives fields and operators. */
|
|
31
31
|
readonly collection: CollectionDeclaration;
|
|
32
|
+
/**
|
|
33
|
+
* The caller's readable fields from `describeDatastore` (the read
|
|
34
|
+
* verb's `readable_fields`). Empty or omitted means every field.
|
|
35
|
+
* When restricted, unreadable fields are not offered — the server
|
|
36
|
+
* refuses conditions on them.
|
|
37
|
+
*/
|
|
38
|
+
readonly readableFields?: readonly string[];
|
|
32
39
|
/** Active conditions (AND-combined, per the DD-005 grammar). */
|
|
33
40
|
readonly conditions: readonly RecordConditionDraft[];
|
|
34
41
|
/** Called with the full condition set on add/remove/clear. */
|
|
@@ -54,11 +61,15 @@ export interface RecordFilterBuilderProps {
|
|
|
54
61
|
*/
|
|
55
62
|
export function RecordFilterBuilder({
|
|
56
63
|
collection,
|
|
64
|
+
readableFields,
|
|
57
65
|
conditions,
|
|
58
66
|
onChange,
|
|
59
67
|
className,
|
|
60
68
|
}: RecordFilterBuilderProps) {
|
|
61
|
-
const fields = useMemo(
|
|
69
|
+
const fields = useMemo(
|
|
70
|
+
() => filterableFields(collection, readableFields),
|
|
71
|
+
[collection, readableFields],
|
|
72
|
+
);
|
|
62
73
|
|
|
63
74
|
const removeCondition = useCallback(
|
|
64
75
|
(index: number) => onChange(conditions.filter((_, i) => i !== index)),
|
|
@@ -204,4 +204,42 @@ describe("CollectionRecordsBrowser — grid and affordances", () => {
|
|
|
204
204
|
expect(screen.getByText(/No records in “bookings” yet/)).toBeTruthy(),
|
|
205
205
|
);
|
|
206
206
|
});
|
|
207
|
+
|
|
208
|
+
it("narrows the grid and filter builder to the caller's readable fields", async () => {
|
|
209
|
+
// A field-restricted read grant (readable_fields on the read verb):
|
|
210
|
+
// the server projects hidden fields out of every envelope and
|
|
211
|
+
// refuses conditions on them, so the grid must not show a
|
|
212
|
+
// permanently-empty column and the builder must not offer an
|
|
213
|
+
// unservable filter (DD-008 invariant 2).
|
|
214
|
+
const describeDatastore = vi.fn().mockResolvedValue(
|
|
215
|
+
create(DatastoreDescriptionSchema, {
|
|
216
|
+
datastore: "clinic",
|
|
217
|
+
collections: [
|
|
218
|
+
{
|
|
219
|
+
name: "bookings",
|
|
220
|
+
access: [{ verb: DatastoreVerb.read, readableFields: ["slot_date"] }],
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
const findRecords = vi.fn().mockResolvedValue(
|
|
226
|
+
create(RecordListSchema, {
|
|
227
|
+
records: [
|
|
228
|
+
create(RecordEnvelopeSchema, {
|
|
229
|
+
id: "dsr_1",
|
|
230
|
+
fields: { slot_date: "2026-07-22" } as never,
|
|
231
|
+
}),
|
|
232
|
+
],
|
|
233
|
+
total: 1,
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
renderBrowser({ describeDatastore, findRecords });
|
|
237
|
+
|
|
238
|
+
await waitFor(() => expect(screen.getByText("2026-07-22")).toBeTruthy());
|
|
239
|
+
expect(screen.getByRole("columnheader", { name: "slot_date" })).toBeTruthy();
|
|
240
|
+
expect(screen.queryByRole("columnheader", { name: "status" })).toBeNull();
|
|
241
|
+
// The filter builder's field narrowing under the same prop is
|
|
242
|
+
// pinned in RecordFilterBuilder.test.tsx (its harness mounts the
|
|
243
|
+
// provider the popover portal needs).
|
|
244
|
+
});
|
|
207
245
|
});
|
|
@@ -27,11 +27,13 @@ const COLLECTION = create(CollectionDeclarationSchema, {
|
|
|
27
27
|
function renderBuilder(
|
|
28
28
|
conditions: readonly RecordConditionDraft[],
|
|
29
29
|
onChange = vi.fn(),
|
|
30
|
+
readableFields?: readonly string[],
|
|
30
31
|
) {
|
|
31
32
|
render(
|
|
32
33
|
<StigmerProvider client={{} as never}>
|
|
33
34
|
<RecordFilterBuilder
|
|
34
35
|
collection={COLLECTION}
|
|
36
|
+
readableFields={readableFields}
|
|
35
37
|
conditions={conditions}
|
|
36
38
|
onChange={onChange}
|
|
37
39
|
/>
|
|
@@ -92,6 +94,18 @@ describe("RecordFilterBuilder — draft-then-apply popover", () => {
|
|
|
92
94
|
expect(ops).toEqual(["=", "≠", ">", "≥", "<", "≤"]);
|
|
93
95
|
});
|
|
94
96
|
|
|
97
|
+
it("narrows to the caller's readable fields; system fields stay offered", async () => {
|
|
98
|
+
// A field-restricted read grant (readable_fields on the read verb):
|
|
99
|
+
// the server refuses conditions on unreadable fields, so the
|
|
100
|
+
// builder must not offer them (DD-008 invariant 2).
|
|
101
|
+
renderBuilder([], vi.fn(), ["status"]);
|
|
102
|
+
fireEvent.click(screen.getByRole("button", { name: /Add filter/ }));
|
|
103
|
+
|
|
104
|
+
const fieldSelect = (await screen.findByLabelText(/Field/)) as HTMLSelectElement;
|
|
105
|
+
const fieldNames = Array.from(fieldSelect.options).map((o) => o.value);
|
|
106
|
+
expect(fieldNames).toEqual(["", "status", "id", "created_at", "updated_at"]);
|
|
107
|
+
});
|
|
108
|
+
|
|
95
109
|
it("applies a complete draft and closes; incomplete drafts cannot apply", async () => {
|
|
96
110
|
const onChange = renderBuilder([]);
|
|
97
111
|
fireEvent.click(screen.getByRole("button", { name: /Add filter/ }));
|
|
@@ -56,6 +56,19 @@ describe("filterableFields", () => {
|
|
|
56
56
|
const status = fields.find((f) => f.name === "status")!;
|
|
57
57
|
expect(status.operators).toContain(RecordConditionOp.is_null);
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
it("narrows to the caller's readable fields; system fields stay offered", () => {
|
|
61
|
+
// A field-restricted read grant: the server refuses conditions on
|
|
62
|
+
// unreadable fields, so the builder must not offer them.
|
|
63
|
+
const names = filterableFields(COLLECTION, ["status"]).map((f) => f.name);
|
|
64
|
+
expect(names).toEqual(["status", "id", "created_at", "updated_at"]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("empty readable_fields means unrestricted (the proto contract)", () => {
|
|
68
|
+
expect(filterableFields(COLLECTION, []).map((f) => f.name)).toEqual(
|
|
69
|
+
filterableFields(COLLECTION).map((f) => f.name),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
59
72
|
});
|
|
60
73
|
|
|
61
74
|
describe("isConditionComplete", () => {
|
|
@@ -66,12 +66,26 @@ export interface FilterableField {
|
|
|
66
66
|
* fields. `id` conditions take string values; the audit timestamps take
|
|
67
67
|
* RFC 3339 timestamps — both ride the `timestamp`/`string` value
|
|
68
68
|
* controls.
|
|
69
|
+
*
|
|
70
|
+
* `readableFields` is the caller's column-level read access from
|
|
71
|
+
* `describeDatastore` (the read verb's `readable_fields`; empty means
|
|
72
|
+
* every field). When restricted, unreadable declared fields are not
|
|
73
|
+
* offered — the server refuses conditions on them (the existence-oracle
|
|
74
|
+
* guard), and the builder must stay structurally unable to express an
|
|
75
|
+
* unservable filter (DD-008 invariant 2). System fields are always
|
|
76
|
+
* readable and always offered.
|
|
69
77
|
*/
|
|
70
|
-
export function filterableFields(
|
|
78
|
+
export function filterableFields(
|
|
79
|
+
collection: CollectionDeclaration,
|
|
80
|
+
readableFields?: readonly string[],
|
|
81
|
+
): FilterableField[] {
|
|
82
|
+
const readable =
|
|
83
|
+
readableFields !== undefined && readableFields.length > 0 ? new Set(readableFields) : null;
|
|
71
84
|
const declared: FilterableField[] = [];
|
|
72
85
|
for (const field of collection.fields) {
|
|
73
86
|
const operators = operatorsForField(field);
|
|
74
87
|
if (operators.length === 0) continue; // json — not filterable
|
|
88
|
+
if (readable !== null && !readable.has(field.name)) continue; // not readable under the grant
|
|
75
89
|
declared.push({ name: field.name, declaration: field, type: field.type, operators });
|
|
76
90
|
}
|
|
77
91
|
return [
|
|
@@ -17,6 +17,8 @@ export interface UseDatastoreCountOptions {
|
|
|
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 useDatastoreCount}. */
|
|
@@ -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 { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
4
5
|
import { cn } from "@stigmer/theme";
|
|
5
6
|
import { ExecutionPhaseBadge } from "./ExecutionPhaseBadge.js";
|
|
6
7
|
import { TodoList } from "./TodoList.js";
|
|
@@ -47,10 +48,19 @@ export function ExecutionProgress({
|
|
|
47
48
|
if (phase === undefined) return null;
|
|
48
49
|
|
|
49
50
|
const todos = execution.status?.todos;
|
|
50
|
-
// Populated by the server
|
|
51
|
-
// badge so this widget explains a failure rather
|
|
52
|
-
//
|
|
53
|
-
|
|
51
|
+
// Populated by the server on EXECUTION_FAILED and EXECUTION_TERMINATED —
|
|
52
|
+
// surface it next to the badge so this widget explains a failure rather
|
|
53
|
+
// than showing a bare phase (consistent with the message thread).
|
|
54
|
+
//
|
|
55
|
+
// CANCELLED is carved out: cancel is a quiet terminal state, not a failure
|
|
56
|
+
// (stigmer#282). A CANCELLED execution can still carry a non-empty error —
|
|
57
|
+
// cancel preserves a preexisting error by design, and an older server may
|
|
58
|
+
// have written a cancellation sentinel — so the phase, not the error field,
|
|
59
|
+
// decides whether to render the alert. The muted Cancelled badge suffices.
|
|
60
|
+
const error =
|
|
61
|
+
phase === ExecutionPhase.EXECUTION_CANCELLED
|
|
62
|
+
? undefined
|
|
63
|
+
: execution.status?.error;
|
|
54
64
|
|
|
55
65
|
return (
|
|
56
66
|
<div
|
|
@@ -941,12 +941,20 @@ export function buildThreadItems(
|
|
|
941
941
|
key: `phase-${lastPhase}`,
|
|
942
942
|
});
|
|
943
943
|
|
|
944
|
-
// The server populates `status.error`
|
|
944
|
+
// The server populates `status.error` on EXECUTION_FAILED and
|
|
945
|
+
// EXECUTION_TERMINATED — never on a user-initiated cancel. Surface it
|
|
945
946
|
// beside the badge so a failure that produced no messages still explains
|
|
946
947
|
// itself (the CLI shows this reason; the chat previously showed nothing).
|
|
947
948
|
// Kept as its own item so the badge component stays presentational.
|
|
949
|
+
//
|
|
950
|
+
// CANCELLED is carved out: cancel is a quiet terminal state, not a failure
|
|
951
|
+
// (stigmer#282). A CANCELLED execution can still carry a non-empty error —
|
|
952
|
+
// cancel preserves a preexisting error by design (merge semantics), and an
|
|
953
|
+
// older server may have written a "Execution cancelled" sentinel — so the
|
|
954
|
+
// phase, not the error field, decides whether to render the loud banner.
|
|
955
|
+
// The muted Cancelled phase badge above remains the visible state.
|
|
948
956
|
const reason = lastExec?.status?.error;
|
|
949
|
-
if (reason) {
|
|
957
|
+
if (reason && lastPhase !== ExecutionPhase.EXECUTION_CANCELLED) {
|
|
950
958
|
const specMessage = lastExec?.spec?.message;
|
|
951
959
|
// A failed build turn offers no inline Retry: resending its label as an
|
|
952
960
|
// ordinary message would drop the buildFromPlan flag (no runner
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import {
|
|
5
|
+
AgentExecutionSchema,
|
|
6
|
+
AgentExecutionStatusSchema,
|
|
7
|
+
type AgentExecution,
|
|
8
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
9
|
+
import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
10
|
+
|
|
11
|
+
import { ExecutionProgress } from "../ExecutionProgress";
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
cleanup();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function makeExecution(phase: ExecutionPhase, error?: string): AgentExecution {
|
|
18
|
+
const exec = create(AgentExecutionSchema);
|
|
19
|
+
const status = create(AgentExecutionStatusSchema);
|
|
20
|
+
status.phase = phase;
|
|
21
|
+
if (error !== undefined) {
|
|
22
|
+
status.error = error;
|
|
23
|
+
}
|
|
24
|
+
exec.status = status;
|
|
25
|
+
return exec;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("ExecutionProgress", () => {
|
|
29
|
+
it("renders nothing without an execution", () => {
|
|
30
|
+
const { container } = render(<ExecutionProgress execution={null} />);
|
|
31
|
+
expect(container.innerHTML).toBe("");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("surfaces the server error as an alert for a FAILED execution", () => {
|
|
35
|
+
render(
|
|
36
|
+
<ExecutionProgress
|
|
37
|
+
execution={makeExecution(
|
|
38
|
+
ExecutionPhase.EXECUTION_FAILED,
|
|
39
|
+
"Activity task timed out",
|
|
40
|
+
)}
|
|
41
|
+
/>,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(screen.getByRole("alert").textContent).toContain(
|
|
45
|
+
"Activity task timed out",
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Cancelled is a quiet terminal state (stigmer#282): a CANCELLED execution
|
|
50
|
+
// can legitimately carry a non-empty status.error (preserved prior error, or
|
|
51
|
+
// a pre-fix server's "Execution cancelled" sentinel), so the phase — not the
|
|
52
|
+
// error field — decides whether the alert renders.
|
|
53
|
+
it("renders a CANCELLED execution quietly even when it carries an error", () => {
|
|
54
|
+
render(
|
|
55
|
+
<ExecutionProgress
|
|
56
|
+
execution={makeExecution(
|
|
57
|
+
ExecutionPhase.EXECUTION_CANCELLED,
|
|
58
|
+
"Execution cancelled",
|
|
59
|
+
)}
|
|
60
|
+
/>,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(screen.getByText(/cancelled/i)).toBeTruthy();
|
|
64
|
+
expect(screen.queryByRole("alert")).toBeNull();
|
|
65
|
+
expect(screen.queryByText("Execution cancelled")).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -604,6 +604,50 @@ describe("MessageThread", () => {
|
|
|
604
604
|
expect(screen.queryByText(/stale error/i)).toBeNull();
|
|
605
605
|
});
|
|
606
606
|
|
|
607
|
+
// Cancelled is a quiet terminal state (stigmer#282): a user-initiated Stop
|
|
608
|
+
// must never render as a failure. A CANCELLED execution can legitimately
|
|
609
|
+
// carry a non-empty status.error — cancel preserves a preexisting error by
|
|
610
|
+
// design, and pre-fix servers wrote an "Execution cancelled" sentinel — so
|
|
611
|
+
// the phase, not the error field, decides whether the loud banner renders.
|
|
612
|
+
it("renders a CANCELLED execution quietly even when it carries a legacy error sentinel", () => {
|
|
613
|
+
const exec = makeExecution({
|
|
614
|
+
id: "exec-cancelled",
|
|
615
|
+
specMessage: "do the thing",
|
|
616
|
+
phase: ExecutionPhase.EXECUTION_CANCELLED,
|
|
617
|
+
error: "Execution cancelled",
|
|
618
|
+
});
|
|
619
|
+
const onRetryExecution = vi.fn();
|
|
620
|
+
|
|
621
|
+
render(
|
|
622
|
+
<MessageThread executions={[exec]} onRetryExecution={onRetryExecution} />,
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
// The muted phase badge is the visible cancelled state.
|
|
626
|
+
expect(screen.getByText(/cancelled/i)).toBeTruthy();
|
|
627
|
+
|
|
628
|
+
// No destructive banner, no error text, no Retry.
|
|
629
|
+
expect(screen.queryByRole("alert")).toBeNull();
|
|
630
|
+
expect(screen.queryByText("Execution cancelled")).toBeNull();
|
|
631
|
+
expect(screen.queryByRole("button", { name: /retry/i })).toBeNull();
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it("renders a CANCELLED execution with a preserved prior error quietly", () => {
|
|
635
|
+
const exec = makeExecution({
|
|
636
|
+
id: "exec-cancelled-preserved",
|
|
637
|
+
phase: ExecutionPhase.EXECUTION_CANCELLED,
|
|
638
|
+
error: "Execution interrupted: agent was unresponsive. Retry or resume.",
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
render(<MessageThread executions={[exec]} />);
|
|
642
|
+
|
|
643
|
+
expect(screen.getByText(/cancelled/i)).toBeTruthy();
|
|
644
|
+
// Neither the loud alert framing nor the neutral interrupted/Resume
|
|
645
|
+
// framing may appear — the preserved error stays out of the transcript.
|
|
646
|
+
expect(screen.queryByText(/Execution interrupted/i)).toBeNull();
|
|
647
|
+
expect(screen.queryByRole("alert")).toBeNull();
|
|
648
|
+
expect(screen.queryByRole("button", { name: /resume/i })).toBeNull();
|
|
649
|
+
});
|
|
650
|
+
|
|
607
651
|
it("offers a Retry that resends the originating message on failure", () => {
|
|
608
652
|
const exec = makeExecution({
|
|
609
653
|
id: "exec-retry",
|