@agent-native/core-corpus 0.209.1 → 0.209.2-nightly-20261016005836

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 (147) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +7 -0
  3. package/corpus/templates/analytics/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  4. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +48 -4
  5. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +16 -3
  6. package/corpus/templates/analytics/app/i18n/en-US.ts +10 -0
  7. package/corpus/templates/analytics/app/lib/personal-column-widths.ts +167 -0
  8. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +19 -2
  9. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +139 -38
  10. package/corpus/templates/analytics/changelog/2026-10-14-dashboards-open-in-view-mode-so-looking-around-never-changes.md +6 -0
  11. package/corpus/templates/assets/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  12. package/corpus/templates/brain/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  13. package/corpus/templates/calendar/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  14. package/corpus/templates/chat/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  15. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +11 -1
  16. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +21 -2
  17. package/corpus/templates/clips/.agents/skills/video-sharing/SKILL.md +11 -1
  18. package/corpus/templates/clips/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  19. package/corpus/templates/clips/AGENTS.md +1 -0
  20. package/corpus/templates/clips/actions/apply-rewind-extension.ts +6 -1
  21. package/corpus/templates/clips/actions/burn-recording-redactions.ts +11 -4
  22. package/corpus/templates/clips/actions/create-recording.ts +184 -42
  23. package/corpus/templates/clips/actions/delete-recording-permanent.ts +87 -5
  24. package/corpus/templates/clips/actions/finalize-recording.ts +435 -36
  25. package/corpus/templates/clips/actions/get-recording-app-activity.ts +78 -0
  26. package/corpus/templates/clips/actions/get-recording-player-data.ts +8 -1
  27. package/corpus/templates/clips/actions/get-video-storage-status.ts +17 -0
  28. package/corpus/templates/clips/actions/import-loom-recording.ts +391 -84
  29. package/corpus/templates/clips/actions/lib/direct-video.ts +2 -1
  30. package/corpus/templates/clips/actions/lib/ensure-recording-filmstrip.ts +10 -3
  31. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +41 -7
  32. package/corpus/templates/clips/actions/lib/loom-import-job.ts +298 -43
  33. package/corpus/templates/clips/actions/lib/loom-video.ts +15 -2
  34. package/corpus/templates/clips/actions/lib/recording-audio-source.ts +20 -0
  35. package/corpus/templates/clips/actions/lib/video-download-limits.ts +30 -8
  36. package/corpus/templates/clips/actions/list-recordings.ts +2 -0
  37. package/corpus/templates/clips/actions/request-transcript.ts +3 -0
  38. package/corpus/templates/clips/actions/save-recording-app-activity.ts +101 -0
  39. package/corpus/templates/clips/actions/set-thumbnail.ts +45 -4
  40. package/corpus/templates/clips/app/components/dictate/vocabulary-section.tsx +17 -6
  41. package/corpus/templates/clips/app/components/library/library-layout.tsx +4 -0
  42. package/corpus/templates/clips/app/components/library/recording-card.tsx +10 -2
  43. package/corpus/templates/clips/app/components/player/browser-diagnostics-panel.tsx +260 -37
  44. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +7 -2
  45. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +11 -23
  46. package/corpus/templates/clips/app/hooks/use-library.ts +1 -0
  47. package/corpus/templates/clips/app/hooks/use-video-storage-status.ts +113 -13
  48. package/corpus/templates/clips/app/i18n/en-US.ts +23 -0
  49. package/corpus/templates/clips/app/lib/local-recording-upload.ts +6 -0
  50. package/corpus/templates/clips/app/lib/upload-target.ts +42 -4
  51. package/corpus/templates/clips/app/routes/_app.dictate.tsx +1 -1
  52. package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +33 -2
  53. package/corpus/templates/clips/app/routes/import.tsx +12 -10
  54. package/corpus/templates/clips/app/routes/record.tsx +187 -52
  55. package/corpus/templates/clips/app/routes/share.$shareId.tsx +13 -2
  56. package/corpus/templates/clips/changelog/2026-10-14-clips-recognizes-existing-sign-ins-when-checking-access-to-f.md +6 -0
  57. package/corpus/templates/clips/changelog/2026-10-14-extension-recordings-of-agent-native-apps-include-what-the-a.md +6 -0
  58. package/corpus/templates/clips/changelog/2026-10-14-fixed-hosted-clips-uploads-failing-when-streamed-chunks-reac.md +6 -0
  59. package/corpus/templates/clips/changelog/2026-10-14-record-and-store-your-first-three-clips-before-connecting-yo.md +6 -0
  60. package/corpus/templates/clips/changelog/2026-10-14-record-three-clips-before-connecting-your-own-storage.md +6 -0
  61. package/corpus/templates/clips/changelog/2026-10-15-the-dictionary-scrolls-through-long-lists-and-stays-closed-d.md +6 -0
  62. package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +18 -1
  63. package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +4 -1
  64. package/corpus/templates/clips/chrome-extension/src/app-activity.ts +278 -0
  65. package/corpus/templates/clips/chrome-extension/src/background.ts +121 -13
  66. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +47 -0
  67. package/corpus/templates/clips/chrome-extension/src/options.html +2 -1
  68. package/corpus/templates/clips/chrome-extension/src/popup.ts +50 -11
  69. package/corpus/templates/clips/chrome-extension/src/recording-duration.ts +29 -0
  70. package/corpus/templates/clips/desktop/src/app.tsx +99 -43
  71. package/corpus/templates/clips/desktop/src/lib/clip-duration-limit.ts +44 -0
  72. package/corpus/templates/clips/desktop/src/lib/recorder.ts +8 -1
  73. package/corpus/templates/clips/scripts/takedown-hosted-recording.ts +340 -0
  74. package/corpus/templates/clips/server/db/schema.ts +46 -1
  75. package/corpus/templates/clips/server/lib/app-activity.ts +79 -0
  76. package/corpus/templates/clips/server/lib/ensure-recording-thumbnail.ts +24 -10
  77. package/corpus/templates/clips/server/lib/hosted-clip-allowance.ts +1203 -0
  78. package/corpus/templates/clips/server/lib/hosted-clip-disposable-domains.ts +1010 -0
  79. package/corpus/templates/clips/server/lib/initial-tool-names.ts +1 -0
  80. package/corpus/templates/clips/server/lib/public-agent-context.ts +133 -15
  81. package/corpus/templates/clips/server/lib/recording-failures.ts +54 -0
  82. package/corpus/templates/clips/server/lib/recording-media-cleanup.ts +157 -7
  83. package/corpus/templates/clips/server/lib/resumable-upload-cleanup.ts +40 -1
  84. package/corpus/templates/clips/server/lib/resumable-upload-provider.ts +8 -0
  85. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +309 -205
  86. package/corpus/templates/clips/server/lib/upload-lease.ts +330 -6
  87. package/corpus/templates/clips/server/lib/upload-recording-media.ts +161 -0
  88. package/corpus/templates/clips/server/lib/video-storage.ts +619 -2
  89. package/corpus/templates/clips/server/plugins/auth.ts +1 -0
  90. package/corpus/templates/clips/server/plugins/config.ts +1 -0
  91. package/corpus/templates/clips/server/plugins/db.ts +42 -0
  92. package/corpus/templates/clips/server/plugins/feature-flags.ts +5 -2
  93. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +5 -1
  94. package/corpus/templates/clips/server/routes/api/agent-app-activity.json.get.ts +134 -0
  95. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +6 -0
  96. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +8 -1
  97. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +50 -1
  98. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +35 -1
  99. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +317 -39
  100. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +45 -5
  101. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +15 -1
  102. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +26 -2
  103. package/corpus/templates/clips/shared/agent-context.ts +8 -0
  104. package/corpus/templates/clips/shared/app-activity.ts +325 -0
  105. package/corpus/templates/clips/shared/browser-diagnostics.ts +43 -0
  106. package/corpus/templates/clips/shared/feature-flags.ts +13 -1
  107. package/corpus/templates/clips/shared/recording-core.ts +6 -3
  108. package/corpus/templates/content/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  109. package/corpus/templates/crm/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  110. package/corpus/templates/design/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  111. package/corpus/templates/design/AGENTS.md +1 -0
  112. package/corpus/templates/design/CHANGELOG.md +6 -0
  113. package/corpus/templates/design/actions/delete-file.ts +5 -0
  114. package/corpus/templates/design/actions/get-motion-timeline.ts +33 -20
  115. package/corpus/templates/design/app/components/design/AddLocalhostScreenDialog.tsx +49 -9
  116. package/corpus/templates/design/app/components/design/EditPanel.tsx +10 -0
  117. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +1 -1
  118. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +2 -0
  119. package/corpus/templates/design/changelog/2026-10-15-empty-inspector-section-titles-stay-compact.md +6 -0
  120. package/corpus/templates/design/changelog/2026-10-15-external-design-agents-can-delete-screens-when-you-ask-them.md +6 -0
  121. package/corpus/templates/design/changelog/2026-10-15-fixed-local-screen-account-and-storage-setup-when-route-or-c.md +6 -0
  122. package/corpus/templates/design/e2e/rails.ts +54 -14
  123. package/corpus/templates/design/server/plugins/agent-chat.ts +1 -0
  124. package/corpus/templates/design/shared/motion-source-read.ts +1 -0
  125. package/corpus/templates/dispatch/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  126. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +2 -2
  127. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +31 -0
  128. package/corpus/templates/forms/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  129. package/corpus/templates/mail/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  130. package/corpus/templates/plan/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  131. package/corpus/templates/slides/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  132. package/corpus/templates/slides/app/components/editor/bullet-editing.ts +4 -0
  133. package/corpus/templates/slides/app/components/editor/in-place-text-session.browser-cases.json +983 -0
  134. package/corpus/templates/slides/app/components/editor/in-place-text-session.browser-host.ts +747 -0
  135. package/corpus/templates/slides/app/components/editor/in-place-text-session.ts +1273 -246
  136. package/corpus/templates/slides/app/components/editor/list-editing.ts +4 -1
  137. package/corpus/templates/slides/app/components/editor/rte-test-utils.ts +265 -0
  138. package/corpus/templates/slides/app/i18n/en-US.ts +2 -0
  139. package/corpus/templates/slides/app/pages/DeckEditor.tsx +33 -23
  140. package/corpus/templates/slides/app/pages/Index.tsx +4 -1
  141. package/corpus/templates/slides/changelog/2026-10-15-pasting-into-slide-text-keeps-lists-links-and-formatting.md +6 -0
  142. package/corpus/templates/slides/changelog/2026-10-15-select-all-and-typing-after-style-toggles-work-first-time.md +6 -0
  143. package/corpus/templates/slides/changelog/2026-10-15-slide-text-editing-matches-standard-rich-text-editors.md +6 -0
  144. package/corpus/templates/slides/changelog/2026-10-15-slides-keeps-your-original-request-visible-when-a-deck-canno.md +6 -0
  145. package/corpus/templates/slides/changelog/2026-10-15-slides-shows-fallback-prompts-immediately-and-persists-gener.md +1 -1
  146. package/corpus/templates/slides/interaction-parity.md +31 -0
  147. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -32,4 +32,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core-corpus/corpu
32
32
 
33
33
  ## Generated Counts
34
34
 
35
- - template files: 12398
35
+ - template files: 12477
@@ -62,6 +62,13 @@ current state first, then syncs open dashboard editors.
62
62
  Saved analyses follow the same undo model with `analysis_revisions`,
63
63
  `list-analysis-revisions`, and `restore-analysis-revision`.
64
64
 
65
+ Dashboards open in View mode. An editor switches the header toggle to Edit
66
+ before drags, renames, and column edits on the page save for everyone; the chat
67
+ context reports the current mode. A table column width dragged in View mode is
68
+ stored only in that person's browser, so "widen this column for me" is a drag
69
+ in View mode, not a dashboard change. `config.columns[].width` is the shared
70
+ width everyone sees; set it only when asked to change the dashboard itself.
71
+
65
72
  Never use `db-patch`, raw SQL, or settings-key edits to create or modify a
66
73
  dashboard config. Those bypass the dashboard action's access checks, SQL
67
74
  validation, collab sync, and verification. If a dashboard action fails
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","analytics-alert-jobs","analytics-rollup-backfill-jobs","auth","collab","config","core-routes","creative-context","dashboard-report-jobs","db","feature-flags","labs","onboarding","otel","session-replay-retention-jobs","transactional-emails","uptime-monitor-jobs"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -1221,6 +1221,13 @@ interface SqlChartProps {
1221
1221
  onCopyTableChange?: (handler: (() => Promise<void>) | null) => void;
1222
1222
  /** Passing this makes a table's columns editable; omit it for read-only tables. */
1223
1223
  onSaveColumns?: (columns: TableColumnConfig[]) => void;
1224
+ /** One viewer's own column widths, drawn over the panel's saved ones. */
1225
+ columnWidths?: Record<string, number>;
1226
+ /**
1227
+ * Passing this lets a viewer drag column widths for themselves; the drag
1228
+ * calls this instead of `onSaveColumns`, and null resets the column.
1229
+ */
1230
+ onColumnWidthChange?: (columnKey: string, width: number | null) => void;
1224
1231
  dashboardId?: string;
1225
1232
  extensionContext?: Record<string, unknown> | null;
1226
1233
  }
@@ -1238,6 +1245,8 @@ export function SqlChart({
1238
1245
  onExportCsvChange,
1239
1246
  onCopyTableChange,
1240
1247
  onSaveColumns,
1248
+ columnWidths,
1249
+ onColumnWidthChange,
1241
1250
  dashboardId,
1242
1251
  extensionContext,
1243
1252
  }: SqlChartProps) {
@@ -1469,6 +1478,8 @@ export function SqlChart({
1469
1478
  onExportCsvChange={onExportCsvChange}
1470
1479
  onCopyTableChange={onCopyTableChange}
1471
1480
  onSaveColumns={onSaveColumns}
1481
+ columnWidths={columnWidths}
1482
+ onColumnWidthChange={onColumnWidthChange}
1472
1483
  />,
1473
1484
  );
1474
1485
  }
@@ -1936,20 +1947,26 @@ function TableRenderer({
1936
1947
  onExportCsvChange,
1937
1948
  onCopyTableChange,
1938
1949
  onSaveColumns,
1950
+ columnWidths,
1951
+ onColumnWidthChange,
1939
1952
  }: {
1940
1953
  rows: Record<string, unknown>[];
1941
1954
  panel: SqlPanel;
1942
1955
  onExportCsvChange?: (handler: (() => void) | null) => void;
1943
1956
  onCopyTableChange?: (handler: (() => Promise<void>) | null) => void;
1944
1957
  onSaveColumns?: (columns: TableColumnConfig[]) => void;
1958
+ columnWidths?: Record<string, number>;
1959
+ onColumnWidthChange?: (columnKey: string, width: number | null) => void;
1945
1960
  }) {
1946
1961
  const t = useT();
1947
1962
  const config = panel.config;
1948
1963
  const sortable = config?.sortable !== false;
1949
1964
  const editable = onSaveColumns !== undefined;
1965
+ const personalResize = onColumnWidthChange !== undefined;
1966
+ const resizable = editable || personalResize;
1950
1967
  const rowKeys = useMemo(() => new Set(Object.keys(rows[0] ?? {})), [rows]);
1951
1968
 
1952
- const columns = useMemo<TableColumnConfig[]>(() => {
1969
+ const savedColumns = useMemo<TableColumnConfig[]>(() => {
1953
1970
  if (config?.columns?.length) {
1954
1971
  const configured = config.columns.filter(
1955
1972
  (c) => !c.hidden && rowKeys.has(c.key),
@@ -1959,6 +1976,18 @@ function TableRenderer({
1959
1976
  return Object.keys(rows[0]).map((key) => ({ key }));
1960
1977
  }, [config?.columns, rows, rowKeys]);
1961
1978
 
1979
+ // A viewer's own widths are drawn over the saved ones but never saved:
1980
+ // edits start from `editableColumns`, which reads the panel config.
1981
+ const columns = useMemo<TableColumnConfig[]>(
1982
+ () =>
1983
+ columnWidths
1984
+ ? savedColumns.map((c) =>
1985
+ columnWidths[c.key] ? { ...c, width: columnWidths[c.key] } : c,
1986
+ )
1987
+ : savedColumns,
1988
+ [savedColumns, columnWidths],
1989
+ );
1990
+
1962
1991
  // Edits start from the persisted list, hidden entries included, so a change
1963
1992
  // never drops a key the agent configured. Visible keys missing from it (the
1964
1993
  // all-keys fallback above) are appended so every visible column is editable.
@@ -2060,7 +2089,9 @@ function TableRenderer({
2060
2089
  MIN_TABLE_COLUMN_WIDTH,
2061
2090
  Math.round(resize.startWidth + event.clientX - resize.startX),
2062
2091
  );
2063
- if (width !== resize.startWidth) updateColumn(resize.key, { width });
2092
+ if (width === resize.startWidth) return;
2093
+ if (personalResize) onColumnWidthChange(resize.key, width);
2094
+ else updateColumn(resize.key, { width });
2064
2095
  };
2065
2096
 
2066
2097
  const renderHeaderCell = (col: TableColumnConfig) => {
@@ -2112,7 +2143,7 @@ function TableRenderer({
2112
2143
  }
2113
2144
  className={`text-left align-bottom py-1.5 px-2 font-medium text-muted-foreground ${
2114
2145
  sortable ? "cursor-pointer select-none hover:text-foreground" : ""
2115
- } ${editable ? "relative" : ""} ${width ? "overflow-hidden text-ellipsis whitespace-nowrap" : ""}`}
2146
+ } ${resizable ? "relative" : ""} ${width ? "overflow-hidden text-ellipsis whitespace-nowrap" : ""}`}
2116
2147
  onClick={(event) => {
2117
2148
  // The second click of a double-click renames the column and must not
2118
2149
  // toggle the sort a second time.
@@ -2165,18 +2196,31 @@ function TableRenderer({
2165
2196
  </>
2166
2197
  )}
2167
2198
  </span>
2168
- {editable &&
2199
+ {resizable &&
2169
2200
  !isRenaming &&
2170
2201
  col.key !== columns[columns.length - 1]?.key ? (
2171
2202
  <span
2172
2203
  role="separator"
2173
2204
  aria-orientation="vertical"
2174
2205
  aria-label={t("sqlDashboard.columnResize")}
2206
+ title={
2207
+ personalResize
2208
+ ? t("dashboardMode.columnResizePersonal")
2209
+ : t("dashboardMode.columnResizeShared")
2210
+ }
2175
2211
  className="absolute inset-y-0 end-0 w-1.5 cursor-col-resize hover:bg-border"
2176
2212
  onPointerDown={(event) => handleResizeStart(col.key, event)}
2177
2213
  onPointerMove={handleResizeMove}
2178
2214
  onPointerUp={handleResizeEnd}
2179
2215
  onClick={(event) => event.stopPropagation()}
2216
+ onDoubleClick={
2217
+ personalResize
2218
+ ? (event) => {
2219
+ event.stopPropagation();
2220
+ onColumnWidthChange(col.key, null);
2221
+ }
2222
+ : undefined
2223
+ }
2180
2224
  />
2181
2225
  ) : null}
2182
2226
  </th>
@@ -26,6 +26,7 @@ export interface DashboardChatContextArgs {
26
26
  title?: string | null;
27
27
  panelCount?: number;
28
28
  canEdit?: boolean;
29
+ editing?: boolean;
29
30
  }
30
31
 
31
32
  export interface DashboardPanelChatContextArgs {
@@ -56,6 +57,7 @@ function dashboardContext(
56
57
  title: string;
57
58
  panelCount?: number;
58
59
  canEdit?: boolean;
60
+ editing?: boolean;
59
61
  },
60
62
  ): string {
61
63
  const lines = [
@@ -69,6 +71,13 @@ function dashboardContext(
69
71
  if (typeof args.canEdit === "boolean") {
70
72
  lines.push(`User can edit: ${args.canEdit ? "yes" : "no"}`);
71
73
  }
74
+ if (args.canEdit && typeof args.editing === "boolean") {
75
+ lines.push(
76
+ args.editing
77
+ ? "Dashboard mode: edit (the user's own drags and renames save for everyone)"
78
+ : "Dashboard mode: view (moving or editing panels, renaming, and undo on the page wait for edit mode, and the user's column drags stay in their browser; saving a view, restoring history, archiving, deleting, and adding a first panel still work and apply to everyone)",
79
+ );
80
+ }
72
81
  if (typeof window !== "undefined") {
73
82
  lines.push(
74
83
  `Current URL: ${window.location.pathname}${window.location.search}`,
@@ -115,7 +124,7 @@ function isAgentSidebarOpenInDocument(): boolean {
115
124
  export function useDashboardChatContext(
116
125
  args: DashboardChatContextArgs,
117
126
  ): DashboardChatContextResult {
118
- const { id, kind, title, panelCount, canEdit } = args;
127
+ const { id, kind, title, panelCount, canEdit, editing } = args;
119
128
  const [isAgentSidebarOpen, setIsAgentSidebarOpen] = useState(
120
129
  isAgentSidebarOpenInDocument,
121
130
  );
@@ -249,13 +258,14 @@ export function useDashboardChatContext(
249
258
  title: displayTitle,
250
259
  panelCount,
251
260
  canEdit,
261
+ editing,
252
262
  }),
253
263
  openSidebar: false,
254
264
  focus: false,
255
265
  });
256
266
  }, CONTEXT_PUBLISH_DELAY_MS);
257
267
  return () => clearTimeout(timer);
258
- }, [canEdit, id, kind, panelCount, title]);
268
+ }, [canEdit, editing, id, kind, panelCount, title]);
259
269
 
260
270
  useEffect(() => {
261
271
  if (!id || hasSelectedPanel) return;
@@ -268,6 +278,9 @@ export function useDashboardChatContext(
268
278
  title: displayTitle,
269
279
  panelCount,
270
280
  canEdit,
281
+ ...(canEdit && typeof editing === "boolean"
282
+ ? { mode: editing ? "edit" : "view" }
283
+ : {}),
271
284
  [SELECTED_OBJECT_SOURCE_FIELD]: TAB_ID,
272
285
  };
273
286
  const timer = setTimeout(() => {
@@ -279,7 +292,7 @@ export function useDashboardChatContext(
279
292
  publishedSelectionRef.current = selection;
280
293
  }, CONTEXT_PUBLISH_DELAY_MS);
281
294
  return () => clearTimeout(timer);
282
- }, [canEdit, hasSelectedPanel, id, kind, panelCount, title]);
295
+ }, [canEdit, editing, hasSelectedPanel, id, kind, panelCount, title]);
283
296
 
284
297
  useEffect(() => {
285
298
  if (!id) return;
@@ -167,6 +167,16 @@ export default {
167
167
  templateCatalog: "Catalog",
168
168
  semanticLayer: "Semantic Layer",
169
169
  },
170
+ dashboardMode: {
171
+ label: "Dashboard mode",
172
+ view: "View",
173
+ edit: "Edit",
174
+ viewHint:
175
+ "Look without changing anything. Column widths you drag stay yours.",
176
+ editHint: "Changes save for everyone",
177
+ columnResizePersonal: "Drag to resize for you only. Double-click to reset.",
178
+ columnResizeShared: "Drag to resize for everyone",
179
+ },
170
180
  dashboardHeader: {
171
181
  updated: "Updated {{date}}",
172
182
  created: "Created {{date}}",
@@ -0,0 +1,167 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+
3
+ import type { TableColumnConfig } from "@/pages/adhoc/sql-dashboard/types";
4
+
5
+ const STORAGE_PREFIX = "analytics-column-widths:v1:";
6
+
7
+ /**
8
+ * A width someone dragged while viewing a dashboard, kept in their browser.
9
+ * `base` is the dashboard's own width for the column at the time (null when it
10
+ * had none): once the dashboard's width changes, the personal one is dropped,
11
+ * so a later edit for everyone is never hidden behind an old personal drag.
12
+ */
13
+ export interface PersonalColumnWidth {
14
+ width: number;
15
+ base: number | null;
16
+ }
17
+
18
+ export type PersonalColumnWidths = Record<string, PersonalColumnWidth>;
19
+
20
+ export function personalColumnWidthsKey(
21
+ dashboardId: string,
22
+ panelId: string,
23
+ ): string {
24
+ return `${STORAGE_PREFIX}${dashboardId}:${panelId}`;
25
+ }
26
+
27
+ function isPersonalColumnWidth(value: unknown): value is PersonalColumnWidth {
28
+ if (!value || typeof value !== "object") return false;
29
+ const entry = value as Record<string, unknown>;
30
+ return (
31
+ typeof entry.width === "number" &&
32
+ Number.isFinite(entry.width) &&
33
+ entry.width > 0 &&
34
+ (entry.base === null ||
35
+ (typeof entry.base === "number" && Number.isFinite(entry.base)))
36
+ );
37
+ }
38
+
39
+ /** Parses a stored value, keeping only well-formed entries. */
40
+ export function parsePersonalColumnWidths(
41
+ raw: string | null,
42
+ ): PersonalColumnWidths {
43
+ if (!raw) return {};
44
+ let parsed: unknown;
45
+ try {
46
+ parsed = JSON.parse(raw);
47
+ } catch {
48
+ // coercion-ok: a corrupt entry means no personal widths; the dashboard's own widths apply.
49
+ return {};
50
+ }
51
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
52
+ return {};
53
+ }
54
+ const widths: PersonalColumnWidths = {};
55
+ for (const [key, value] of Object.entries(parsed)) {
56
+ if (isPersonalColumnWidth(value)) widths[key] = value;
57
+ }
58
+ return widths;
59
+ }
60
+
61
+ /** The stored entries still based on the dashboard's current widths. */
62
+ export function currentPersonalColumnWidths(
63
+ stored: PersonalColumnWidths,
64
+ columns: TableColumnConfig[] | undefined,
65
+ ): PersonalColumnWidths {
66
+ const sharedWidth = new Map(
67
+ (columns ?? []).map((column) => [column.key, column.width ?? null]),
68
+ );
69
+ const current: PersonalColumnWidths = {};
70
+ for (const [key, entry] of Object.entries(stored)) {
71
+ if ((sharedWidth.get(key) ?? null) === entry.base) current[key] = entry;
72
+ }
73
+ return current;
74
+ }
75
+
76
+ /** The personal widths that apply against the dashboard's current widths. */
77
+ export function activePersonalColumnWidths(
78
+ stored: PersonalColumnWidths,
79
+ columns: TableColumnConfig[] | undefined,
80
+ ): Record<string, number> {
81
+ const active: Record<string, number> = {};
82
+ for (const [key, entry] of Object.entries(
83
+ currentPersonalColumnWidths(stored, columns),
84
+ )) {
85
+ active[key] = entry.width;
86
+ }
87
+ return active;
88
+ }
89
+
90
+ function readStoredWidths(key: string | null): PersonalColumnWidths {
91
+ if (!key || typeof window === "undefined") return {};
92
+ let raw: string | null;
93
+ try {
94
+ raw = window.localStorage.getItem(key);
95
+ } catch {
96
+ // coercion-ok: storage blocked by the browser; widths then last for this visit only.
97
+ return {};
98
+ }
99
+ return parsePersonalColumnWidths(raw);
100
+ }
101
+
102
+ function writeStoredWidths(key: string, widths: PersonalColumnWidths): void {
103
+ try {
104
+ if (Object.keys(widths).length === 0) window.localStorage.removeItem(key);
105
+ else window.localStorage.setItem(key, JSON.stringify(widths));
106
+ } catch {
107
+ // coercion-ok: storage blocked or full; the width still applies until the page reloads.
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Column widths one person dragged on one dashboard table. They live in this
113
+ * browser only and never change the dashboard for anyone else.
114
+ */
115
+ export function usePersonalColumnWidths(
116
+ dashboardId: string | undefined,
117
+ panelId: string,
118
+ columns: TableColumnConfig[] | undefined,
119
+ ): {
120
+ widths: Record<string, number>;
121
+ setWidth: (columnKey: string, width: number | null) => void;
122
+ } {
123
+ const key = dashboardId
124
+ ? personalColumnWidthsKey(dashboardId, panelId)
125
+ : null;
126
+ const [stored, setStored] = useState<PersonalColumnWidths>(() =>
127
+ readStoredWidths(key),
128
+ );
129
+ useEffect(() => {
130
+ setStored(readStoredWidths(key));
131
+ }, [key]);
132
+ // Hiding a stale width is not enough: kept in storage, it would come back
133
+ // if the dashboard's width returned to its base and hide that newer change.
134
+ useEffect(() => {
135
+ if (!key) return;
136
+ setStored((current) => {
137
+ const kept = currentPersonalColumnWidths(current, columns);
138
+ if (Object.keys(kept).length === Object.keys(current).length) {
139
+ return current;
140
+ }
141
+ writeStoredWidths(key, kept);
142
+ return kept;
143
+ });
144
+ }, [columns, key]);
145
+
146
+ const setWidth = useCallback(
147
+ (columnKey: string, width: number | null) => {
148
+ if (!key) return;
149
+ const base =
150
+ columns?.find((column) => column.key === columnKey)?.width ?? null;
151
+ setStored((current) => {
152
+ const next = { ...current };
153
+ if (width === null) delete next[columnKey];
154
+ else next[columnKey] = { width, base };
155
+ writeStoredWidths(key, next);
156
+ return next;
157
+ });
158
+ },
159
+ [columns, key],
160
+ );
161
+
162
+ const widths = useMemo(
163
+ () => activePersonalColumnWidths(stored, columns),
164
+ [stored, columns],
165
+ );
166
+ return { widths, setWidth };
167
+ }
@@ -59,6 +59,7 @@ import {
59
59
  } from "@/components/ui/tooltip";
60
60
  import type { SelectDashboardPanelOptions } from "@/hooks/use-dashboard-chat-context";
61
61
  import { buildCustomBlockPromotionRequest } from "@/lib/custom-block-promotion";
62
+ import { usePersonalColumnWidths } from "@/lib/personal-column-widths";
62
63
  import { cn } from "@/lib/utils";
63
64
 
64
65
  import { timeRangeDays } from "./pivot";
@@ -164,6 +165,22 @@ export function SqlChartCard({
164
165
  const exportToGoogleSheets = useActionMutation(
165
166
  "export-dashboard-panel-to-google-sheet",
166
167
  );
168
+ // While viewing, dragging a column edge sizes it for this person only. A
169
+ // table offers rename, reorder, and format whenever it gets onSaveColumns,
170
+ // so only edit mode passes it on.
171
+ const personalColumnWidths = usePersonalColumnWidths(
172
+ dashboardId,
173
+ panel.id,
174
+ panel.config?.columns,
175
+ );
176
+ const tableColumnProps = editable
177
+ ? { onSaveColumns }
178
+ : dashboardId && panel.chartType === "table"
179
+ ? {
180
+ columnWidths: personalColumnWidths.widths,
181
+ onColumnWidthChange: personalColumnWidths.setWidth,
182
+ }
183
+ : {};
167
184
 
168
185
  const [confirmOpen, setConfirmOpen] = useState(false);
169
186
  const [menuOpen, setMenuOpen] = useState(false);
@@ -811,7 +828,7 @@ export function SqlChartCard({
811
828
  dashboardId={dashboardId}
812
829
  onExportCsvChange={handleExportCsvChange}
813
830
  onCopyTableChange={handleCopyTableChange}
814
- onSaveColumns={onSaveColumns}
831
+ {...tableColumnProps}
815
832
  extensionContext={extensionContext}
816
833
  />
817
834
  </CardContent>
@@ -832,7 +849,7 @@ export function SqlChartCard({
832
849
  reportScreenshot={reportScreenshot}
833
850
  refreshToken={refreshToken}
834
851
  onRefreshRequested={handleRefresh}
835
- onSaveColumns={onSaveColumns}
852
+ {...tableColumnProps}
836
853
  dashboardId={dashboardId}
837
854
  extensionContext={extensionContext}
838
855
  />