@agent-native/core 0.149.1 → 0.149.2

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 (115) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +2 -0
  3. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +150 -46
  4. package/corpus/templates/analytics/app/global.css +1 -1
  5. package/corpus/templates/analytics/app/i18n-data.ts +37 -0
  6. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +1 -1
  7. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +32 -0
  8. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -0
  9. package/corpus/templates/analytics/changelog/2026-08-10-dashboard-tables-can-be-copied-and-sorted-by-multiple-column.md +6 -0
  10. package/corpus/templates/analytics/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  11. package/corpus/templates/assets/app/global.css +1 -1
  12. package/corpus/templates/assets/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  13. package/corpus/templates/brain/app/global.css +1 -1
  14. package/corpus/templates/brain/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  15. package/corpus/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  16. package/corpus/templates/clips/changelog/2026-08-10-clips-desktop-sign-in-now-starts-with-magic-links-and-google.md +6 -0
  17. package/corpus/templates/clips/chrome-extension/src/overlay.css +1 -0
  18. package/corpus/templates/clips/desktop/src/app.tsx +274 -137
  19. package/corpus/templates/clips/desktop/src/styles.css +35 -0
  20. package/corpus/templates/content/app/global.css +1 -0
  21. package/corpus/templates/crm/app/global.css +1 -1
  22. package/corpus/templates/crm/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  23. package/corpus/templates/dispatch/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  24. package/corpus/templates/factory/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  25. package/corpus/templates/forms/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  26. package/corpus/templates/plan/app/global.css +1 -0
  27. package/corpus/templates/plan/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  28. package/corpus/templates/plan/server/ui-plan-html.ts +4 -1
  29. package/corpus/templates/slides/AGENTS.md +11 -10
  30. package/corpus/templates/slides/actions/add-deck.ts +2 -0
  31. package/corpus/templates/slides/actions/add-slide.ts +15 -1
  32. package/corpus/templates/slides/actions/create-deck.ts +18 -6
  33. package/corpus/templates/slides/actions/get-deck.ts +29 -1
  34. package/corpus/templates/slides/actions/patch-deck.ts +122 -9
  35. package/corpus/templates/slides/actions/save-deck.ts +24 -1
  36. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +134 -618
  37. package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +69 -60
  38. package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -1
  39. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +13 -1
  40. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +56 -174
  41. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +239 -116
  42. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +3 -3
  43. package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +1 -1
  44. package/corpus/templates/slides/app/components/layout/Layout.tsx +34 -35
  45. package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +2 -5
  46. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +45 -40
  47. package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +63 -16
  48. package/corpus/templates/slides/app/hooks/use-generating-slide-preview.ts +25 -3
  49. package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
  50. package/corpus/templates/slides/app/lib/create-deck-generation.ts +5 -4
  51. package/corpus/templates/slides/app/lib/recent-references.ts +23 -1
  52. package/corpus/templates/slides/app/lib/slide-animation-elements.ts +41 -3
  53. package/corpus/templates/slides/app/pages/DeckEditor.tsx +4 -27
  54. package/corpus/templates/slides/app/pages/Index.tsx +240 -89
  55. package/corpus/templates/slides/changelog/2026-08-10-ai-editing-avatar-badge.md +6 -0
  56. package/corpus/templates/slides/changelog/2026-08-10-generated-decks-keep-a-clear-human-readable-title.md +6 -0
  57. package/corpus/templates/slides/changelog/2026-08-10-new-deck-creation-now-shows-progress-opens-the-editor-direct.md +6 -0
  58. package/corpus/templates/slides/changelog/2026-08-10-presentation-reveals-now-follow-valid-targets.md +6 -0
  59. package/corpus/templates/slides/changelog/2026-08-10-slides-editing-controls-are-consolidated-into-one-focused-me.md +6 -0
  60. package/corpus/templates/slides/changelog/2026-08-10-slides-now-verify-and-restyle-every-imported-slide-in-one-pa.md +6 -0
  61. package/corpus/templates/slides/changelog/2026-08-10-slides-only-suggests-a-google-connection-for-relevant-reques.md +6 -0
  62. package/corpus/templates/slides/changelog/2026-08-10-the-google-slides-import-option-is-now-labeled-slides.md +6 -0
  63. package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
  64. package/corpus/templates/slides/server/plugins/agent-chat.ts +5 -3
  65. package/corpus/templates/slides/shared/api.ts +2 -1
  66. package/corpus/templates/slides/shared/deck-title.ts +135 -0
  67. package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +4 -1
  68. package/dist/agent/engine/ai-sdk-engine.js +1 -1
  69. package/dist/agent/production-agent.d.ts +21 -1
  70. package/dist/agent/production-agent.js +129 -10
  71. package/dist/agent/tool-approval-store.d.ts +18 -0
  72. package/dist/agent/tool-approval-store.js +122 -0
  73. package/dist/agent/types.d.ts +1 -1
  74. package/dist/cli/code-agent-executor.js +11 -2
  75. package/dist/client/AgentPanel.js +31 -6
  76. package/dist/client/AssistantChat.d.ts +3 -2
  77. package/dist/client/AssistantChat.js +7 -20
  78. package/dist/client/agent-chat/index.d.ts +1 -1
  79. package/dist/client/agent-chat-adapter.js +11 -0
  80. package/dist/client/agent-chat.d.ts +1 -1
  81. package/dist/client/chat/markdown-renderer.js +14 -2
  82. package/dist/client/chat/message-components.d.ts +6 -2
  83. package/dist/client/chat/message-components.js +25 -47
  84. package/dist/client/chat/tool-call-display.d.ts +3 -5
  85. package/dist/client/chat/tool-call-display.js +7 -212
  86. package/dist/client/chat-first/apps-rail.js +3 -3
  87. package/dist/client/chat-first/browser-pane.js +4 -1
  88. package/dist/client/chat-first/chat-history.d.ts +3 -1
  89. package/dist/client/chat-first/chat-history.js +2 -2
  90. package/dist/client/chat-first/surface-tabs.d.ts +1 -1
  91. package/dist/client/chat-first/surface-tabs.js +52 -41
  92. package/dist/client/chat-first/types.d.ts +3 -0
  93. package/dist/client/chat-first.d.ts +10 -0
  94. package/dist/client/chat-first.js +37 -2
  95. package/dist/client/components/ui/message-scroller.js +4 -1
  96. package/dist/client/i18n.js +3 -1
  97. package/dist/client/resources/McpConnectionSuggestion.js +9 -1
  98. package/dist/client/sse-event-processor.js +10 -0
  99. package/dist/client/use-chat-models.js +156 -80
  100. package/dist/collab/struct-routes.d.ts +1 -1
  101. package/dist/integrations/webhook-handler.js +1 -1
  102. package/dist/observability/routes.d.ts +1 -1
  103. package/dist/provider-api/actions/custom-provider-registration.d.ts +1 -1
  104. package/dist/resources/handlers.d.ts +1 -1
  105. package/dist/secrets/routes.d.ts +9 -9
  106. package/dist/server/auth.js +262 -31
  107. package/dist/server/onboarding-html.js +56 -9
  108. package/dist/shared/reasoning-effort.d.ts +8 -8
  109. package/dist/shared/reasoning-effort.js +10 -10
  110. package/dist/shared/streaming-text-smoothing.js +41 -11
  111. package/dist/styles/agent-native.css +19 -32
  112. package/dist/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
  113. package/docs/content/template-chat.mdx +1 -1
  114. package/package.json +2 -2
  115. package/src/templates/chat/changelog/2026-08-10-full-page-chat-now-uses-the-available-width-up-to-1000px-for.md +6 -0
@@ -3,9 +3,12 @@ import { PromptComposer } from "@agent-native/core/client/composer";
3
3
  import { ensureEmbedAuthFetchInterceptor } from "@agent-native/core/client/host";
4
4
  import { useT } from "@agent-native/core/client/i18n";
5
5
  import {
6
+ IconArrowLeft,
6
7
  IconBrandGoogle,
7
8
  IconFileTypePdf,
9
+ IconLoader2,
8
10
  IconPresentation,
11
+ IconUpload,
9
12
  } from "@tabler/icons-react";
10
13
  import { useState, useEffect, useCallback, useRef } from "react";
11
14
  import { createPortal } from "react-dom";
@@ -86,7 +89,7 @@ interface PromptPopoverProps {
86
89
  placeholder?: string;
87
90
  onSkip?: () => void;
88
91
  skipLabel?: string;
89
- onSubmit: (prompt: string, files: UploadedFile[]) => void;
92
+ onSubmit: (prompt: string, files: UploadedFile[]) => void | Promise<void>;
90
93
  loading?: boolean;
91
94
  anchorRef?: React.RefObject<HTMLElement | null>;
92
95
  centered?: boolean;
@@ -125,10 +128,14 @@ export default function PromptPopover({
125
128
  }: PromptPopoverProps) {
126
129
  const t = useT();
127
130
  const [uploading, setUploading] = useState(false);
131
+ const [submitting, setSubmitting] = useState(false);
128
132
  const [promptText, setPromptText] = useState("");
129
133
  const [googleDocContext, setGoogleDocContext] = useState("");
130
134
  const [googleSlidesUrl, setGoogleSlidesUrl] = useState("");
131
- const [googleSlidesInputOpen, setGoogleSlidesInputOpen] = useState(false);
135
+ const [importMode, setImportMode] = useState<PromptImportSource | null>(null);
136
+ const [selectedImportFile, setSelectedImportFile] = useState<File | null>(
137
+ null,
138
+ );
132
139
  const [importingSource, setImportingSource] =
133
140
  useState<PromptImportSource | null>(null);
134
141
  const pdfInputRef = useRef<HTMLInputElement>(null);
@@ -216,16 +223,20 @@ export default function PromptPopover({
216
223
  if (files.length > 0 && onBeforeUpload?.(enrichedText, files) === false) {
217
224
  return;
218
225
  }
226
+ setSubmitting(true);
219
227
  try {
220
228
  const uploaded = await uploadFiles(files);
221
- onSubmit(enrichedText, uploaded);
229
+ await onSubmit(enrichedText, uploaded);
230
+ setSubmitting(false);
222
231
  } catch (error) {
232
+ setSubmitting(false);
223
233
  toast.error(t("raw.uploadFailed"), {
224
234
  description:
225
235
  error instanceof Error
226
236
  ? error.message
227
237
  : t("raw.uploadAttachedFailed"),
228
238
  });
239
+ throw error;
229
240
  }
230
241
  },
231
242
  [googleDocContext, onBeforeUpload, onSubmit, uploadFiles, t],
@@ -255,11 +266,25 @@ export default function PromptPopover({
255
266
  const handleFileImport = useCallback(
256
267
  (kind: "pdf" | "pptx", file: File | undefined) => {
257
268
  if (!file) return;
269
+ setSelectedImportFile(file);
258
270
  void runImport({ kind, files: [file] });
259
271
  },
260
272
  [runImport],
261
273
  );
262
274
 
275
+ const chooseImportMode = useCallback((kind: PromptImportSource) => {
276
+ setImportMode(kind);
277
+ setSelectedImportFile(null);
278
+ setGoogleSlidesUrl("");
279
+ }, []);
280
+
281
+ const returnToPrompt = useCallback(() => {
282
+ if (importingSource) return;
283
+ setImportMode(null);
284
+ setSelectedImportFile(null);
285
+ setGoogleSlidesUrl("");
286
+ }, [importingSource]);
287
+
263
288
  const handleGoogleSlidesImport = useCallback(() => {
264
289
  const url = googleSlidesUrl.trim();
265
290
  if (!url) return;
@@ -271,13 +296,29 @@ export default function PromptPopover({
271
296
  setPromptText("");
272
297
  setGoogleDocContext("");
273
298
  setGoogleSlidesUrl("");
274
- setGoogleSlidesInputOpen(false);
299
+ setImportMode(null);
300
+ setSelectedImportFile(null);
275
301
  setImportingSource(null);
302
+ setSubmitting(false);
276
303
  }
277
304
  }, [open]);
278
305
 
279
306
  if (!open) return null;
280
307
 
308
+ const importEnabled = Boolean(onImport && importFromLabel);
309
+ const importFromCopy = importFromLabel?.trim().toLowerCase().startsWith("or ")
310
+ ? importFromLabel.trim()
311
+ : importFromLabel
312
+ ? `or ${importFromLabel.trim().toLowerCase()}`
313
+ : "";
314
+ const importModeLabel =
315
+ importMode === "google-slides"
316
+ ? t("home.googleSlidesReferenceTitle")
317
+ : importMode === "pdf"
318
+ ? "PDF"
319
+ : "PPT";
320
+ const importInputRef = importMode === "pdf" ? pdfInputRef : pptxInputRef;
321
+
281
322
  const popover = (
282
323
  <>
283
324
  {centered && (
@@ -296,7 +337,7 @@ export default function PromptPopover({
296
337
  >
297
338
  <div className="flex items-center justify-between gap-3 px-4 pb-2.5 pt-3.5">
298
339
  <span className="text-sm font-medium text-foreground">{title}</span>
299
- {onSkip && (
340
+ {onSkip && !importMode && !submitting && (
300
341
  <button
301
342
  type="button"
302
343
  onClick={() => {
@@ -310,132 +351,214 @@ export default function PromptPopover({
310
351
  )}
311
352
  </div>
312
353
 
313
- <div className="px-2.5 pb-2.5">
314
- <PromptComposer
315
- autoFocus
316
- attachmentsEnabled
317
- maxDocumentAttachmentBytes={MAX_REFERENCE_FILE_BYTES}
318
- documentAttachmentLimitLabel="Slides reference files"
319
- disabled={loading || uploading}
320
- placeholder={placeholder}
321
- onSubmit={handleSubmit}
322
- onTextChange={setPromptText}
323
- draftScope={draftScope}
324
- initialText={initialText}
325
- initialTextKey={initialTextKey}
354
+ {importEnabled && (
355
+ <input
356
+ ref={pdfInputRef}
357
+ type="file"
358
+ accept=".pdf,application/pdf"
359
+ className="sr-only"
360
+ aria-label={t("editorToolbar.importFile")}
361
+ onChange={(event) => {
362
+ handleFileImport("pdf", event.target.files?.[0]);
363
+ event.target.value = "";
364
+ }}
326
365
  />
327
- </div>
366
+ )}
367
+ {importEnabled && (
368
+ <input
369
+ ref={pptxInputRef}
370
+ type="file"
371
+ accept=".pptx,application/vnd.openxmlformats-officedocument.presentationml.presentation"
372
+ className="sr-only"
373
+ aria-label={t("editorToolbar.importFile")}
374
+ onChange={(event) => {
375
+ handleFileImport("pptx", event.target.files?.[0]);
376
+ event.target.value = "";
377
+ }}
378
+ />
379
+ )}
328
380
 
329
- {onImport && importFromLabel && (
330
- <div className="border-t border-border/60 px-4 pb-3 pt-2.5">
331
- <div className="flex flex-wrap items-center gap-1.5">
332
- <span className="mr-1 text-xs text-muted-foreground">
333
- {importFromLabel}
334
- </span>
335
- <input
336
- ref={pdfInputRef}
337
- type="file"
338
- accept=".pdf,application/pdf"
339
- className="sr-only"
340
- aria-label={t("editorToolbar.importFile")}
341
- onChange={(event) => {
342
- handleFileImport("pdf", event.target.files?.[0]);
343
- event.target.value = "";
344
- }}
345
- />
346
- <input
347
- ref={pptxInputRef}
348
- type="file"
349
- accept=".pptx,application/vnd.openxmlformats-officedocument.presentationml.presentation"
350
- className="sr-only"
351
- aria-label={t("editorToolbar.importFile")}
352
- onChange={(event) => {
353
- handleFileImport("pptx", event.target.files?.[0]);
354
- event.target.value = "";
355
- }}
381
+ <div className={importEnabled ? "grid" : undefined}>
382
+ <div
383
+ className={[
384
+ importEnabled ? "col-start-1 row-start-1" : "",
385
+ importMode ? "invisible pointer-events-none" : "",
386
+ ]
387
+ .filter(Boolean)
388
+ .join(" ")}
389
+ aria-hidden={importMode ? true : undefined}
390
+ >
391
+ <div className="px-2.5 pb-2.5">
392
+ <PromptComposer
393
+ autoFocus
394
+ attachmentsEnabled
395
+ maxDocumentAttachmentBytes={MAX_REFERENCE_FILE_BYTES}
396
+ documentAttachmentLimitLabel="Slides reference files"
397
+ disabled={
398
+ loading || uploading || submitting || Boolean(importMode)
399
+ }
400
+ placeholder={placeholder}
401
+ onSubmit={handleSubmit}
402
+ onTextChange={setPromptText}
403
+ draftScope={draftScope}
404
+ initialText={initialText}
405
+ initialTextKey={initialTextKey}
356
406
  />
357
- <Button
358
- type="button"
359
- variant="ghost"
360
- size="sm"
361
- className="h-7 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
362
- disabled={importingSource !== null || loading || uploading}
363
- onClick={() => pdfInputRef.current?.click()}
364
- >
365
- <IconFileTypePdf className="size-3.5" />
366
- PDF
367
- </Button>
368
- <Button
369
- type="button"
370
- variant="ghost"
371
- size="sm"
372
- className="h-7 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
373
- disabled={importingSource !== null || loading || uploading}
374
- onClick={() => setGoogleSlidesInputOpen((current) => !current)}
375
- >
376
- <IconBrandGoogle className="size-3.5" />
377
- {t("home.googleSlidesReferenceTitle")}
378
- </Button>
379
- <Button
380
- type="button"
381
- variant="ghost"
382
- size="sm"
383
- className="h-7 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
384
- disabled={importingSource !== null || loading || uploading}
385
- onClick={() => pptxInputRef.current?.click()}
386
- >
387
- <IconPresentation className="size-3.5" />
388
- PPT
389
- </Button>
390
- {importingSource && (
391
- <span className="ml-auto text-xs text-muted-foreground">
392
- {importingLabel}
393
- </span>
394
- )}
395
407
  </div>
396
- {googleSlidesInputOpen && (
397
- <div className="mt-2 space-y-2">
398
- <GoogleDriveConnectionCta />
399
- <div className="flex gap-2">
400
- <Input
401
- autoFocus
402
- type="url"
403
- value={googleSlidesUrl}
404
- placeholder={t("home.googleSlidesReferenceUrl")}
405
- aria-label={t("home.googleSlidesReferenceUrl")}
406
- className="h-8 text-xs"
407
- disabled={importingSource !== null || loading || uploading}
408
- onChange={(event) => setGoogleSlidesUrl(event.target.value)}
409
- onKeyDown={(event) => {
410
- if (event.key === "Enter") handleGoogleSlidesImport();
411
- }}
412
- />
408
+
409
+ {submitting && (
410
+ <div
411
+ className="flex items-center gap-2 border-t border-border/60 px-4 py-2.5 text-xs text-muted-foreground"
412
+ role="status"
413
+ aria-live="polite"
414
+ >
415
+ <IconLoader2 className="size-3.5 animate-spin" />
416
+ <span>{t("raw.uploading")}</span>
417
+ </div>
418
+ )}
419
+
420
+ {importEnabled && (
421
+ <div className="border-t border-border/60 px-4 pb-3 pt-2.5">
422
+ <div className="flex flex-wrap items-center gap-1.5">
423
+ <span className="mr-1 text-xs text-muted-foreground">
424
+ {importFromCopy}
425
+ </span>
426
+ <Button
427
+ type="button"
428
+ variant="ghost"
429
+ size="sm"
430
+ className="h-7 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
431
+ disabled={loading || uploading || submitting}
432
+ onClick={() => chooseImportMode("pdf")}
433
+ >
434
+ <IconFileTypePdf className="size-3.5" />
435
+ PDF
436
+ </Button>
413
437
  <Button
414
438
  type="button"
439
+ variant="ghost"
415
440
  size="sm"
416
- className="h-8 shrink-0 px-3 text-xs"
417
- disabled={
418
- !googleSlidesUrl.trim() ||
419
- importingSource !== null ||
420
- loading ||
421
- uploading
422
- }
423
- onClick={handleGoogleSlidesImport}
441
+ className="h-7 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
442
+ disabled={loading || uploading || submitting}
443
+ onClick={() => chooseImportMode("google-slides")}
424
444
  >
425
- Import
445
+ <IconBrandGoogle className="size-3.5" />
446
+ {t("home.googleSlidesImportLabel")}
447
+ </Button>
448
+ <Button
449
+ type="button"
450
+ variant="ghost"
451
+ size="sm"
452
+ className="h-7 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground"
453
+ disabled={loading || uploading || submitting}
454
+ onClick={() => chooseImportMode("pptx")}
455
+ >
456
+ <IconPresentation className="size-3.5" />
457
+ PPT
426
458
  </Button>
427
459
  </div>
428
460
  </div>
429
461
  )}
462
+
463
+ {children}
464
+
465
+ <GoogleDocImportHint
466
+ promptText={promptText}
467
+ onSourceContextChange={setGoogleDocContext}
468
+ />
430
469
  </div>
431
- )}
432
470
 
433
- {children}
471
+ {importEnabled && importMode && (
472
+ <div className="col-start-1 row-start-1 flex min-h-full flex-col px-4 pb-4 pt-3">
473
+ <div className="flex items-center gap-2">
474
+ <button
475
+ type="button"
476
+ onClick={returnToPrompt}
477
+ disabled={importingSource !== null}
478
+ className="-ms-1 inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
479
+ aria-label={
480
+ "Back to prompt" /* i18n-ignore -- local import-flow control label */
481
+ }
482
+ >
483
+ <IconArrowLeft className="size-4" />
484
+ </button>
485
+ <span className="text-sm font-medium text-foreground">
486
+ Import {importModeLabel}
487
+ </span>
488
+ </div>
434
489
 
435
- <GoogleDocImportHint
436
- promptText={promptText}
437
- onSourceContextChange={setGoogleDocContext}
438
- />
490
+ <div className="flex flex-1 flex-col justify-center gap-3">
491
+ {importMode === "google-slides" ? (
492
+ <>
493
+ <GoogleDriveConnectionCta />
494
+ <div className="flex gap-2">
495
+ <Input
496
+ autoFocus
497
+ type="url"
498
+ value={googleSlidesUrl}
499
+ placeholder={t("home.googleSlidesReferenceUrl")}
500
+ aria-label={t("home.googleSlidesReferenceUrl")}
501
+ className="h-8 text-xs"
502
+ disabled={importingSource !== null || loading}
503
+ onChange={(event) =>
504
+ setGoogleSlidesUrl(event.target.value)
505
+ }
506
+ onKeyDown={(event) => {
507
+ if (event.key === "Enter") handleGoogleSlidesImport();
508
+ }}
509
+ />
510
+ <Button
511
+ type="button"
512
+ size="sm"
513
+ className="h-8 shrink-0 px-3 text-xs"
514
+ disabled={
515
+ !googleSlidesUrl.trim() ||
516
+ importingSource !== null ||
517
+ loading
518
+ }
519
+ onClick={handleGoogleSlidesImport}
520
+ >
521
+ Import
522
+ </Button>
523
+ </div>
524
+ </>
525
+ ) : (
526
+ <>
527
+ <p className="text-xs text-muted-foreground">
528
+ Choose a {importModeLabel} file to open it as a deck.
529
+ </p>
530
+ <Button
531
+ type="button"
532
+ variant="outline"
533
+ className="w-full justify-center gap-2"
534
+ disabled={importingSource !== null || loading}
535
+ onClick={() => importInputRef.current?.click()}
536
+ >
537
+ <IconUpload className="size-4" />
538
+ Upload {importModeLabel}
539
+ </Button>
540
+ {selectedImportFile && (
541
+ <p className="truncate text-center text-xs text-muted-foreground">
542
+ {selectedImportFile.name}
543
+ </p>
544
+ )}
545
+ </>
546
+ )}
547
+ </div>
548
+
549
+ {importingSource && (
550
+ <div
551
+ className="flex items-center justify-center gap-2 text-xs text-muted-foreground"
552
+ role="status"
553
+ aria-live="polite"
554
+ >
555
+ <IconLoader2 className="size-3.5 animate-spin" />
556
+ <span>{importingLabel}</span>
557
+ </div>
558
+ )}
559
+ </div>
560
+ )}
561
+ </div>
439
562
  </div>
440
563
  </>
441
564
  );
@@ -150,7 +150,7 @@ function ExcalidrawExitButton(props: { onExit: () => void; label: string }) {
150
150
  <Button
151
151
  variant="ghost"
152
152
  size="icon"
153
- className="absolute right-3 top-3 z-20 h-8 w-8 cursor-pointer border border-border bg-popover/95 shadow-lg backdrop-blur"
153
+ className="absolute right-3 top-3 z-20 h-8 w-8 cursor-pointer border border-border bg-popover/95 shadow-lg"
154
154
  onClick={props.onExit}
155
155
  aria-label={props.label}
156
156
  >
@@ -652,7 +652,7 @@ function SameSlidePresenceIndicator({ users }: { users: CollabUser[] }) {
652
652
  ? `${users[0].name} is here`
653
653
  : `${users.length} others here`;
654
654
  return (
655
- <div className="pointer-events-auto flex items-center gap-1.5 rounded-full border border-border bg-popover/95 py-1 pl-1 pr-2.5 text-xs text-popover-foreground shadow-lg backdrop-blur">
655
+ <div className="pointer-events-auto flex items-center gap-1.5 rounded-full border border-border bg-popover/95 py-1 pl-1 pr-2.5 text-xs text-popover-foreground shadow-lg">
656
656
  <div className="flex items-center">
657
657
  {visible.map((u) => (
658
658
  <SamePresenceAvatar key={u.email} user={u} />
@@ -4246,7 +4246,7 @@ export default function SlideEditor({
4246
4246
  </div>
4247
4247
  ) : (
4248
4248
  <div className="relative h-full bg-background">
4249
- <div className="absolute right-3 top-3 z-20 flex h-8 items-center gap-0.5 rounded-md border border-border bg-popover/95 px-1 shadow-lg backdrop-blur">
4249
+ <div className="absolute right-3 top-3 z-20 flex h-8 items-center gap-0.5 rounded-md border border-border bg-popover/95 px-1 shadow-lg">
4250
4250
  <Tooltip>
4251
4251
  <TooltipTrigger asChild>
4252
4252
  <Button
@@ -87,7 +87,7 @@ export function AgentWorkIndicator() {
87
87
 
88
88
  return (
89
89
  <div className="pointer-events-none fixed bottom-4 left-1/2 z-50 w-[calc(100vw-2rem)] max-w-sm -translate-x-1/2 md:bottom-5">
90
- <div className="pointer-events-auto flex items-center justify-between gap-3 rounded-lg border border-border bg-popover/95 px-3 py-2 text-popover-foreground shadow-xl shadow-black/20 backdrop-blur">
90
+ <div className="pointer-events-auto flex items-center justify-between gap-3 rounded-lg border border-border bg-popover/95 px-3 py-2 text-popover-foreground shadow-xl shadow-black/20">
91
91
  <div className="flex min-w-0 items-center gap-2">
92
92
  <IconLoader2 className="h-4 w-4 shrink-0 animate-spin text-[#609FF8]" />
93
93
  <span className="truncate text-sm font-medium">
@@ -12,13 +12,12 @@ import { useSidebarCollapsed } from "@/hooks/use-sidebar-collapsed";
12
12
  import { TAB_ID } from "@/lib/tab-id";
13
13
  import { cn } from "@/lib/utils";
14
14
 
15
- import { GoogleDriveConnectionCta } from "../editor/GoogleDriveConnectionCta";
16
15
  import { AgentWorkIndicator } from "./AgentWorkIndicator";
17
16
  import { Header } from "./Header";
18
17
  import {
19
- getDeckChatScopeLabel,
20
18
  getEffectiveSlidesSidebarCollapsed,
21
19
  isSlidesEditorRoute,
20
+ shouldShowSlidesAppSidebar,
22
21
  } from "./layout-route-policy";
23
22
  import { Sidebar } from "./Sidebar";
24
23
 
@@ -50,26 +49,24 @@ export function Layout({ children }: LayoutProps) {
50
49
  useState<EditorSidebarOverride | null>(null);
51
50
  const { collapsed: sidebarCollapsed, setCollapsed: setSidebarCollapsed } =
52
51
  useSidebarCollapsed();
53
- const { decks, getDeck, loading: decksLoading } = useDecks();
52
+ const { decks, loading: decksLoading } = useDecks();
54
53
  const isEmptyDecksState =
55
54
  location.pathname === "/" && !decksLoading && decks.length === 0;
56
55
 
57
56
  // Scope new chats to the deck the user is currently editing. The route
58
57
  // is `/deck/:id`; everywhere else (list, presentation) leaves
59
- // scope null so chats stay in the general pool. Falling back to the
60
- // raw deck id keeps the chat bound even before the deck object has
61
- // streamed in — once the title arrives the badge updates in place.
58
+ // scope null so chats stay in the general pool. Keep the visible label
59
+ // semantic so an imported deck id never leaks into the composer chip.
62
60
  const deckScope = useMemo(() => {
63
61
  const match = location.pathname.match(/^\/deck\/([^/]+)/);
64
62
  const deckId = match?.[1];
65
63
  if (!deckId) return null;
66
- const deck = getDeck(deckId);
67
64
  return {
68
65
  type: "deck" as const,
69
66
  id: deckId,
70
- label: getDeckChatScopeLabel(deck?.title, t("agent.thisDeck")),
67
+ label: t("agent.thisDeck"),
71
68
  };
72
- }, [location.pathname, getDeck, t]);
69
+ }, [location.pathname, t]);
73
70
 
74
71
  useEffect(() => {
75
72
  setSidebarOpen(false);
@@ -84,6 +81,7 @@ export function Layout({ children }: LayoutProps) {
84
81
  }, []);
85
82
 
86
83
  const ownToolbar = pageHasOwnToolbar(location.pathname);
84
+ const showAppSidebar = shouldShowSlidesAppSidebar(location.pathname);
87
85
  const editorSidebarOverrideForLocation =
88
86
  editorSidebarOverride?.locationKey === location.key
89
87
  ? editorSidebarOverride.collapsed
@@ -120,35 +118,36 @@ export function Layout({ children }: LayoutProps) {
120
118
  agentPageHref="/settings/agent"
121
119
  suppressFirstRunOnboarding={isSlidesEditorRoute(location.pathname)}
122
120
  composerSlot={<CreativeContextComposerChip />}
123
- threadFooterSlot={<GoogleDriveConnectionCta />}
124
121
  >
125
122
  <div className="agent-layout-shell flex h-screen w-full overflow-hidden bg-background text-foreground">
126
- {sidebarOpen && (
127
- <div
128
- className="fixed inset-0 z-40 bg-black/50 md:hidden"
129
- onClick={() => setSidebarOpen(false)}
130
- />
131
- )}
132
- {!isEmptyDecksState && (
133
- <div
134
- className={cn(
135
- "agent-layout-left-drawer fixed inset-y-0 start-0 z-50 transition-transform duration-200 ease-out md:static md:z-auto md:transition-none",
136
- sidebarOpen
137
- ? "translate-x-0"
138
- : "-translate-x-full rtl:translate-x-full md:translate-x-0 md:rtl:translate-x-0",
123
+ {!isEmptyDecksState && showAppSidebar && (
124
+ <>
125
+ {sidebarOpen && (
126
+ <div
127
+ className="fixed inset-0 z-40 bg-foreground/50 md:hidden"
128
+ onClick={() => setSidebarOpen(false)}
129
+ />
139
130
  )}
140
- >
141
- <Sidebar
142
- collapsed={effectiveSidebarCollapsed && !sidebarOpen}
143
- // In the mobile drawer the sidebar is forced expanded, so the
144
- // desktop collapse toggle would be a silent no-op (worse: it'd
145
- // mutate the desktop preference). Hide it while the drawer is
146
- // open.
147
- onToggleCollapsed={
148
- sidebarOpen ? undefined : toggleSidebarCollapsed
149
- }
150
- />
151
- </div>
131
+ <div
132
+ className={cn(
133
+ "agent-layout-left-drawer fixed inset-y-0 start-0 z-50 transition-transform duration-200 ease-out md:static md:z-auto md:transition-none",
134
+ sidebarOpen
135
+ ? "translate-x-0"
136
+ : "-translate-x-full rtl:translate-x-full md:translate-x-0 md:rtl:translate-x-0",
137
+ )}
138
+ >
139
+ <Sidebar
140
+ collapsed={effectiveSidebarCollapsed && !sidebarOpen}
141
+ // In the mobile drawer the sidebar is forced expanded, so the
142
+ // desktop collapse toggle would be a silent no-op (worse: it'd
143
+ // mutate the desktop preference). Hide it while the drawer is
144
+ // open.
145
+ onToggleCollapsed={
146
+ sidebarOpen ? undefined : toggleSidebarCollapsed
147
+ }
148
+ />
149
+ </div>
150
+ </>
152
151
  )}
153
152
  <div className="agent-layout-main-surface flex h-full min-w-0 flex-1 flex-col overflow-hidden">
154
153
  {/* Mobile-only nav strip with hamburger — only when there's no page toolbar */}
@@ -2,11 +2,8 @@ export function isSlidesEditorRoute(pathname: string): boolean {
2
2
  return /^\/deck\/[^/]+\/?$/.test(pathname);
3
3
  }
4
4
 
5
- export function getDeckChatScopeLabel(
6
- deckTitle: string | undefined,
7
- untitledDeckLabel: string,
8
- ): string {
9
- return deckTitle === "Untitled Deck" ? untitledDeckLabel : deckTitle || "";
5
+ export function shouldShowSlidesAppSidebar(pathname: string): boolean {
6
+ return !isSlidesEditorRoute(pathname);
10
7
  }
11
8
 
12
9
  export function getEffectiveSlidesSidebarCollapsed({