@k2b/cloud 0.31.0 → 0.33.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 (173) hide show
  1. package/package.json +8 -6
  2. package/src/_internal/define-app.ts +20 -9
  3. package/src/_internal/page-responses.ts +5 -1
  4. package/src/ai/app-skills.ts +1 -7
  5. package/src/ai/audio-tool.ts +2 -1
  6. package/src/ai/browser.ts +109 -0
  7. package/src/ai/capabilities.ts +6 -2
  8. package/src/ai/chart-block.ts +431 -81
  9. package/src/ai/chat/action-sentences.ts +13 -7
  10. package/src/ai/chat/blocks.tsx +11 -7
  11. package/src/ai/chat/builtin-tools.tsx +9 -7
  12. package/src/ai/chat/chart-block.tsx +218 -44
  13. package/src/ai/chat/explorer-block.tsx +276 -0
  14. package/src/ai/chat/file-tools.tsx +40 -1
  15. package/src/ai/chat/message-actions.tsx +6 -0
  16. package/src/ai/chat/message-utils.ts +7 -0
  17. package/src/ai/chat/messages.ts +16 -0
  18. package/src/ai/chat/presentation.tsx +13 -3
  19. package/src/ai/chat/reference-text.tsx +34 -0
  20. package/src/ai/chat/tool-groups.ts +1 -0
  21. package/src/ai/chat/tool-names.ts +14 -0
  22. package/src/ai/chat/turn-view.tsx +17 -7
  23. package/src/ai/chat/view-block.browser-harness.tsx +35 -0
  24. package/src/ai/chat/view-block.tsx +417 -0
  25. package/src/ai/chat/visual-tools.tsx +32 -53
  26. package/src/ai/client-tool-results.ts +9 -0
  27. package/src/ai/code-source-contracts.ts +17 -2
  28. package/src/ai/code-source-tools.ts +1 -0
  29. package/src/ai/default-tool-contracts.ts +334 -91
  30. package/src/ai/default-tools.ts +89 -13
  31. package/src/ai/executor.ts +173 -31
  32. package/src/ai/file-error.ts +2 -0
  33. package/src/ai/file-tools.ts +27 -7
  34. package/src/ai/files-store.ts +60 -0
  35. package/src/ai/html-pdf-tool.ts +3 -2
  36. package/src/ai/http.ts +37 -12
  37. package/src/ai/index.ts +90 -0
  38. package/src/ai/local-tool-contracts.ts +244 -0
  39. package/src/ai/malformed-tool-call-repair.ts +144 -0
  40. package/src/ai/markdown-pdf-tool.ts +2 -1
  41. package/src/ai/memories.ts +48 -1
  42. package/src/ai/memory-learning.ts +17 -6
  43. package/src/ai/memory-tool.ts +1 -0
  44. package/src/ai/migrate.ts +15 -0
  45. package/src/ai/openai-api-messages.ts +53 -0
  46. package/src/ai/openai-api-slots.ts +40 -0
  47. package/src/ai/openai-api.ts +383 -0
  48. package/src/ai/protocol.ts +6 -2
  49. package/src/ai/provider.ts +17 -4
  50. package/src/ai/request-context.ts +33 -0
  51. package/src/ai/routes.ts +96 -57
  52. package/src/ai/runtime.ts +48 -12
  53. package/src/ai/settings.ts +5 -0
  54. package/src/ai/skill-catalog.ts +20 -5
  55. package/src/ai/skill-format.ts +17 -7
  56. package/src/ai/skill-tool.ts +15 -5
  57. package/src/ai/skills-routes.ts +20 -6
  58. package/src/ai/skills.ts +26 -7
  59. package/src/ai/store.ts +25 -0
  60. package/src/ai/system-prompt.ts +69 -10
  61. package/src/ai/timeline.ts +3 -1
  62. package/src/ai/tools.ts +3 -0
  63. package/src/ai/types.ts +9 -0
  64. package/src/ai/view-block.ts +941 -0
  65. package/src/ai/vision-tool.ts +2 -1
  66. package/src/api/admin-identity.ts +6 -1
  67. package/src/api/app-approval-actions.ts +36 -0
  68. package/src/api/app-approval.ts +10 -2
  69. package/src/api/approvals-loader.ts +15 -0
  70. package/src/api/approvals.ts +120 -0
  71. package/src/api/index.ts +3 -0
  72. package/src/api/me.ts +12 -3
  73. package/src/approvals/Approvals.browser-harness.tsx +16 -0
  74. package/src/approvals/Approvals.tsx +621 -0
  75. package/src/approvals/ApprovalsNavStatus.tsx +20 -0
  76. package/src/approvals/messages.ts +95 -0
  77. package/src/approvals/ui.ts +8 -0
  78. package/src/browser/app-approval.ts +32 -0
  79. package/src/browser/choose-files.tsx +26 -13
  80. package/src/browser/commands.ts +13 -4
  81. package/src/browser/file-provider-error.ts +11 -0
  82. package/src/browser/file-providers.ts +3 -12
  83. package/src/browser/files.tsx +1 -2
  84. package/src/browser/live-connection.ts +2 -3
  85. package/src/browser/navigation-search.ts +4 -1
  86. package/src/browser/provider-list.ts +2 -1
  87. package/src/browser/save-files.tsx +13 -2
  88. package/src/cli/account.ts +21 -2
  89. package/src/cli/commands.ts +4 -2
  90. package/src/contracts/app-approval.ts +120 -0
  91. package/src/contracts/approval-paths.ts +4 -0
  92. package/src/contracts/approvals.ts +81 -0
  93. package/src/contracts/command-path.ts +17 -0
  94. package/src/contracts/commands.ts +5 -15
  95. package/src/contracts/shared.ts +5 -0
  96. package/src/events/live-protocol.ts +2 -1
  97. package/src/events/live-server-message.ts +40 -0
  98. package/src/events/testing.ts +88 -0
  99. package/src/server/index.ts +2 -0
  100. package/src/server/middleware/auth.ts +11 -1
  101. package/src/server/request-timeout.ts +22 -0
  102. package/src/server/services/access.ts +4 -3
  103. package/src/services/app-approval.ts +103 -15
  104. package/src/services/approvals/cursor.ts +15 -0
  105. package/src/services/approvals/device.ts +112 -0
  106. package/src/services/approvals/index.ts +27 -11
  107. package/src/services/approvals/list.ts +120 -0
  108. package/src/services/approvals/migrate.ts +9 -1
  109. package/src/services/identity/models-only-auth.ts +25 -0
  110. package/src/services/notifications/email-frame.ts +16 -3
  111. package/src/services/outbox.ts +13 -6
  112. package/src/services/service-account-credentials.ts +22 -10
  113. package/src/services/settings/core-settings.ts +27 -0
  114. package/src/shared/ai-platform-prompt.ts +29 -3
  115. package/src/shared/email-html.ts +4 -0
  116. package/src/shared/index.ts +6 -1
  117. package/src/shared/markdown/extensions/code.ts +14 -0
  118. package/src/shared/markdown/index.ts +4 -4
  119. package/src/shared/theme.ts +41 -20
  120. package/src/ssr/AnnouncementBanners.tsx +30 -0
  121. package/src/ssr/AppBadge.tsx +4 -4
  122. package/src/ssr/AppLaunchpadPanel.tsx +6 -6
  123. package/src/ssr/CloudShell.island.tsx +91 -0
  124. package/src/ssr/GlobalSearchTrigger.tsx +51 -0
  125. package/src/ssr/Layout.tsx +56 -85
  126. package/src/ssr/LayoutHeader.tsx +31 -77
  127. package/src/ssr/LayoutHelp.tsx +2 -57
  128. package/src/ssr/LayoutHelpTrigger.tsx +51 -0
  129. package/src/ssr/{LayoutPreferences.island.tsx → LayoutPreferences.tsx} +2 -2
  130. package/src/ssr/LayoutRail.tsx +15 -69
  131. package/src/ssr/MinimalLayout.tsx +2 -3
  132. package/src/ssr/MinimalLayoutPreferences.island.tsx +12 -15
  133. package/src/ssr/MobileNavigation.tsx +13 -8
  134. package/src/ssr/MobileProfileActions.tsx +10 -7
  135. package/src/ssr/PageTrailRow.tsx +14 -0
  136. package/src/ssr/{ProfilePreferences.island.tsx → ProfilePreferences.tsx} +12 -7
  137. package/src/ssr/PwaLayout.tsx +0 -2
  138. package/src/ssr/PwaRuntime.island.tsx +1 -1
  139. package/src/ssr/{RailApps.island.tsx → RailApps.tsx} +2 -2
  140. package/src/ssr/ShellRuntime.island.tsx +170 -0
  141. package/src/ssr/TimezoneCookie.island.tsx +3 -19
  142. package/src/ssr/WorkspaceNavigationProvider.tsx +20 -1
  143. package/src/ssr/announcements-dialog.tsx +66 -0
  144. package/src/ssr/app-badges.ts +50 -6
  145. package/src/ssr/{AppLaunchpad.island.tsx → app-launchpad.tsx} +42 -62
  146. package/src/ssr/busy-action.ts +19 -0
  147. package/src/ssr/islands/index.ts +9 -6
  148. package/src/ssr/launchpad-surface.tsx +15 -0
  149. package/src/ssr/layout-help-registry.tsx +60 -0
  150. package/src/ssr/mobile-navigation.browser-harness.tsx +20 -0
  151. package/src/ssr/page-trail.browser-harness.tsx +52 -3
  152. package/src/ssr/preference-controller.ts +30 -21
  153. package/src/ssr/profile-actions.ts +26 -15
  154. package/src/ssr/profile-preferences-messages.ts +8 -4
  155. package/src/ssr/rail-context.ts +8 -12
  156. package/src/ssr/rail-navigation.ts +5 -3
  157. package/src/ssr/runtime.ts +6 -1
  158. package/src/ssr/shell-data.tsx +48 -0
  159. package/src/ssr/timezone-cookie.ts +22 -0
  160. package/src/ssr/workspace-navigation.ts +25 -2
  161. package/src/styles/approvals.css +390 -0
  162. package/src/styles/global.css +22 -7
  163. package/src/styles/utilities-navigation.css +13 -0
  164. package/src/workflows/language/compiler.ts +1 -1
  165. package/src/ssr/AppWorkspaceController.island.tsx +0 -21
  166. package/src/ssr/BrowserPushRegistration.island.tsx +0 -12
  167. package/src/ssr/GlobalAnnouncements.island.tsx +0 -117
  168. package/src/ssr/GlobalSearchTrigger.island.tsx +0 -75
  169. package/src/ssr/LayoutHelpTrigger.island.tsx +0 -61
  170. package/src/ssr/PageTrailRow.island.tsx +0 -20
  171. package/src/ssr/PageTrailRuntime.island.tsx +0 -26
  172. package/src/ssr/RegisteredHelpDocuments.island.tsx +0 -3
  173. /package/src/ssr/{LayoutBreadcrumbs.island.tsx → LayoutBreadcrumbs.tsx} +0 -0
@@ -1,13 +1,13 @@
1
1
  import { z } from "zod";
2
- import { chartDataIssue, chartDateTime, chartDrawingIssue } from "./chart-block";
2
+ import { chartDataIssue, chartDateTime, chartDrawingIssue, chartSteppingIssue, cloudAiChartMarks } from "./chart-block";
3
3
 
4
4
  const ToneSchema = z.enum(["neutral", "blue", "teal", "green", "amber", "red"]);
5
5
 
6
6
  export const CloudAiCardInputSchema = z.object({
7
7
  title: z.string().min(1),
8
- value: z.string().min(1),
8
+ value: z.string().min(1).describe("The figure or status itself, short enough to read at a glance: about 20 characters."),
9
9
  emoji: z.string().max(8).optional().describe("Optional single emoji shown next to the card content. Omit when none fits."),
10
- caption: z.string().optional(),
10
+ caption: z.string().optional().describe("Optional scope or time range of the value in one short line."),
11
11
  tone: ToneSchema.default("teal"),
12
12
  trendLabel: z.string().min(1).optional(),
13
13
  trendValue: z.string().min(1).optional(),
@@ -20,193 +20,434 @@ export const CLOUD_AI_CHART_MAX_SERIES = 8;
20
20
  /** Charts draw on a 480-unit wide plane: more values per list than units cannot be told apart. */
21
21
  export const CLOUD_AI_CHART_MAX_VALUES = 480;
22
22
 
23
- // Every chart schema is built by a function whose call is marked pure. Bundles that only use other exports of this
24
- // module, such as every Studio app frame through `@k2b/cloud/ai/browser`, can then leave the chart contract out.
25
- const chartParts = () => {
26
- const ChartLabel = z.string().trim().min(1).max(80);
23
+ /** A view, including a group of charts, never draws more marks than the largest single chart. */
24
+ export const CLOUD_AI_VIEW_MAX_MARKS = CLOUD_AI_CHART_MAX_SERIES * CLOUD_AI_CHART_MAX_VALUES;
25
+ /**
26
+ * Every chart of a group mounts its own frame, axes and controls: about 30 ms each on a phone (4x CPU throttling)
27
+ * even with a dozen points. Six stay within the 200 ms in which a response still feels immediate; eight took 420 ms.
28
+ */
29
+ export const CLOUD_AI_GROUP_MAX_CHARTS = 6;
30
+
31
+ // Every chart schema is built by a function whose call is marked pure, so a bundle that imports neither chart contract
32
+ // leaves them out. App frames include the app contract through `cloud.chart()`.
33
+ export const cloudAiNumberFormatSchema = () =>
34
+ z
35
+ .strictObject({
36
+ style: z.enum(["number", "percent", "currency"]).optional(),
37
+ currency: z
38
+ .string()
39
+ .regex(/^[A-Z]{3}$/)
40
+ .optional(),
41
+ // Units fit after a value in the shared renderer, like the existing gauge unit.
42
+ unit: z.string().trim().min(1).max(12).optional(),
43
+ // Intl.NumberFormat supports this precision; table formats already use 0..10.
44
+ decimals: z.number().int().min(0).max(10).optional(),
45
+ compact: z.boolean().optional(),
46
+ })
47
+ .superRefine((value, ctx) => {
48
+ if ((value.style === "currency") !== (value.currency !== undefined))
49
+ ctx.addIssue({ code: "custom", path: ["currency"], message: "Currency is required exactly when style is currency." });
50
+ });
51
+ const dateFormatSchema = () => z.strictObject({ date: z.enum(["day", "minute", "second"]) });
52
+ export const cloudAiFormatSchema = () => z.union([cloudAiNumberFormatSchema(), dateFormatSchema()]);
53
+ export type CloudAiNumberFormat = z.output<ReturnType<typeof cloudAiNumberFormatSchema>>;
54
+ export type CloudAiDateFormat = z.output<ReturnType<typeof dateFormatSchema>>;
55
+ export type CloudAiFormat = CloudAiNumberFormat | CloudAiDateFormat;
56
+
57
+ export const cloudAiChartKindSchema = () =>
58
+ z.enum(["bar", "line", "scatter", "pie", "donut", "histogram", "gauge", "sparkline", "boxplot", "heatmap", "barGauge", "stat"]);
59
+
60
+ const chartParts = (limits = true) => {
61
+ const ChartLabel = limits ? z.string().trim().min(1).max(80) : z.union([z.string(), z.number().transform(String)]);
62
+ // Missing observations stay numeric so the renderer can omit them with its finite-value checks.
63
+ const ChartNumber = limits
64
+ ? z.number()
65
+ : z
66
+ .custom<number>((value) => typeof value === "number", "Expected a number, null or undefined.")
67
+ .nullable()
68
+ .optional()
69
+ .transform((value) => value ?? Number.NaN);
70
+ const list = <T extends z.ZodType>(item: T, min: number, max?: number, message?: string) => {
71
+ const array = z.array(item);
72
+ const bounded = limits && min > 0 ? array.min(min) : array;
73
+ return limits && max !== undefined ? bounded.max(max, message) : bounded;
74
+ };
27
75
  const ChartDate = z
28
76
  .string()
29
77
  .regex(/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2})?)?$/)
30
78
  .refine((value) => chartDateTime(value) !== undefined, "Not a valid date.");
31
79
  const ChartPoint = z.object({
32
- x: z
33
- .union([z.number(), ChartDate])
34
- .describe(
35
- "A number, a date YYYY-MM-DD, or a date and time YYYY-MM-DDTHH:mm or YYYY-MM-DDTHH:mm:ss in the user's time zone without an offset.",
36
- ),
37
- y: z.number(),
80
+ x: z.union([ChartNumber, ChartDate]).describe("line, scatter: number or YYYY-MM-DD[THH:mm[:ss]], wall-clock time without an offset."),
81
+ y: ChartNumber.describe("line, scatter: value."),
38
82
  });
83
+ // As many lines as series a reader can tell apart.
84
+ const ChartReferences = list(
85
+ z.object({ value: z.union([z.number(), ChartDate]), axis: z.enum(["x", "y"]).optional(), label: ChartLabel.optional() }),
86
+ 0,
87
+ CLOUD_AI_CHART_MAX_SERIES,
88
+ `Use at most ${CLOUD_AI_CHART_MAX_SERIES} references.`,
89
+ );
90
+ // One palette color per band.
91
+ const ChartThresholds = list(
92
+ z.object({ value: z.number(), label: ChartLabel.optional() }),
93
+ 1,
94
+ CLOUD_AI_CHART_MAX_SERIES,
95
+ `Use at most ${CLOUD_AI_CHART_MAX_SERIES} thresholds.`,
96
+ );
39
97
  return {
40
98
  ChartLabel,
99
+ ChartNumber,
100
+ list,
101
+ ChartReferences,
102
+ ChartThresholds,
41
103
  ChartAxis: z.object({
42
104
  label: ChartLabel.optional(),
43
- domain: z.array(z.number()).length(2).optional().describe("Exact [min, max]; must contain every value."),
44
- ticks: z.number().int().min(2).max(12).optional(),
105
+ domain: z.array(z.number()).length(2).optional().describe("Exact [min, max]; must contain every value and reference."),
106
+ ticks: (limits ? z.number().int().min(2).max(12) : z.number().int().min(2)).optional(),
45
107
  scale: z.enum(["linear", "log"]).optional().describe("log needs values above zero."),
108
+ format: cloudAiFormatSchema().optional(),
46
109
  }),
47
- ChartCategory: z.object({ label: ChartLabel, value: z.number() }),
48
- ChartSlice: z.object({ label: ChartLabel, value: z.number().min(0) }),
49
- ChartSeries: (points: z.ZodArray<typeof ChartPoint>) => z.object({ label: ChartLabel.optional(), data: points }),
50
- ChartPoints: z.array(ChartPoint).max(CLOUD_AI_CHART_MAX_VALUES),
110
+ ChartCategory: z.object({ label: ChartLabel, value: ChartNumber }),
111
+ ChartSlice: z.object({ label: ChartLabel, value: limits ? z.number().min(0) : ChartNumber }),
112
+ ChartSeries: (points: z.ZodArray<typeof ChartPoint>) =>
113
+ z.object({
114
+ label: ChartLabel.optional(),
115
+ data: points,
116
+ role: z.literal("reference").optional(),
117
+ }),
118
+ ChartPoints: list(ChartPoint, 0, CLOUD_AI_CHART_MAX_VALUES),
51
119
  ChartBins: z
52
120
  .union([
53
- z.number().int().min(1).max(CLOUD_AI_CHART_MAX_VALUES),
54
- z
55
- .array(z.number())
56
- .min(2)
57
- .max(CLOUD_AI_CHART_MAX_VALUES + 1),
121
+ limits ? z.number().int().min(1).max(CLOUD_AI_CHART_MAX_VALUES) : z.number().int().min(1),
122
+ limits
123
+ ? z
124
+ .array(z.number())
125
+ .min(2)
126
+ .max(CLOUD_AI_CHART_MAX_VALUES + 1)
127
+ : z.array(z.number()).min(2),
58
128
  ])
59
- .describe("A bin count or ascending bin edges."),
129
+ .describe("histogram: a bin count or ascending bin edges."),
60
130
  ChartCommon: {
61
- title: z.string().trim().min(1).max(160),
62
- subtitle: z.string().trim().min(1).max(300).optional().describe("Unit, period or source, shown under the title."),
131
+ title: (limits ? z.string().trim().min(1).max(160) : z.string()).describe("All kinds and groups: chart title."),
132
+ subtitle: (limits ? z.string().trim().min(1).max(300) : z.string())
133
+ .optional()
134
+ .describe("All kinds and groups: unit, period or source."),
63
135
  },
136
+ ChartUnit: limits ? z.string().trim().min(1).max(12) : z.string(),
137
+ ChartBox: z.object({ label: ChartLabel, values: list(ChartNumber, 1) }),
138
+ ChartCell: z.object({ x: ChartLabel, y: ChartLabel, value: ChartNumber }),
139
+ ChartBarGauge: z.object({ label: ChartLabel, value: ChartNumber, min: z.number().optional(), max: z.number().optional() }),
64
140
  };
65
141
  };
66
142
 
67
- const chartKinds = () => {
68
- const { ChartAxis, ChartBins, ChartCategory, ChartCommon, ChartLabel, ChartPoints, ChartSeries, ChartSlice } = chartParts();
143
+ const chartKinds = (limits = true) => {
144
+ const {
145
+ ChartNumber,
146
+ list,
147
+ ChartAxis,
148
+ ChartBins,
149
+ ChartCategory,
150
+ ChartCommon,
151
+ ChartLabel,
152
+ ChartPoints,
153
+ ChartSeries,
154
+ ChartSlice,
155
+ ChartReferences,
156
+ ChartThresholds,
157
+ ChartUnit,
158
+ ChartBox,
159
+ ChartCell,
160
+ ChartBarGauge,
161
+ } = chartParts(limits);
162
+ const numberFormat = cloudAiNumberFormatSchema();
69
163
  return z.discriminatedUnion("kind", [
70
164
  z.object({
71
165
  kind: z.literal("bar"),
72
166
  ...ChartCommon,
73
- data: z.array(ChartCategory).min(1).max(CLOUD_AI_CHART_MAX_VALUES),
167
+ data: list(ChartCategory, 1, CLOUD_AI_CHART_MAX_VALUES),
74
168
  yAxis: ChartAxis.optional(),
75
169
  colorByBar: z.boolean().optional(),
76
170
  showValues: z.boolean().optional(),
77
171
  legend: z.boolean().optional(),
172
+ references: ChartReferences.optional(),
78
173
  }),
79
174
  z.object({
80
175
  kind: z.literal("line"),
81
176
  ...ChartCommon,
82
- series: z
83
- .array(
84
- ChartSeries(ChartPoints.min(2, "A line needs at least two points per series; show single values as a bar or scatter chart.")),
85
- )
86
- .min(1)
87
- .max(CLOUD_AI_CHART_MAX_SERIES),
177
+ series: list(
178
+ ChartSeries(
179
+ limits
180
+ ? ChartPoints.min(2, "A line needs at least two points per series; show single values as a bar or scatter chart.")
181
+ : ChartPoints,
182
+ ).extend({ style: z.enum(["solid", "dashed", "dotted"]).optional() }),
183
+ 1,
184
+ CLOUD_AI_CHART_MAX_SERIES,
185
+ ),
88
186
  xAxis: ChartAxis.optional(),
89
187
  yAxis: ChartAxis.optional(),
90
188
  legend: z.boolean().optional(),
91
189
  area: z.boolean().optional(),
92
190
  smooth: z.boolean().optional(),
191
+ references: ChartReferences.optional(),
93
192
  }),
94
193
  z.object({
95
194
  kind: z.literal("scatter"),
96
195
  ...ChartCommon,
97
- series: z
98
- .array(ChartSeries(ChartPoints.min(1)))
99
- .min(1)
100
- .max(CLOUD_AI_CHART_MAX_SERIES),
196
+ series: list(ChartSeries(limits ? ChartPoints.min(1) : ChartPoints), 1, CLOUD_AI_CHART_MAX_SERIES),
101
197
  xAxis: ChartAxis.optional(),
102
198
  yAxis: ChartAxis.optional(),
103
199
  legend: z.boolean().optional(),
200
+ references: ChartReferences.optional(),
104
201
  }),
105
202
  z.object({
106
203
  kind: z.enum(["pie", "donut"]),
107
204
  ...ChartCommon,
108
- data: z.array(ChartSlice).min(1).max(CLOUD_AI_CHART_MAX_VALUES),
205
+ data: list(ChartSlice, 1, CLOUD_AI_CHART_MAX_VALUES),
109
206
  legend: z.boolean().optional(),
110
207
  showLabels: z.boolean().optional(),
208
+ format: numberFormat.optional(),
111
209
  }),
112
210
  z.object({
113
211
  kind: z.literal("histogram"),
114
212
  ...ChartCommon,
115
- data: z.array(z.number()).min(1).max(CLOUD_AI_CHART_MAX_VALUES),
213
+ data: list(ChartNumber, 1, CLOUD_AI_CHART_MAX_VALUES),
116
214
  bins: ChartBins.optional(),
215
+ references: ChartReferences.optional(),
117
216
  }),
118
217
  z.object({
119
218
  kind: z.literal("gauge"),
120
219
  ...ChartCommon,
121
- value: z.number(),
220
+ value: ChartNumber,
122
221
  min: z.number().optional(),
123
222
  max: z.number().optional(),
124
223
  label: ChartLabel.optional(),
125
- unit: z.string().trim().min(1).max(12).optional(),
224
+ unit: ChartUnit.optional(),
225
+ format: numberFormat.optional(),
226
+ thresholds: ChartThresholds.optional(),
126
227
  }),
127
228
  z.object({
128
229
  kind: z.literal("sparkline"),
129
230
  ...ChartCommon,
130
- data: z.array(z.number()).min(2).max(CLOUD_AI_CHART_MAX_VALUES),
231
+ data: list(ChartNumber, 2, CLOUD_AI_CHART_MAX_VALUES),
131
232
  area: z.boolean().optional(),
132
233
  }),
234
+ z.object({
235
+ kind: z.literal("boxplot"),
236
+ ...ChartCommon,
237
+ data: list(ChartBox, 1, CLOUD_AI_CHART_MAX_VALUES),
238
+ yAxis: ChartAxis.optional(),
239
+ references: ChartReferences.optional(),
240
+ }),
241
+ z.object({
242
+ kind: z.literal("heatmap"),
243
+ ...ChartCommon,
244
+ data: list(ChartCell, 1, CLOUD_AI_VIEW_MAX_MARKS),
245
+ format: numberFormat.optional(),
246
+ showValues: z.boolean().optional(),
247
+ min: z.number().optional(),
248
+ max: z.number().optional(),
249
+ }),
250
+ z.object({
251
+ kind: z.literal("barGauge"),
252
+ ...ChartCommon,
253
+ // The shared renderer draws 280 units high and a bar gauge row is 28 units, so 8 rows fit without a taller drawing.
254
+ data: list(ChartBarGauge, 1, CLOUD_AI_CHART_MAX_SERIES, `Use at most ${CLOUD_AI_CHART_MAX_SERIES} bar gauge items.`),
255
+ min: z.number().optional(),
256
+ max: z.number().optional(),
257
+ unit: ChartUnit.optional(),
258
+ format: numberFormat.optional(),
259
+ thresholds: ChartThresholds.optional(),
260
+ }),
261
+ z.object({
262
+ kind: z.literal("stat"),
263
+ ...ChartCommon,
264
+ value: ChartNumber,
265
+ format: numberFormat.optional(),
266
+ unit: ChartUnit.optional(),
267
+ delta: limits ? z.number().optional() : ChartNumber,
268
+ better: z.enum(["up", "down"]).optional(),
269
+ sparkline: list(ChartNumber, 2, CLOUD_AI_CHART_MAX_VALUES).optional(),
270
+ }),
133
271
  ]);
134
272
  };
135
273
 
136
274
  /** Checks run on complete input only, so out-of-range input fails on its bounds before anything is computed or drawn. */
137
275
  const whenValid = { when: (payload: { issues: readonly unknown[] }) => payload.issues.length === 0 };
138
276
 
139
- const chartSchema = () =>
277
+ export const cloudAiChartSchema = () =>
140
278
  chartKinds().superRefine((chart, context) => {
141
- const issue = chartDataIssue(chart);
279
+ const total = cloudAiChartMarks(chart);
280
+ const issue =
281
+ total > CLOUD_AI_VIEW_MAX_MARKS
282
+ ? `The chart has ${total} marks; the limit is ${CLOUD_AI_VIEW_MAX_MARKS}. Aggregate or split into several views.`
283
+ : chartDataIssue(chart);
284
+ if (issue) context.addIssue({ code: "custom", message: issue });
285
+ }, whenValid);
286
+ /** Apps retain the renderer's permissive data contract, except values that would stall tick generation. */
287
+ export const cloudAiAppChartSchema = () =>
288
+ chartKinds(false).superRefine((chart, context) => {
289
+ const issue = chartSteppingIssue(chart);
142
290
  if (issue) context.addIssue({ code: "custom", message: issue });
143
291
  }, whenValid);
144
- const ChartSchema = /* @__PURE__ */ chartSchema();
292
+ const ChartSchema = /* @__PURE__ */ cloudAiChartSchema();
145
293
 
146
294
  /** One chart as the renderers read it: a `cloud.chart()` kind with its options. */
147
295
  export type CloudAiChartInput = z.output<ReturnType<typeof chartKinds>>;
148
296
 
149
297
  /**
150
- * Reads the arguments of a `chart` call without drawing them; undefined when they are not a chart. The tool schema
151
- * has drawn them once already, so renderers draw only for display.
298
+ * Reads one chart without drawing it; undefined when it is not a chart. Writers have drawn it once already, so
299
+ * renderers draw only for display.
152
300
  */
153
301
  export const parseCloudAiChartInput = (value: unknown): CloudAiChartInput | undefined => {
154
302
  const parsed = ChartSchema.safeParse(value);
155
303
  return parsed.success ? parsed.data : undefined;
156
304
  };
157
305
 
158
- const chartInputSchema = () => {
159
- const { ChartAxis, ChartBins, ChartCategory, ChartCommon, ChartLabel, ChartPoints, ChartSeries } = chartParts();
306
+ export const cloudAiGroupSchema = () => {
307
+ const { ChartCommon } = chartParts();
160
308
  return z
161
- .object({
162
- kind: z.enum(["bar", "line", "scatter", "pie", "donut", "histogram", "gauge", "sparkline"]),
309
+ .strictObject({
163
310
  ...ChartCommon,
164
- data: z
165
- .union([z.array(ChartCategory).min(1).max(CLOUD_AI_CHART_MAX_VALUES), z.array(z.number()).min(1).max(CLOUD_AI_CHART_MAX_VALUES)])
166
- .optional()
167
- .describe("bar, pie, donut: [{ label, value }]. histogram: raw observations to count into bins. sparkline: values in order."),
168
- series: z
169
- .array(ChartSeries(ChartPoints.min(1)))
170
- .min(1)
171
- .max(CLOUD_AI_CHART_MAX_SERIES)
172
- .optional()
173
- .describe("line and scatter: one entry per series. A line needs at least two points per series."),
174
- xAxis: ChartAxis.optional().describe("line and scatter."),
175
- yAxis: ChartAxis.optional().describe("bar, line and scatter."),
176
- legend: z
177
- .boolean()
311
+ charts: z
312
+ .array(cloudAiChartSchema())
313
+ .min(2, "Use at least 2 charts in a group.")
314
+ .max(CLOUD_AI_GROUP_MAX_CHARTS, `Use at most ${CLOUD_AI_GROUP_MAX_CHARTS} charts in a group.`),
315
+ linkX: z.boolean().optional(),
316
+ layout: z.enum(["stack", "grid"]).optional(),
317
+ })
318
+ .superRefine((group, context) => {
319
+ const total = group.charts.reduce((sum, chart) => sum + cloudAiChartMarks(chart), 0);
320
+ if (total > CLOUD_AI_VIEW_MAX_MARKS)
321
+ context.addIssue({
322
+ code: "custom",
323
+ message: `The group has ${total} marks; the limit is ${CLOUD_AI_VIEW_MAX_MARKS}. Aggregate or split into several views.`,
324
+ });
325
+ if (
326
+ group.linkX &&
327
+ (group.charts.some((chart) => chart.kind !== "line") ||
328
+ new Set(
329
+ group.charts.flatMap((chart) =>
330
+ chart.kind === "line" ? chart.series.flatMap((series) => series.data.map((point) => typeof point.x)) : [],
331
+ ),
332
+ ).size !== 1)
333
+ )
334
+ context.addIssue({ code: "custom", path: ["linkX"], message: "linkX links line charts with the same kind of x." });
335
+ }, whenValid);
336
+ };
337
+
338
+ const flatChartSchema = () => {
339
+ const { ChartAxis, ChartBins, ChartCommon, ChartLabel, ChartPoints, ChartSeries, ChartReferences, ChartThresholds, ChartUnit } =
340
+ chartParts();
341
+ return z.object({
342
+ kind: cloudAiChartKindSchema().optional().describe("Single charts: kind. Omit for groups."),
343
+ ...ChartCommon,
344
+ // One item object preserves every kind's fields before canonical validation; unions would strip overlapping items.
345
+ data: z
346
+ .union([
347
+ z
348
+ .array(
349
+ z.object({
350
+ label: ChartLabel.optional(),
351
+ value: z.number().optional(),
352
+ values: z.array(z.number()).min(1).optional(),
353
+ x: ChartLabel.optional(),
354
+ y: ChartLabel.optional(),
355
+ min: z.number().optional(),
356
+ max: z.number().optional(),
357
+ }),
358
+ )
359
+ .min(1)
360
+ .max(CLOUD_AI_VIEW_MAX_MARKS),
361
+ z.array(z.number()).min(1).max(CLOUD_AI_CHART_MAX_VALUES),
362
+ ])
363
+ .optional()
364
+ .describe(
365
+ "bar, pie, donut, barGauge: label/value (barGauge: min/max). boxplot: label/values. heatmap: x/y/value. histogram, sparkline: numbers.",
366
+ ),
367
+ series: z
368
+ .array(ChartSeries(ChartPoints.min(1)).extend({ style: z.enum(["solid", "dashed", "dotted"]).optional() }))
369
+ .min(1)
370
+ .max(CLOUD_AI_CHART_MAX_SERIES)
371
+ .optional()
372
+ .describe("line, scatter: label/data, optional reference role; line: style. Line needs 2 points per series."),
373
+ xAxis: ChartAxis.optional().describe("line, scatter: x axis."),
374
+ yAxis: ChartAxis.optional().describe("bar, line, scatter, boxplot: y axis."),
375
+ legend: z
376
+ .boolean()
377
+ .optional()
378
+ .describe("bar (colorByBar), line, scatter, pie, donut: legend; defaults to several series or unlabeled slices."),
379
+ colorByBar: z.boolean().optional().describe("bar: one palette color per bar."),
380
+ showValues: z.boolean().optional().describe("bar, heatmap: print values."),
381
+ area: z.boolean().optional().describe("line, sparkline: fill under the line."),
382
+ smooth: z.boolean().optional().describe("line: smooth curves."),
383
+ showLabels: z.boolean().optional().describe("pie, donut: name slices next to them."),
384
+ bins: ChartBins.optional(),
385
+ value: z.number().optional().describe("gauge, stat: value."),
386
+ min: z.number().optional().describe("gauge, barGauge: scale start (default 0). heatmap: color minimum."),
387
+ max: z.number().optional().describe("gauge, barGauge: scale end (default 100). heatmap: color maximum."),
388
+ label: ChartLabel.optional().describe("gauge: what the value measures."),
389
+ unit: ChartUnit.optional().describe("gauge, barGauge, stat: unit after value."),
390
+ format: cloudAiNumberFormatSchema()
391
+ .optional()
392
+ .describe("gauge, barGauge, stat, heatmap, pie, donut: number format; percent values are fractions."),
393
+ references: ChartReferences.optional().describe(
394
+ "bar, line, scatter, histogram, boxplot: reference lines; x only for line, scatter, histogram.",
395
+ ),
396
+ thresholds: ChartThresholds.optional().describe("gauge, barGauge: ascending band bounds, within min/max."),
397
+ delta: z.number().optional().describe("stat: change, in the value's units."),
398
+ better: z.enum(["up", "down"]).optional().describe("stat: which direction improves the measure."),
399
+ sparkline: z.array(z.number()).min(2).max(CLOUD_AI_CHART_MAX_VALUES).optional().describe("stat: recent values in order."),
400
+ });
401
+ };
402
+ const chartInputSchema = () => {
403
+ const flat = flatChartSchema();
404
+ const groupSchema = cloudAiGroupSchema();
405
+ return flat
406
+ .extend({
407
+ charts: z
408
+ .array(flat.required({ kind: true }).strict())
409
+ .min(2)
410
+ .max(CLOUD_AI_GROUP_MAX_CHARTS)
178
411
  .optional()
179
- .describe(
180
- "bar (with colorByBar), line, scatter, pie, donut. Defaults to true for several series, and for pie and donut unless showLabels is set.",
181
- ),
182
- colorByBar: z.boolean().optional().describe("bar: one palette color per bar."),
183
- showValues: z.boolean().optional().describe("bar: print each value on its bar."),
184
- area: z.boolean().optional().describe("line and sparkline: fill the area under the line."),
185
- smooth: z.boolean().optional().describe("line."),
186
- showLabels: z.boolean().optional().describe("pie and donut: name each slice next to it instead of in a legend."),
187
- bins: ChartBins.optional().describe("histogram: a bin count or ascending bin edges."),
188
- value: z.number().optional().describe("gauge: the value to show."),
189
- min: z.number().optional().describe("gauge: start of the scale, default 0."),
190
- max: z.number().optional().describe("gauge: end of the scale, default 100."),
191
- label: ChartLabel.optional().describe("gauge: what the value measures."),
192
- unit: z.string().trim().min(1).max(12).optional().describe("gauge: unit after the value."),
412
+ .describe(`Groups: 2..${CLOUD_AI_GROUP_MAX_CHARTS} related charts, each with a kind.`),
413
+ linkX: z.boolean().optional().describe("Groups: share the x cursor between line charts with the same kind of x."),
414
+ layout: z.enum(["stack", "grid"]).optional().describe("Groups: stack (default) or grid."),
193
415
  })
194
416
  .superRefine((value, context) => {
195
- const parsed = ChartSchema.safeParse(value);
196
- if (!parsed.success) {
197
- for (const issue of parsed.error.issues) context.addIssue({ code: "custom", message: issue.message, path: issue.path });
417
+ if ((value.kind !== undefined) === (value.charts !== undefined)) {
418
+ context.addIssue({ code: "custom", message: "Set exactly one of kind or charts." });
198
419
  return;
199
420
  }
200
- const issue = chartDrawingIssue(parsed.data);
201
- if (issue) context.addIssue({ code: "custom", message: issue });
421
+ if (value.charts !== undefined) {
422
+ const parsed = groupSchema.safeParse(value);
423
+ if (!parsed.success) {
424
+ for (const issue of parsed.error.issues) context.addIssue({ code: "custom", message: issue.message, path: issue.path });
425
+ return;
426
+ }
427
+ parsed.data.charts.forEach((chart, index) => {
428
+ const issue = chartDrawingIssue(chart);
429
+ if (issue) context.addIssue({ code: "custom", message: issue, path: ["charts", index] });
430
+ });
431
+ } else {
432
+ if (value.linkX !== undefined || value.layout !== undefined)
433
+ context.addIssue({ code: "custom", message: "linkX and layout belong to groups." });
434
+ const parsed = ChartSchema.safeParse(value);
435
+ if (!parsed.success) {
436
+ for (const issue of parsed.error.issues) context.addIssue({ code: "custom", message: issue.message, path: issue.path });
437
+ return;
438
+ }
439
+ const issue = chartDrawingIssue(parsed.data);
440
+ if (issue) context.addIssue({ code: "custom", message: issue });
441
+ }
202
442
  }, whenValid);
203
443
  };
204
444
 
205
445
  /**
206
- * The `cloud.chart()` options of Studio code as plain data, without `width`, `height` and `format` functions. Model
207
- * providers accept only an object at the root of a tool schema, so every kind shares one object and each field names
208
- * the kinds it belongs to; the kind's own rules are checked after that. Valid input also draws: values outside a
209
- * domain, values at or below zero on a log axis, or a pie too small for its legend fail here instead of in the chat.
446
+ * The `cloud.chart()` options of Studio code as plain data, without `width`, `height` and `format` functions, or a
447
+ * group of such charts. Model providers accept only an object at the root of a tool schema, so every kind shares one
448
+ * object and each field names the kinds it belongs to; the kind's or group's own rules are checked after that. Valid
449
+ * input also draws: values outside a domain, values at or below zero on a log axis, or a pie too small for its legend
450
+ * fail here instead of in the chat.
210
451
  */
211
452
  export const CloudAiChartInputSchema = /* @__PURE__ */ chartInputSchema();
212
453
  export const CloudAiChartOutputSchema = z.object({ displayed: z.boolean() });
@@ -293,6 +534,8 @@ export const CloudAiLocalBashOutputSchema = z.object({
293
534
  stdout: z.string().max(512 * 1024),
294
535
  stderr: z.string().max(512 * 1024),
295
536
  truncated: z.boolean(),
537
+ /** Local file with the complete output when the client returned only its beginning and end. */
538
+ outputPath: z.string().max(4096).optional(),
296
539
  });
297
540
 
298
541
  export type CloudAiCardInput = z.infer<typeof CloudAiCardInputSchema>;