@stigmer/react 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/billing/index.d.ts +0 -18
  2. package/billing/index.d.ts.map +1 -1
  3. package/billing/index.js +4 -9
  4. package/billing/index.js.map +1 -1
  5. package/cursor-accounts/CursorAccountEditor.d.ts +7 -1
  6. package/cursor-accounts/CursorAccountEditor.d.ts.map +1 -1
  7. package/cursor-accounts/CursorAccountEditor.js +21 -5
  8. package/cursor-accounts/CursorAccountEditor.js.map +1 -1
  9. package/cursor-accounts/CursorAccountsConsole.d.ts +9 -5
  10. package/cursor-accounts/CursorAccountsConsole.d.ts.map +1 -1
  11. package/cursor-accounts/CursorAccountsConsole.js +34 -140
  12. package/cursor-accounts/CursorAccountsConsole.js.map +1 -1
  13. package/cursor-accounts/MemberCoverageTable.d.ts +29 -0
  14. package/cursor-accounts/MemberCoverageTable.d.ts.map +1 -0
  15. package/cursor-accounts/MemberCoverageTable.js +150 -0
  16. package/cursor-accounts/MemberCoverageTable.js.map +1 -0
  17. package/cursor-accounts/MemberKeysPanel.d.ts +25 -0
  18. package/cursor-accounts/MemberKeysPanel.d.ts.map +1 -0
  19. package/cursor-accounts/MemberKeysPanel.js +115 -0
  20. package/cursor-accounts/MemberKeysPanel.js.map +1 -0
  21. package/cursor-accounts/badges.d.ts +9 -0
  22. package/cursor-accounts/badges.d.ts.map +1 -0
  23. package/cursor-accounts/badges.js +10 -0
  24. package/cursor-accounts/badges.js.map +1 -0
  25. package/cursor-accounts/cursor-account-coverage.d.ts +41 -0
  26. package/cursor-accounts/cursor-account-coverage.d.ts.map +1 -0
  27. package/cursor-accounts/cursor-account-coverage.js +26 -0
  28. package/cursor-accounts/cursor-account-coverage.js.map +1 -0
  29. package/cursor-accounts/cursor-account-format.d.ts +13 -0
  30. package/cursor-accounts/cursor-account-format.d.ts.map +1 -1
  31. package/cursor-accounts/cursor-account-format.js +16 -1
  32. package/cursor-accounts/cursor-account-format.js.map +1 -1
  33. package/datastore/CollectionRecordsBrowser.js +18 -4
  34. package/datastore/CollectionRecordsBrowser.js.map +1 -1
  35. package/datastore/RecordFilterBuilder.d.ts +8 -1
  36. package/datastore/RecordFilterBuilder.d.ts.map +1 -1
  37. package/datastore/RecordFilterBuilder.js +2 -2
  38. package/datastore/RecordFilterBuilder.js.map +1 -1
  39. package/datastore/recordFilter.d.ts +9 -1
  40. package/datastore/recordFilter.d.ts.map +1 -1
  41. package/datastore/recordFilter.js +12 -1
  42. package/datastore/recordFilter.js.map +1 -1
  43. package/execution/ExecutionProgress.d.ts.map +1 -1
  44. package/execution/ExecutionProgress.js +13 -4
  45. package/execution/ExecutionProgress.js.map +1 -1
  46. package/execution/MessageThread.d.ts.map +1 -1
  47. package/execution/MessageThread.js +10 -2
  48. package/execution/MessageThread.js.map +1 -1
  49. package/execution/useCreateAgentExecution.d.ts +31 -1
  50. package/execution/useCreateAgentExecution.d.ts.map +1 -1
  51. package/execution/useCreateAgentExecution.js +2 -0
  52. package/execution/useCreateAgentExecution.js.map +1 -1
  53. package/index.d.ts +4 -2
  54. package/index.d.ts.map +1 -1
  55. package/index.js +6 -2
  56. package/index.js.map +1 -1
  57. package/internal/form-primitives.d.ts +10 -0
  58. package/internal/form-primitives.d.ts.map +1 -0
  59. package/{billing → internal}/form-primitives.js +5 -9
  60. package/internal/form-primitives.js.map +1 -0
  61. package/package.json +4 -4
  62. package/pricing-governance/BaselineEditor.d.ts.map +1 -0
  63. package/{billing → pricing-governance}/BaselineEditor.js +19 -2
  64. package/pricing-governance/BaselineEditor.js.map +1 -0
  65. package/pricing-governance/ModelCatalogPanel.d.ts.map +1 -0
  66. package/pricing-governance/ModelCatalogPanel.js.map +1 -0
  67. package/pricing-governance/ModelGovernanceDetail.d.ts.map +1 -0
  68. package/pricing-governance/ModelGovernanceDetail.js.map +1 -0
  69. package/pricing-governance/OperatorAccessNotice.d.ts.map +1 -0
  70. package/pricing-governance/OperatorAccessNotice.js.map +1 -0
  71. package/pricing-governance/PricingGovernanceConsole.d.ts.map +1 -0
  72. package/{billing → pricing-governance}/PricingGovernanceConsole.js +1 -1
  73. package/pricing-governance/PricingGovernanceConsole.js.map +1 -0
  74. package/pricing-governance/PricingGovernancePanel.d.ts.map +1 -0
  75. package/pricing-governance/PricingGovernancePanel.js.map +1 -0
  76. package/pricing-governance/RetireConfirm.d.ts.map +1 -0
  77. package/{billing → pricing-governance}/RetireConfirm.js +1 -1
  78. package/pricing-governance/RetireConfirm.js.map +1 -0
  79. package/pricing-governance/governance-primitives.d.ts.map +1 -0
  80. package/pricing-governance/governance-primitives.js.map +1 -0
  81. package/pricing-governance/index.d.ts +19 -0
  82. package/pricing-governance/index.d.ts.map +1 -0
  83. package/pricing-governance/index.js +20 -0
  84. package/pricing-governance/index.js.map +1 -0
  85. package/pricing-governance/pricing-format.d.ts.map +1 -0
  86. package/pricing-governance/pricing-format.js.map +1 -0
  87. package/pricing-governance/useDecidePricingOverride.d.ts.map +1 -0
  88. package/pricing-governance/useDecidePricingOverride.js.map +1 -0
  89. package/pricing-governance/useModelGovernanceView.d.ts.map +1 -0
  90. package/pricing-governance/useModelGovernanceView.js.map +1 -0
  91. package/pricing-governance/useModelPricingBaselines.d.ts.map +1 -0
  92. package/pricing-governance/useModelPricingBaselines.js.map +1 -0
  93. package/pricing-governance/usePricingGovernance.d.ts.map +1 -0
  94. package/pricing-governance/usePricingGovernance.js.map +1 -0
  95. package/pricing-governance/useRetireModelPricingBaseline.d.ts.map +1 -0
  96. package/pricing-governance/useRetireModelPricingBaseline.js.map +1 -0
  97. package/pricing-governance/useUpsertModelPricingBaseline.d.ts.map +1 -0
  98. package/pricing-governance/useUpsertModelPricingBaseline.js.map +1 -0
  99. package/session/useCreateSession.d.ts +28 -0
  100. package/session/useCreateSession.d.ts.map +1 -1
  101. package/session/useCreateSession.js +2 -1
  102. package/session/useCreateSession.js.map +1 -1
  103. package/session/useNewSessionFlow.d.ts +30 -0
  104. package/session/useNewSessionFlow.d.ts.map +1 -1
  105. package/session/useNewSessionFlow.js +7 -1
  106. package/session/useNewSessionFlow.js.map +1 -1
  107. package/session/useUpdateSession.d.ts +29 -10
  108. package/session/useUpdateSession.d.ts.map +1 -1
  109. package/session/useUpdateSession.js +29 -10
  110. package/session/useUpdateSession.js.map +1 -1
  111. package/src/billing/index.ts +5 -37
  112. package/src/cursor-accounts/CursorAccountEditor.tsx +61 -13
  113. package/src/cursor-accounts/CursorAccountsConsole.tsx +44 -400
  114. package/src/cursor-accounts/MemberCoverageTable.tsx +439 -0
  115. package/src/cursor-accounts/MemberKeysPanel.tsx +278 -0
  116. package/src/cursor-accounts/__tests__/CursorAccountsConsole.test.tsx +306 -10
  117. package/src/cursor-accounts/__tests__/cursor-account-coverage.test.ts +73 -0
  118. package/src/cursor-accounts/badges.tsx +26 -0
  119. package/src/cursor-accounts/cursor-account-coverage.ts +71 -0
  120. package/src/cursor-accounts/cursor-account-format.ts +17 -1
  121. package/src/datastore/CollectionRecordsBrowser.tsx +29 -3
  122. package/src/datastore/RecordFilterBuilder.tsx +12 -1
  123. package/src/datastore/__tests__/CollectionRecordsBrowser.test.tsx +38 -0
  124. package/src/datastore/__tests__/RecordFilterBuilder.test.tsx +14 -0
  125. package/src/datastore/__tests__/recordFilter.test.ts +13 -0
  126. package/src/datastore/recordFilter.ts +15 -1
  127. package/src/execution/ExecutionProgress.tsx +14 -4
  128. package/src/execution/MessageThread.tsx +10 -2
  129. package/src/execution/__tests__/ExecutionProgress.test.tsx +67 -0
  130. package/src/execution/__tests__/MessageThread.test.tsx +44 -0
  131. package/src/execution/__tests__/useCreateAgentExecution.test.tsx +86 -0
  132. package/src/execution/useCreateAgentExecution.ts +41 -6
  133. package/src/index.ts +32 -24
  134. package/src/{billing → internal}/form-primitives.tsx +4 -32
  135. package/src/{billing → pricing-governance}/BaselineEditor.tsx +36 -3
  136. package/src/{billing → pricing-governance}/PricingGovernanceConsole.tsx +1 -1
  137. package/src/{billing → pricing-governance}/RetireConfirm.tsx +1 -1
  138. package/src/pricing-governance/index.ts +50 -0
  139. package/src/session/__tests__/useCreateSession.test.tsx +94 -0
  140. package/src/session/__tests__/useNewSessionFlow.test.tsx +33 -0
  141. package/src/session/useCreateSession.ts +30 -0
  142. package/src/session/useNewSessionFlow.ts +45 -1
  143. package/src/session/useUpdateSession.ts +29 -10
  144. package/styles.css +1 -1
  145. package/billing/BaselineEditor.d.ts.map +0 -1
  146. package/billing/BaselineEditor.js.map +0 -1
  147. package/billing/ModelCatalogPanel.d.ts.map +0 -1
  148. package/billing/ModelCatalogPanel.js.map +0 -1
  149. package/billing/ModelGovernanceDetail.d.ts.map +0 -1
  150. package/billing/ModelGovernanceDetail.js.map +0 -1
  151. package/billing/OperatorAccessNotice.d.ts.map +0 -1
  152. package/billing/OperatorAccessNotice.js.map +0 -1
  153. package/billing/PricingGovernanceConsole.d.ts.map +0 -1
  154. package/billing/PricingGovernanceConsole.js.map +0 -1
  155. package/billing/PricingGovernancePanel.d.ts.map +0 -1
  156. package/billing/PricingGovernancePanel.js.map +0 -1
  157. package/billing/RetireConfirm.d.ts.map +0 -1
  158. package/billing/RetireConfirm.js.map +0 -1
  159. package/billing/form-primitives.d.ts +0 -18
  160. package/billing/form-primitives.d.ts.map +0 -1
  161. package/billing/form-primitives.js.map +0 -1
  162. package/billing/governance-primitives.d.ts.map +0 -1
  163. package/billing/governance-primitives.js.map +0 -1
  164. package/billing/pricing-format.d.ts.map +0 -1
  165. package/billing/pricing-format.js.map +0 -1
  166. package/billing/useDecidePricingOverride.d.ts.map +0 -1
  167. package/billing/useDecidePricingOverride.js.map +0 -1
  168. package/billing/useModelGovernanceView.d.ts.map +0 -1
  169. package/billing/useModelGovernanceView.js.map +0 -1
  170. package/billing/useModelPricingBaselines.d.ts.map +0 -1
  171. package/billing/useModelPricingBaselines.js.map +0 -1
  172. package/billing/usePricingGovernance.d.ts.map +0 -1
  173. package/billing/usePricingGovernance.js.map +0 -1
  174. package/billing/useRetireModelPricingBaseline.d.ts.map +0 -1
  175. package/billing/useRetireModelPricingBaseline.js.map +0 -1
  176. package/billing/useUpsertModelPricingBaseline.d.ts.map +0 -1
  177. package/billing/useUpsertModelPricingBaseline.js.map +0 -1
  178. /package/{billing → pricing-governance}/BaselineEditor.d.ts +0 -0
  179. /package/{billing → pricing-governance}/ModelCatalogPanel.d.ts +0 -0
  180. /package/{billing → pricing-governance}/ModelCatalogPanel.js +0 -0
  181. /package/{billing → pricing-governance}/ModelGovernanceDetail.d.ts +0 -0
  182. /package/{billing → pricing-governance}/ModelGovernanceDetail.js +0 -0
  183. /package/{billing → pricing-governance}/OperatorAccessNotice.d.ts +0 -0
  184. /package/{billing → pricing-governance}/OperatorAccessNotice.js +0 -0
  185. /package/{billing → pricing-governance}/PricingGovernanceConsole.d.ts +0 -0
  186. /package/{billing → pricing-governance}/PricingGovernancePanel.d.ts +0 -0
  187. /package/{billing → pricing-governance}/PricingGovernancePanel.js +0 -0
  188. /package/{billing → pricing-governance}/RetireConfirm.d.ts +0 -0
  189. /package/{billing → pricing-governance}/governance-primitives.d.ts +0 -0
  190. /package/{billing → pricing-governance}/governance-primitives.js +0 -0
  191. /package/{billing → pricing-governance}/pricing-format.d.ts +0 -0
  192. /package/{billing → pricing-governance}/pricing-format.js +0 -0
  193. /package/{billing → pricing-governance}/useDecidePricingOverride.d.ts +0 -0
  194. /package/{billing → pricing-governance}/useDecidePricingOverride.js +0 -0
  195. /package/{billing → pricing-governance}/useModelGovernanceView.d.ts +0 -0
  196. /package/{billing → pricing-governance}/useModelGovernanceView.js +0 -0
  197. /package/{billing → pricing-governance}/useModelPricingBaselines.d.ts +0 -0
  198. /package/{billing → pricing-governance}/useModelPricingBaselines.js +0 -0
  199. /package/{billing → pricing-governance}/usePricingGovernance.d.ts +0 -0
  200. /package/{billing → pricing-governance}/usePricingGovernance.js +0 -0
  201. /package/{billing → pricing-governance}/useRetireModelPricingBaseline.d.ts +0 -0
  202. /package/{billing → pricing-governance}/useRetireModelPricingBaseline.js +0 -0
  203. /package/{billing → pricing-governance}/useUpsertModelPricingBaseline.d.ts +0 -0
  204. /package/{billing → pricing-governance}/useUpsertModelPricingBaseline.js +0 -0
  205. /package/src/{billing → pricing-governance}/ModelCatalogPanel.tsx +0 -0
  206. /package/src/{billing → pricing-governance}/ModelGovernanceDetail.tsx +0 -0
  207. /package/src/{billing → pricing-governance}/OperatorAccessNotice.tsx +0 -0
  208. /package/src/{billing → pricing-governance}/PricingGovernancePanel.tsx +0 -0
  209. /package/src/{billing → pricing-governance}/__tests__/ModelCatalogPanel.test.tsx +0 -0
  210. /package/src/{billing → pricing-governance}/__tests__/PricingGovernanceConsole.test.tsx +0 -0
  211. /package/src/{billing → pricing-governance}/__tests__/useModelGovernanceView.test.tsx +0 -0
  212. /package/src/{billing → pricing-governance}/governance-primitives.tsx +0 -0
  213. /package/src/{billing → pricing-governance}/pricing-format.ts +0 -0
  214. /package/src/{billing → pricing-governance}/useDecidePricingOverride.ts +0 -0
  215. /package/src/{billing → pricing-governance}/useModelGovernanceView.ts +0 -0
  216. /package/src/{billing → pricing-governance}/useModelPricingBaselines.ts +0 -0
  217. /package/src/{billing → pricing-governance}/usePricingGovernance.ts +0 -0
  218. /package/src/{billing → pricing-governance}/useRetireModelPricingBaseline.ts +0 -0
  219. /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
- // billing pricing-format precedent).
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(() => buildRecordColumns(collection), [collection]);
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(collection: CollectionDeclaration): RecordColumnDef[] {
440
- const fieldColumns: RecordColumnDef[] = collection.fields.map((field) => ({
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(() => filterableFields(collection), [collection]);
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(collection: CollectionDeclaration): FilterableField[] {
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 [
@@ -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 only on EXECUTION_FAILED surface it next to the
51
- // badge so this widget explains a failure rather than showing a bare phase
52
- // (consistent with the failure reason in the message thread).
53
- const error = execution.status?.error;
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` only on EXECUTION_FAILED. Surface it
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",