@agent-native/core 0.76.9 → 0.76.11

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 (137) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +39 -0
  3. package/corpus/core/docs/content/locales/ar-SA/actions.md +7 -7
  4. package/corpus/core/docs/content/locales/ar-SA/template-plan.md +6 -4
  5. package/corpus/core/docs/content/locales/de-DE/actions.md +9 -9
  6. package/corpus/core/docs/content/locales/de-DE/template-plan.md +6 -4
  7. package/corpus/core/docs/content/locales/es-ES/actions.md +9 -9
  8. package/corpus/core/docs/content/locales/es-ES/template-plan.md +6 -4
  9. package/corpus/core/docs/content/locales/fr-FR/actions.md +9 -9
  10. package/corpus/core/docs/content/locales/fr-FR/template-plan.md +6 -4
  11. package/corpus/core/docs/content/locales/hi-IN/actions.md +9 -9
  12. package/corpus/core/docs/content/locales/hi-IN/template-plan.md +4 -4
  13. package/corpus/core/docs/content/locales/ja-JP/actions.md +9 -9
  14. package/corpus/core/docs/content/locales/ja-JP/template-plan.md +4 -4
  15. package/corpus/core/docs/content/locales/ko-KR/actions.md +9 -9
  16. package/corpus/core/docs/content/locales/ko-KR/template-plan.md +4 -4
  17. package/corpus/core/docs/content/locales/pt-BR/actions.md +9 -9
  18. package/corpus/core/docs/content/locales/pt-BR/template-plan.md +6 -4
  19. package/corpus/core/docs/content/locales/zh-CN/actions.md +9 -9
  20. package/corpus/core/docs/content/locales/zh-CN/template-plan.md +4 -4
  21. package/corpus/core/package.json +1 -1
  22. package/corpus/core/scripts/generate-og-font-data.mjs +9 -5
  23. package/corpus/core/src/client/AgentPanel.tsx +70 -1
  24. package/corpus/core/src/client/AssistantChat.tsx +1 -3
  25. package/corpus/core/src/client/ErrorBoundary.tsx +151 -10
  26. package/corpus/core/src/client/MultiTabAssistantChat.tsx +0 -62
  27. package/corpus/core/src/client/composer/TiptapComposer.tsx +6 -13
  28. package/corpus/core/src/server/og-fonts-data.ts +7 -4
  29. package/corpus/core/src/server/og-fonts.ts +10 -3
  30. package/corpus/core/src/server/social-og-image.ts +45 -6
  31. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +199 -19
  32. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +51 -31
  33. package/corpus/templates/analytics/app/components/layout/DashboardHeader.tsx +26 -16
  34. package/corpus/templates/analytics/app/components/layout/Header.tsx +1 -2
  35. package/corpus/templates/analytics/app/components/layout/HeaderActions.tsx +8 -1
  36. package/corpus/templates/analytics/app/components/layout/Layout.tsx +2 -5
  37. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +172 -107
  38. package/corpus/templates/analytics/app/i18n-data.ts +1076 -5
  39. package/corpus/templates/analytics/app/pages/Settings.tsx +2 -2
  40. package/corpus/templates/analytics/app/pages/adhoc/AdhocRouter.tsx +1 -21
  41. package/corpus/templates/analytics/app/pages/adhoc/DashboardSkeleton.tsx +35 -0
  42. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -23
  43. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +3 -26
  44. package/corpus/templates/analytics/changelog/2026-06-24-analytics-navigation-command-palette-and-settings-now-honor-.md +6 -0
  45. package/corpus/templates/analytics/changelog/2026-06-24-daily-dashboard-charts-now-use-local-day-buckets-and-show-th.md +6 -0
  46. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-loading-placeholders-now-span-the-available-page-w.md +6 -0
  47. package/corpus/templates/analytics/changelog/2026-06-24-first-party-dashboard-dates-now-align-to-pacific-time-for-da.md +6 -0
  48. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +19 -7
  49. package/corpus/templates/assets/app/components/layout/Header.tsx +0 -2
  50. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +0 -2
  51. package/corpus/templates/calendar/app/pages/CalendarView.tsx +0 -2
  52. package/corpus/templates/chat/app/components/layout/Header.tsx +1 -6
  53. package/corpus/templates/clips/app/components/layout/Header.tsx +1 -6
  54. package/corpus/templates/clips/app/hooks/use-library.ts +2 -0
  55. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +3 -1
  56. package/corpus/templates/clips/changelog/2026-06-24-archive-and-spaces-navigation-is-more-resilient-when-sidebar.md +6 -0
  57. package/corpus/templates/content/app/components/layout/Header.tsx +0 -2
  58. package/corpus/templates/design/app/components/layout/Header.tsx +1 -6
  59. package/corpus/templates/forms/app/components/layout/Header.tsx +1 -6
  60. package/corpus/templates/macros/app/components/layout/Header.tsx +1 -6
  61. package/corpus/templates/mail/app/components/email/CodeBlockLangPicker.tsx +4 -2
  62. package/corpus/templates/mail/app/components/email/ComposeBubbleToolbar.tsx +18 -12
  63. package/corpus/templates/mail/app/components/email/ComposeEditor.tsx +9 -5
  64. package/corpus/templates/mail/app/components/email/ComposeModal.tsx +36 -19
  65. package/corpus/templates/mail/app/components/email/EmailList.tsx +59 -55
  66. package/corpus/templates/mail/app/components/email/EmailListItem.tsx +7 -3
  67. package/corpus/templates/mail/app/components/email/EmailThread.tsx +25 -16
  68. package/corpus/templates/mail/app/components/email/InlineReplyComposer.tsx +25 -19
  69. package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +32 -17
  70. package/corpus/templates/mail/app/components/email/RecipientInput.tsx +26 -10
  71. package/corpus/templates/mail/app/components/email/SendLaterButton.tsx +5 -3
  72. package/corpus/templates/mail/app/components/email/SnoozeModal.tsx +4 -2
  73. package/corpus/templates/mail/app/components/email/SnoozePopover.tsx +9 -5
  74. package/corpus/templates/mail/app/components/email/extensions/ComposeImageBlock.tsx +4 -2
  75. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +168 -86
  76. package/corpus/templates/mail/app/components/layout/SearchBar.tsx +4 -2
  77. package/corpus/templates/mail/app/i18n/ar-SA.ts +212 -0
  78. package/corpus/templates/mail/app/i18n/de-DE.ts +212 -0
  79. package/corpus/templates/mail/app/i18n/en-US.ts +212 -0
  80. package/corpus/templates/mail/app/i18n/es-ES.ts +212 -0
  81. package/corpus/templates/mail/app/i18n/fr-FR.ts +212 -0
  82. package/corpus/templates/mail/app/i18n/hi-IN.ts +212 -0
  83. package/corpus/templates/mail/app/i18n/ja-JP.ts +212 -0
  84. package/corpus/templates/mail/app/i18n/ko-KR.ts +212 -0
  85. package/corpus/templates/mail/app/i18n/pt-BR.ts +212 -0
  86. package/corpus/templates/mail/app/i18n/zh-CN.ts +211 -0
  87. package/corpus/templates/mail/changelog/2026-06-24-interface-language-support-now-covers-more-mail-controls-and.md +6 -0
  88. package/corpus/templates/plan/app/components/layout/Header.tsx +1 -6
  89. package/corpus/templates/slides/app/components/layout/Header.tsx +1 -6
  90. package/corpus/templates/videos/app/components/layout/Header.tsx +1 -6
  91. package/dist/client/AgentPanel.d.ts +5 -1
  92. package/dist/client/AgentPanel.d.ts.map +1 -1
  93. package/dist/client/AgentPanel.js +23 -4
  94. package/dist/client/AgentPanel.js.map +1 -1
  95. package/dist/client/AssistantChat.d.ts +1 -3
  96. package/dist/client/AssistantChat.d.ts.map +1 -1
  97. package/dist/client/AssistantChat.js.map +1 -1
  98. package/dist/client/ErrorBoundary.d.ts.map +1 -1
  99. package/dist/client/ErrorBoundary.js +128 -7
  100. package/dist/client/ErrorBoundary.js.map +1 -1
  101. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  102. package/dist/client/MultiTabAssistantChat.js +1 -17
  103. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  104. package/dist/client/composer/TiptapComposer.d.ts +4 -0
  105. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  106. package/dist/client/composer/TiptapComposer.js +5 -9
  107. package/dist/client/composer/TiptapComposer.js.map +1 -1
  108. package/dist/server/og-fonts-data.d.ts +1 -0
  109. package/dist/server/og-fonts-data.d.ts.map +1 -1
  110. package/dist/server/og-fonts-data.js +5 -4
  111. package/dist/server/og-fonts-data.js.map +1 -1
  112. package/dist/server/og-fonts.d.ts +1 -0
  113. package/dist/server/og-fonts.d.ts.map +1 -1
  114. package/dist/server/og-fonts.js +10 -4
  115. package/dist/server/og-fonts.js.map +1 -1
  116. package/dist/server/social-og-image.d.ts.map +1 -1
  117. package/dist/server/social-og-image.js +33 -6
  118. package/dist/server/social-og-image.js.map +1 -1
  119. package/docs/content/locales/ar-SA/actions.md +7 -7
  120. package/docs/content/locales/ar-SA/template-plan.md +6 -4
  121. package/docs/content/locales/de-DE/actions.md +9 -9
  122. package/docs/content/locales/de-DE/template-plan.md +6 -4
  123. package/docs/content/locales/es-ES/actions.md +9 -9
  124. package/docs/content/locales/es-ES/template-plan.md +6 -4
  125. package/docs/content/locales/fr-FR/actions.md +9 -9
  126. package/docs/content/locales/fr-FR/template-plan.md +6 -4
  127. package/docs/content/locales/hi-IN/actions.md +9 -9
  128. package/docs/content/locales/hi-IN/template-plan.md +4 -4
  129. package/docs/content/locales/ja-JP/actions.md +9 -9
  130. package/docs/content/locales/ja-JP/template-plan.md +4 -4
  131. package/docs/content/locales/ko-KR/actions.md +9 -9
  132. package/docs/content/locales/ko-KR/template-plan.md +4 -4
  133. package/docs/content/locales/pt-BR/actions.md +9 -9
  134. package/docs/content/locales/pt-BR/template-plan.md +6 -4
  135. package/docs/content/locales/zh-CN/actions.md +9 -9
  136. package/docs/content/locales/zh-CN/template-plan.md +4 -4
  137. package/package.json +1 -1
@@ -5,14 +5,16 @@ import path from "node:path";
5
5
  import {
6
6
  LIBERATION_SANS_BOLD_BASE64,
7
7
  LIBERATION_SANS_REGULAR_BASE64,
8
+ NOTO_NASKH_ARABIC_BASE64,
8
9
  } from "./og-fonts-data.js";
9
10
 
10
11
  /**
11
12
  * Liberation Sans is the metric-compatible libre replacement for
12
- * Arial/Helvetica that the OG image SVG asks for. It ships embedded as base64
13
+ * Arial/Helvetica that the OG image SVG asks for. Noto Naskh Arabic fills the
14
+ * Arabic script gap for localized docs titles. They ship embedded as base64
13
15
  * (see {@link ./og-fonts-data.ts}) so the renderer never depends on the host's
14
- * system fonts — Linux serverless runtimes (Netlify/Lambda) have neither Arial
15
- * nor Inter, which previously left every `<text>` element rendering nothing.
16
+ * system fonts — Linux serverless runtimes (Netlify/Lambda) have neither Arial,
17
+ * Inter, nor Arabic fallback fonts.
16
18
  */
17
19
  const OG_FONT_FILES = [
18
20
  {
@@ -20,9 +22,14 @@ const OG_FONT_FILES = [
20
22
  base64: LIBERATION_SANS_REGULAR_BASE64,
21
23
  },
22
24
  { filename: "LiberationSans-Bold.ttf", base64: LIBERATION_SANS_BOLD_BASE64 },
25
+ {
26
+ filename: "NotoNaskhArabic-Variable.ttf",
27
+ base64: NOTO_NASKH_ARABIC_BASE64,
28
+ },
23
29
  ] as const;
24
30
 
25
31
  export const OG_FONT_FAMILY = "Liberation Sans";
32
+ export const OG_ARABIC_FONT_FAMILY = "Noto Naskh Arabic";
26
33
 
27
34
  let cachedFontFiles: string[] | null | undefined;
28
35
 
@@ -11,7 +11,11 @@ import {
11
11
  resolveBuiltInAuthMarketingByName,
12
12
  } from "./auth-marketing.js";
13
13
  import { getAppName } from "./app-name.js";
14
- import { OG_FONT_FAMILY, resolveOgFontFiles } from "./og-fonts.js";
14
+ import {
15
+ OG_ARABIC_FONT_FAMILY,
16
+ OG_FONT_FAMILY,
17
+ resolveOgFontFiles,
18
+ } from "./og-fonts.js";
15
19
 
16
20
  export interface AgentNativeOgImageInput {
17
21
  appName?: string | null;
@@ -32,7 +36,8 @@ const BRAND_BLUE = "#00B5FF";
32
36
  const BRAND_MINT = "#48FFE4";
33
37
  const BG = "#000000";
34
38
  const FG = "#f5f5f5";
35
- const FONT_FAMILY = `${OG_FONT_FAMILY}, Arial, Helvetica, system-ui, sans-serif`;
39
+ const DEFAULT_FONT_FAMILY = `${OG_FONT_FAMILY}, Arial, Helvetica, system-ui, sans-serif`;
40
+ const ARABIC_FONT_FAMILY = `${OG_ARABIC_FONT_FAMILY}, ${OG_FONT_FAMILY}, Arial, Helvetica, system-ui, sans-serif`;
36
41
  const DEFAULT_ACCENT_TEXT = "100% free and open source";
37
42
 
38
43
  const LOGO_MARK = `
@@ -101,6 +106,16 @@ function estimateTextWidth(value: string, fontSize: number): number {
101
106
  return units * fontSize;
102
107
  }
103
108
 
109
+ function containsArabicText(value: string): boolean {
110
+ return /[\u0600-\u06ff\u0750-\u077f\u0870-\u089f\ufb50-\ufdff\ufe70-\ufeff]/u.test(
111
+ value,
112
+ );
113
+ }
114
+
115
+ function fontFamilyForText(value: string): string {
116
+ return containsArabicText(value) ? ARABIC_FONT_FAMILY : DEFAULT_FONT_FAMILY;
117
+ }
118
+
104
119
  function trimTextToWidth(
105
120
  value: string,
106
121
  fontSize: number,
@@ -205,6 +220,8 @@ function textBlock({
205
220
  weight,
206
221
  fill,
207
222
  anchor = "start",
223
+ direction,
224
+ fontFamily = DEFAULT_FONT_FAMILY,
208
225
  }: {
209
226
  lines: string[];
210
227
  x: number;
@@ -213,9 +230,14 @@ function textBlock({
213
230
  lineHeight: number;
214
231
  weight: number;
215
232
  fill: string;
216
- anchor?: "start" | "middle";
233
+ anchor?: "start" | "middle" | "end";
234
+ direction?: "ltr" | "rtl";
235
+ fontFamily?: string;
217
236
  }): string {
218
- return `<text x="${x}" y="${y}" text-anchor="${anchor}" font-family="${FONT_FAMILY}" font-size="${fontSize}" font-weight="${weight}" fill="${fill}">${lines
237
+ const directionAttrs = direction
238
+ ? ` direction="${direction}" unicode-bidi="plaintext"`
239
+ : "";
240
+ return `<text x="${x}" y="${y}" text-anchor="${anchor}"${directionAttrs} font-family="${fontFamily}" font-size="${fontSize}" font-weight="${weight}" fill="${fill}">${lines
219
241
  .map(
220
242
  (line, index) =>
221
243
  `<tspan x="${x}" dy="${index === 0 ? 0 : lineHeight}">${escapeSvg(line)}</tspan>`,
@@ -289,6 +311,10 @@ export function renderAgentNativeOgImageSvg(
289
311
  const title = cleanText(input.title) || titleFromAppName(appName);
290
312
  const accentText = cleanText(input.accentText) || DEFAULT_ACCENT_TEXT;
291
313
  const titleLayout = getTitleLayout(title);
314
+ const titleIsRtl = containsArabicText(title);
315
+ const textX = titleIsRtl ? WIDTH - 80 : 80;
316
+ const accentX = titleIsRtl ? WIDTH - 84 : 84;
317
+ const textAnchor = titleIsRtl ? "end" : "start";
292
318
  const titleY = titleLayout.lines.length > 1 ? 288 : 330;
293
319
  const accentY =
294
320
  titleY + titleLayout.lineHeight * (titleLayout.lines.length - 1) + 70;
@@ -312,7 +338,7 @@ export function renderAgentNativeOgImageSvg(
312
338
  <g>
313
339
  ${textBlock({
314
340
  lines: titleLayout.lines,
315
- x: 80,
341
+ x: textX,
316
342
  y: titleY,
317
343
  fontSize: titleLayout.fontSize,
318
344
  lineHeight: titleLayout.lineHeight,
@@ -321,8 +347,21 @@ export function renderAgentNativeOgImageSvg(
321
347
  // bundle, which is the intended look for the display title.
322
348
  weight: 800,
323
349
  fill: FG,
350
+ anchor: textAnchor,
351
+ direction: titleIsRtl ? "rtl" : undefined,
352
+ fontFamily: fontFamilyForText(title),
353
+ })}
354
+ ${textBlock({
355
+ lines: [accentText],
356
+ x: accentX,
357
+ y: accentY,
358
+ fontSize: 34,
359
+ lineHeight: 40,
360
+ weight: 800,
361
+ fill: BRAND_BLUE,
362
+ anchor: textAnchor,
363
+ fontFamily: fontFamilyForText(accentText),
324
364
  })}
325
- <text x="84" y="${accentY}" font-family="${FONT_FAMILY}" font-size="34" font-weight="800" fill="${BRAND_BLUE}">${escapeSvg(accentText)}</text>
326
365
  </g>
327
366
  </svg>`;
328
367
  }
@@ -97,6 +97,10 @@ const CHART_LEGEND_PROPS = {
97
97
  wrapperStyle: CHART_LEGEND_WRAPPER_STYLE,
98
98
  } as const;
99
99
 
100
+ const PARTIAL_DAY_TIME_ZONE = "America/Los_Angeles";
101
+ const PARTIAL_DAY_DASH = "3 5";
102
+ const PARTIAL_DAY_KEY_PREFIX = "__sql_chart_partial_day";
103
+
100
104
  function formatYValue(
101
105
  value: number,
102
106
  formatter?: "number" | "currency" | "percent",
@@ -167,6 +171,122 @@ function truncateLabel(value: string, max = 48): string {
167
171
  return `${value.slice(0, max - 3)}...`;
168
172
  }
169
173
 
174
+ function parseCalendarDate(value: string): Date | null {
175
+ const normalized = /^\d{8}$/.test(value)
176
+ ? `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`
177
+ : value;
178
+ const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(normalized);
179
+ if (dateOnly) {
180
+ return new Date(
181
+ Number(dateOnly[1]),
182
+ Number(dateOnly[2]) - 1,
183
+ Number(dateOnly[3]),
184
+ );
185
+ }
186
+ const parsed = new Date(normalized);
187
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
188
+ }
189
+
190
+ export function toSqlChartDateKey(value: unknown): string | null {
191
+ if (typeof value === "string") {
192
+ const trimmed = value.trim();
193
+ const ymd = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
194
+ if (ymd) return `${ymd[1]}-${ymd[2]}-${ymd[3]}`;
195
+ const compact = /^(\d{4})(\d{2})(\d{2})$/.exec(trimmed);
196
+ if (compact) return `${compact[1]}-${compact[2]}-${compact[3]}`;
197
+ }
198
+
199
+ const parsed = new Date(String(value ?? ""));
200
+ return Number.isNaN(parsed.getTime()) ? null : sqlChartLocalDateKey(parsed);
201
+ }
202
+
203
+ export function sqlChartLocalDateKey(
204
+ date = new Date(),
205
+ timeZone = PARTIAL_DAY_TIME_ZONE,
206
+ ): string {
207
+ try {
208
+ const parts = new Intl.DateTimeFormat("en-US", {
209
+ timeZone,
210
+ year: "numeric",
211
+ month: "2-digit",
212
+ day: "2-digit",
213
+ }).formatToParts(date);
214
+ const get = (type: string) =>
215
+ parts.find((part) => part.type === type)?.value ?? "";
216
+ const year = get("year");
217
+ const month = get("month");
218
+ const day = get("day");
219
+ if (year && month && day) return `${year}-${month}-${day}`;
220
+ } catch {}
221
+ return date.toISOString().slice(0, 10);
222
+ }
223
+
224
+ export interface SplitTimeSeries {
225
+ key: string;
226
+ solidKey: string;
227
+ partialKey: string | null;
228
+ }
229
+
230
+ export function splitCurrentDayTimeSeriesRows(
231
+ rows: Record<string, unknown>[],
232
+ xKey: string,
233
+ yKeys: string[],
234
+ todayKey = sqlChartLocalDateKey(),
235
+ ): { rows: Record<string, unknown>[]; series: SplitTimeSeries[] } {
236
+ const todayIndexes = rows
237
+ .map((row, index) => ({ index, dateKey: toSqlChartDateKey(row[xKey]) }))
238
+ .filter((entry) => entry.dateKey === todayKey)
239
+ .map((entry) => entry.index);
240
+ const firstTodayIndex = todayIndexes[0] ?? -1;
241
+
242
+ if (firstTodayIndex <= 0 || yKeys.length === 0) {
243
+ return {
244
+ rows,
245
+ series: yKeys.map((key) => ({ key, solidKey: key, partialKey: null })),
246
+ };
247
+ }
248
+
249
+ const todaySet = new Set(todayIndexes);
250
+ const previousIndex = firstTodayIndex - 1;
251
+ const series = yKeys.map((key, index) => ({
252
+ key,
253
+ solidKey: `${PARTIAL_DAY_KEY_PREFIX}_${index}_solid`,
254
+ partialKey: `${PARTIAL_DAY_KEY_PREFIX}_${index}_partial`,
255
+ }));
256
+ const splitRows = rows.map((row, index) => {
257
+ const next = { ...row };
258
+ const isToday = todaySet.has(index);
259
+ const isPartialSegmentStart = index === previousIndex;
260
+ for (const item of series) {
261
+ next[item.solidKey] = isToday ? null : row[item.key];
262
+ next[item.partialKey] =
263
+ isToday || isPartialSegmentStart ? row[item.key] : null;
264
+ }
265
+ return next;
266
+ });
267
+
268
+ return { rows: splitRows, series };
269
+ }
270
+
271
+ export function shouldSplitCurrentDayTimeSeries(
272
+ panel: Pick<SqlPanel, "source">,
273
+ xKey: string,
274
+ ): boolean {
275
+ if (panel.source === "prometheus") return false;
276
+
277
+ const normalizedKey = xKey.trim().toLowerCase();
278
+ if (normalizedKey === "timestamp" || normalizedKey.endsWith("_timestamp")) {
279
+ return false;
280
+ }
281
+
282
+ return (
283
+ normalizedKey === "date" ||
284
+ normalizedKey === "day" ||
285
+ normalizedKey.endsWith("_date") ||
286
+ normalizedKey.endsWith("_day")
287
+ );
288
+ }
289
+
170
290
  function formatSeriesLabel(value: string): string {
171
291
  const { metric, labels } = parsePrometheusSeriesLabel(value);
172
292
  const target =
@@ -232,11 +352,8 @@ function formatSeriesLabelForPanel(panel: SqlPanel, value: string): string {
232
352
  function formatXLabel(value: string, panel: SqlPanel): string {
233
353
  try {
234
354
  const s = String(value);
235
- const normalized = /^\d{8}$/.test(s)
236
- ? `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`
237
- : s;
238
- const d = new Date(normalized);
239
- if (!isNaN(d.getTime()) && s.length >= 8) {
355
+ const d = parseCalendarDate(s);
356
+ if (d && s.length >= 8) {
240
357
  return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
241
358
  }
242
359
  } catch {}
@@ -252,12 +369,21 @@ function numericTooltipValue(value: unknown): number {
252
369
  return Number.isFinite(numeric) ? numeric : Number.NEGATIVE_INFINITY;
253
370
  }
254
371
 
372
+ function tooltipItemName(item: {
373
+ dataKey?: string | number;
374
+ name?: string | number;
375
+ }): string {
376
+ return String(item.name ?? item.dataKey ?? "");
377
+ }
378
+
255
379
  export function sortTooltipPayloadItems<
256
380
  T extends {
381
+ dataKey?: string | number;
382
+ name?: string | number;
257
383
  value?: unknown;
258
384
  },
259
385
  >(items: T[]): T[] {
260
- return items
386
+ const sorted = items
261
387
  .map((item, index) => ({ item, index }))
262
388
  .sort((a, b) => {
263
389
  const diff =
@@ -265,6 +391,13 @@ export function sortTooltipPayloadItems<
265
391
  return diff !== 0 ? diff : a.index - b.index;
266
392
  })
267
393
  .map(({ item }) => item);
394
+ const seen = new Set<string>();
395
+ return sorted.filter((item) => {
396
+ const name = tooltipItemName(item);
397
+ if (seen.has(name)) return false;
398
+ seen.add(name);
399
+ return true;
400
+ });
268
401
  }
269
402
 
270
403
  function useSeriesVisibility(keys: string[]): {
@@ -1390,6 +1523,21 @@ function TimeSeriesRenderer({
1390
1523
  const seriesNameFormatter = (name: string) =>
1391
1524
  formatSeriesLabelForPanel(panel, name);
1392
1525
  const { hiddenKeys, visibleKeys, toggleSeries } = useSeriesVisibility(yKeys);
1526
+ const splitPartialDay = shouldSplitCurrentDayTimeSeries(panel, xKey);
1527
+ const { rows: chartRows, series } = useMemo(
1528
+ () =>
1529
+ splitPartialDay
1530
+ ? splitCurrentDayTimeSeriesRows(rows, xKey, yKeys)
1531
+ : {
1532
+ rows,
1533
+ series: yKeys.map((key) => ({
1534
+ key,
1535
+ solidKey: key,
1536
+ partialKey: null,
1537
+ })),
1538
+ },
1539
+ [rows, xKey, yKeys, splitPartialDay],
1540
+ );
1393
1541
 
1394
1542
  if (chartType === "line") {
1395
1543
  return (
@@ -1402,7 +1550,7 @@ function TimeSeriesRenderer({
1402
1550
  showCustomLegend
1403
1551
  >
1404
1552
  <ResponsiveContainer width="100%" height="100%">
1405
- <LineChart data={rows}>
1553
+ <LineChart data={chartRows}>
1406
1554
  <XAxis
1407
1555
  dataKey={xKey}
1408
1556
  stroke="hsl(var(--muted-foreground))"
@@ -1435,18 +1583,33 @@ function TimeSeriesRenderer({
1435
1583
  }
1436
1584
  itemSorter={(item) => -(Number(item.value) || 0)}
1437
1585
  />
1438
- {yKeys.map((key, i) => (
1586
+ {series.map((item, i) => (
1439
1587
  <Line
1440
- key={key}
1588
+ key={item.solidKey}
1441
1589
  type="monotone"
1442
- dataKey={key}
1443
- name={seriesNameFormatter(key)}
1590
+ dataKey={item.solidKey}
1591
+ name={seriesNameFormatter(item.key)}
1444
1592
  stroke={colors[i % colors.length]}
1445
1593
  strokeWidth={2}
1446
1594
  dot={false}
1447
- hide={hiddenKeys.has(key)}
1595
+ hide={hiddenKeys.has(item.key)}
1448
1596
  />
1449
1597
  ))}
1598
+ {series.map((item, i) =>
1599
+ item.partialKey ? (
1600
+ <Line
1601
+ key={item.partialKey}
1602
+ type="monotone"
1603
+ dataKey={item.partialKey}
1604
+ name={seriesNameFormatter(item.key)}
1605
+ stroke={colors[i % colors.length]}
1606
+ strokeWidth={2}
1607
+ strokeDasharray={PARTIAL_DAY_DASH}
1608
+ dot={false}
1609
+ hide={hiddenKeys.has(item.key)}
1610
+ />
1611
+ ) : null,
1612
+ )}
1450
1613
  </LineChart>
1451
1614
  </ResponsiveContainer>
1452
1615
  </ChartFrame>
@@ -1468,7 +1631,7 @@ function TimeSeriesRenderer({
1468
1631
  showCustomLegend
1469
1632
  >
1470
1633
  <ResponsiveContainer width="100%" height="100%">
1471
- <AreaChart data={rows}>
1634
+ <AreaChart data={chartRows}>
1472
1635
  {showFill && (
1473
1636
  <defs>
1474
1637
  {yKeys.map((key, i) => (
@@ -1526,20 +1689,37 @@ function TimeSeriesRenderer({
1526
1689
  }
1527
1690
  itemSorter={(item) => -(Number(item.value) || 0)}
1528
1691
  />
1529
- {yKeys.map((key, i) => (
1692
+ {series.map((item, i) => (
1530
1693
  <Area
1531
- key={key}
1694
+ key={item.solidKey}
1532
1695
  type="monotone"
1533
- dataKey={key}
1534
- name={seriesNameFormatter(key)}
1696
+ dataKey={item.solidKey}
1697
+ name={seriesNameFormatter(item.key)}
1535
1698
  stroke={colors[i % colors.length]}
1536
1699
  strokeWidth={2}
1537
1700
  fillOpacity={showFill ? 1 : 0}
1538
- fill={showFill ? `url(#sql-gradient-${key})` : "none"}
1701
+ fill={showFill ? `url(#sql-gradient-${item.key})` : "none"}
1539
1702
  stackId={stacked ? "stack" : undefined}
1540
- hide={hiddenKeys.has(key)}
1703
+ hide={hiddenKeys.has(item.key)}
1541
1704
  />
1542
1705
  ))}
1706
+ {series.map((item, i) =>
1707
+ item.partialKey ? (
1708
+ <Area
1709
+ key={item.partialKey}
1710
+ type="monotone"
1711
+ dataKey={item.partialKey}
1712
+ name={seriesNameFormatter(item.key)}
1713
+ stroke={colors[i % colors.length]}
1714
+ strokeWidth={2}
1715
+ strokeDasharray={PARTIAL_DAY_DASH}
1716
+ fill="none"
1717
+ fillOpacity={0}
1718
+ stackId={stacked ? "partial-stack" : undefined}
1719
+ hide={hiddenKeys.has(item.key)}
1720
+ />
1721
+ ) : null,
1722
+ )}
1543
1723
  </AreaChart>
1544
1724
  </ResponsiveContainer>
1545
1725
  </ChartFrame>
@@ -27,6 +27,7 @@ import {
27
27
  callAction,
28
28
  useChangeVersions,
29
29
  ChangelogDialog,
30
+ useT,
30
31
  } from "@agent-native/core/client";
31
32
  import { extensionPath } from "@agent-native/core/client/extensions";
32
33
  import changelog from "../../../CHANGELOG.md?raw";
@@ -50,10 +51,14 @@ interface ExtensionSearchItem {
50
51
  }
51
52
 
52
53
  const defaultTools = [
53
- { id: "explorer", name: "Explorer", href: "/dashboards/explorer" },
54
+ {
55
+ id: "explorer",
56
+ nameKey: "commandPalette.toolExplorer",
57
+ href: "/dashboards/explorer",
58
+ },
54
59
  {
55
60
  id: "customer-health",
56
- name: "Customer Health",
61
+ nameKey: "commandPalette.toolCustomerHealth",
57
62
  href: "/dashboards/customer-health",
58
63
  },
59
64
  ];
@@ -126,9 +131,9 @@ async function fetchExplorerDashboards(): Promise<ExplorerDashboard[]> {
126
131
  }
127
132
  }
128
133
 
129
- async function fetchSqlDashboards(): Promise<
130
- { id: string; name: string; hiddenAt: string | null }[]
131
- > {
134
+ async function fetchSqlDashboards(
135
+ t: (key: string) => string,
136
+ ): Promise<{ id: string; name: string; hiddenAt: string | null }[]> {
132
137
  try {
133
138
  const rows = await callAction(
134
139
  "list-sql-dashboards",
@@ -142,7 +147,7 @@ async function fetchSqlDashboards(): Promise<
142
147
  name:
143
148
  typeof d.name === "string" && d.name.trim().length > 0
144
149
  ? d.name
145
- : "Untitled dashboard",
150
+ : t("commandPalette.untitledDashboard"),
146
151
  hiddenAt: typeof d.hiddenAt === "string" ? d.hiddenAt : null,
147
152
  }));
148
153
  } catch {
@@ -183,6 +188,7 @@ function persistThemePreference(theme: "light" | "dark") {
183
188
  }
184
189
 
185
190
  export function CommandPalette() {
191
+ const t = useT();
186
192
  const [open, setOpen] = useState(false);
187
193
  const [changelogOpen, setChangelogOpen] = useState(false);
188
194
  const [searchQuery, setSearchQuery] = useState("");
@@ -213,7 +219,7 @@ export function CommandPalette() {
213
219
  const { data: sqlDashboards = [], isFetching: sqlDashboardsFetching } =
214
220
  useQuery({
215
221
  queryKey: ["sql-dashboards-palette", dashboardsSync],
216
- queryFn: fetchSqlDashboards,
222
+ queryFn: () => fetchSqlDashboards(t),
217
223
  staleTime: 30_000,
218
224
  enabled: open,
219
225
  placeholderData: (prev) => prev,
@@ -276,21 +282,24 @@ export function CommandPalette() {
276
282
  }}
277
283
  >
278
284
  <CommandInput
279
- placeholder="Search dashboards, extensions, charts..."
285
+ placeholder={t("commandPalette.searchPlaceholder")}
280
286
  value={searchQuery}
281
287
  onValueChange={setSearchQuery}
282
288
  />
283
289
  <CommandList>
284
290
  {!asyncGroupsLoading && (
285
- <CommandEmpty>No results found.</CommandEmpty>
291
+ <CommandEmpty>{t("commandPalette.noResults")}</CommandEmpty>
286
292
  )}
287
293
 
288
294
  {explorerDashboardsFetching && explorerDashboards.length === 0 && (
289
- <CommandLoadingGroup heading="Explorer Dashboards" rows={2} />
295
+ <CommandLoadingGroup
296
+ heading={t("commandPalette.groupExplorerDashboards")}
297
+ rows={2}
298
+ />
290
299
  )}
291
300
 
292
301
  {visibleExplorerDashboards.length > 0 && (
293
- <CommandGroup heading="Explorer Dashboards">
302
+ <CommandGroup heading={t("commandPalette.groupExplorerDashboards")}>
294
303
  {visibleExplorerDashboards.map((d) => (
295
304
  <CommandItem
296
305
  key={`ed-${d.id}`}
@@ -307,7 +316,7 @@ export function CommandPalette() {
307
316
  <span className="truncate">{d.name}</span>
308
317
  {d.hiddenAt ? (
309
318
  <span className="ms-2 rounded border border-border px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
310
- Hidden
319
+ {t("commandPalette.hidden")}
311
320
  </span>
312
321
  ) : null}
313
322
  </CommandItem>
@@ -316,11 +325,14 @@ export function CommandPalette() {
316
325
  )}
317
326
 
318
327
  {sqlDashboardsFetching && sqlDashboards.length === 0 && (
319
- <CommandLoadingGroup heading="SQL Dashboards" rows={3} />
328
+ <CommandLoadingGroup
329
+ heading={t("commandPalette.groupSqlDashboards")}
330
+ rows={3}
331
+ />
320
332
  )}
321
333
 
322
334
  {visibleSqlDashboards.length > 0 && (
323
- <CommandGroup heading="SQL Dashboards">
335
+ <CommandGroup heading={t("commandPalette.groupSqlDashboards")}>
324
336
  {visibleSqlDashboards.map((d) => (
325
337
  <CommandItem
326
338
  key={`sql-${d.id}`}
@@ -335,7 +347,7 @@ export function CommandPalette() {
335
347
  <span className="truncate">{d.name}</span>
336
348
  {d.hiddenAt ? (
337
349
  <span className="ms-2 rounded border border-border px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
338
- Hidden
350
+ {t("commandPalette.hidden")}
339
351
  </span>
340
352
  ) : null}
341
353
  </CommandItem>
@@ -344,11 +356,14 @@ export function CommandPalette() {
344
356
  )}
345
357
 
346
358
  {extensionsFetching && extensions.length === 0 && (
347
- <CommandLoadingGroup heading="Extensions" rows={3} />
359
+ <CommandLoadingGroup
360
+ heading={t("commandPalette.groupExtensions")}
361
+ rows={3}
362
+ />
348
363
  )}
349
364
 
350
365
  {extensions.length > 0 && (
351
- <CommandGroup heading="Extensions">
366
+ <CommandGroup heading={t("commandPalette.groupExtensions")}>
352
367
  {extensions.map((extension) => (
353
368
  <CommandItem
354
369
  key={`extension-${extension.id}`}
@@ -369,7 +384,7 @@ export function CommandPalette() {
369
384
  </CommandGroup>
370
385
  )}
371
386
 
372
- <CommandGroup heading="Dashboards">
387
+ <CommandGroup heading={t("commandPalette.groupDashboards")}>
373
388
  {dashboards.map((d) => (
374
389
  <CommandItem
375
390
  key={`dash-${d.id}`}
@@ -382,20 +397,20 @@ export function CommandPalette() {
382
397
  ))}
383
398
  </CommandGroup>
384
399
 
385
- <CommandGroup heading="Tools">
386
- {defaultTools.map((t) => (
400
+ <CommandGroup heading={t("commandPalette.groupTools")}>
401
+ {defaultTools.map((tool) => (
387
402
  <CommandItem
388
- key={`tool-${t.id}`}
389
- onSelect={() => go(t.href)}
390
- keywords={commandPaletteKeywords(t.name, "tool")}
403
+ key={`tool-${tool.id}`}
404
+ onSelect={() => go(tool.href)}
405
+ keywords={commandPaletteKeywords(t(tool.nameKey), "tool")}
391
406
  >
392
407
  <IconTool className="me-2 h-4 w-4 text-muted-foreground" />
393
- {t.name}
408
+ {t(tool.nameKey)}
394
409
  </CommandItem>
395
410
  ))}
396
411
  </CommandGroup>
397
412
 
398
- <CommandGroup heading="Appearance">
413
+ <CommandGroup heading={t("commandPalette.groupAppearance")}>
399
414
  <CommandItem
400
415
  onSelect={() => {
401
416
  const nextTheme = isDark ? "light" : "dark";
@@ -409,18 +424,20 @@ export function CommandPalette() {
409
424
  ) : (
410
425
  <IconMoon className="me-2 h-4 w-4 text-muted-foreground" />
411
426
  )}
412
- Toggle {isDark ? "light" : "dark"} mode
427
+ {isDark
428
+ ? t("commandPalette.toggleLightMode")
429
+ : t("commandPalette.toggleDarkMode")}
413
430
  </CommandItem>
414
431
  </CommandGroup>
415
432
 
416
- <CommandGroup heading="Help">
433
+ <CommandGroup heading={t("commandPalette.groupHelp")}>
417
434
  <CommandItem
418
435
  onSelect={() => {
419
436
  setOpen(false);
420
437
  setChangelogOpen(true);
421
438
  }}
422
439
  keywords={commandPaletteKeywords(
423
- "What's new",
440
+ t("commandPalette.whatsNew"),
424
441
  "changelog",
425
442
  "updates",
426
443
  "release notes",
@@ -428,16 +445,19 @@ export function CommandPalette() {
428
445
  )}
429
446
  >
430
447
  <IconHistory className="me-2 h-4 w-4 text-muted-foreground" />
431
- What's new
448
+ {t("commandPalette.whatsNew")}
432
449
  </CommandItem>
433
450
  </CommandGroup>
434
451
 
435
452
  {savedChartsFetching && savedCharts.length === 0 && (
436
- <CommandLoadingGroup heading="Saved Charts" rows={2} />
453
+ <CommandLoadingGroup
454
+ heading={t("commandPalette.groupSavedCharts")}
455
+ rows={2}
456
+ />
437
457
  )}
438
458
 
439
459
  {savedCharts.length > 0 && (
440
- <CommandGroup heading="Saved Charts">
460
+ <CommandGroup heading={t("commandPalette.groupSavedCharts")}>
441
461
  {savedCharts.map((c) => (
442
462
  <CommandItem
443
463
  key={`chart-${c.id}`}