@agent-native/core 0.84.57 → 0.84.58

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 (69) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/action.ts +11 -0
  5. package/corpus/core/src/agent/production-agent.ts +24 -46
  6. package/corpus/core/src/client/AssistantChat.tsx +69 -12
  7. package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
  8. package/corpus/core/src/client/conversation/use-near-bottom-autoscroll.ts +45 -2
  9. package/corpus/core/src/coding-tools/run-code.ts +1 -0
  10. package/corpus/core/src/integrations/webhook-handler.ts +10 -9
  11. package/corpus/core/src/server/action-discovery.ts +3 -0
  12. package/corpus/core/src/server/credential-provider.ts +11 -6
  13. package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +29 -0
  14. package/corpus/templates/analytics/server/plugins/agent-chat.ts +9 -1
  15. package/corpus/templates/design/actions/list-design-extensions.ts +2 -2
  16. package/corpus/templates/design/app/components/design/CodeWorkbenchHost.tsx +11 -89
  17. package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +314 -181
  18. package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +92 -134
  19. package/corpus/templates/design/app/components/design/EditPanel.tsx +93 -19
  20. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +18 -26
  21. package/corpus/templates/design/app/components/design/TokensPanel.tsx +220 -192
  22. package/corpus/templates/design/app/components/design/code-workbench-theme.ts +150 -0
  23. package/corpus/templates/design/app/i18n/zh-TW.ts +5 -7
  24. package/corpus/templates/design/app/i18n-data.ts +61 -77
  25. package/corpus/templates/design/app/lib/design-import.ts +4 -1
  26. package/corpus/templates/design/app/pages/DesignEditor.tsx +97 -48
  27. package/corpus/templates/design/changelog/2026-07-02-design-inspector-and-canvas-controls-stay-consistent-when-se.md +6 -0
  28. package/corpus/templates/design/changelog/2026-07-02-fixed-a-crash-when-opening-the-code-editor.md +6 -0
  29. package/corpus/templates/design/changelog/2026-07-02-fixed-motion-drawer-open-transition.md +6 -0
  30. package/corpus/templates/design/changelog/2026-07-02-improved-design-assets-panel.md +6 -0
  31. package/dist/action.d.ts +8 -0
  32. package/dist/action.d.ts.map +1 -1
  33. package/dist/action.js +3 -0
  34. package/dist/action.js.map +1 -1
  35. package/dist/agent/production-agent.d.ts +10 -11
  36. package/dist/agent/production-agent.d.ts.map +1 -1
  37. package/dist/agent/production-agent.js +21 -44
  38. package/dist/agent/production-agent.js.map +1 -1
  39. package/dist/client/AssistantChat.d.ts +1 -0
  40. package/dist/client/AssistantChat.d.ts.map +1 -1
  41. package/dist/client/AssistantChat.js +61 -11
  42. package/dist/client/AssistantChat.js.map +1 -1
  43. package/dist/client/MultiTabAssistantChat.js +1 -1
  44. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  45. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -1
  46. package/dist/client/conversation/use-near-bottom-autoscroll.js +43 -2
  47. package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -1
  48. package/dist/coding-tools/run-code.d.ts.map +1 -1
  49. package/dist/coding-tools/run-code.js +1 -0
  50. package/dist/coding-tools/run-code.js.map +1 -1
  51. package/dist/collab/awareness.d.ts +2 -2
  52. package/dist/collab/awareness.d.ts.map +1 -1
  53. package/dist/collab/routes.d.ts +1 -1
  54. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  55. package/dist/integrations/webhook-handler.d.ts +1 -0
  56. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  57. package/dist/integrations/webhook-handler.js +9 -9
  58. package/dist/integrations/webhook-handler.js.map +1 -1
  59. package/dist/notifications/routes.d.ts +3 -3
  60. package/dist/observability/routes.d.ts +5 -5
  61. package/dist/server/action-discovery.d.ts.map +1 -1
  62. package/dist/server/action-discovery.js +3 -0
  63. package/dist/server/action-discovery.js.map +1 -1
  64. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  65. package/dist/server/credential-provider.d.ts +4 -0
  66. package/dist/server/credential-provider.d.ts.map +1 -1
  67. package/dist/server/credential-provider.js +13 -7
  68. package/dist/server/credential-provider.js.map +1 -1
  69. package/package.json +1 -1
@@ -24,7 +24,6 @@ import {
24
24
  IconChevronDown,
25
25
  IconChevronRight,
26
26
  IconCircle,
27
- IconCode,
28
27
  IconFileText,
29
28
  IconFolder,
30
29
  IconLetterCase,
@@ -335,109 +334,10 @@ function TokenGroupSection({
335
334
  // New Token popover
336
335
  // ---------------------------------------------------------------------------
337
336
 
337
+ type TokenCreateMode = "menu" | "add" | "text";
338
+
338
339
  interface NewTokenPopoverProps {
339
340
  onAdd: (cssVar: string, value: string) => void;
340
- }
341
-
342
- function NewTokenPopover({ onAdd }: NewTokenPopoverProps) {
343
- const [open, setOpen] = useState(false);
344
- const [cssVar, setCssVar] = useState("--my-token");
345
- const [value, setValue] = useState("#000000");
346
- const t = useT();
347
-
348
- const handleAdd = () => {
349
- const cleanVar = cssVar.startsWith("--") ? cssVar : `--${cssVar}`;
350
- onAdd(cleanVar, value);
351
- setOpen(false);
352
- setCssVar("--my-token");
353
- setValue("#000000");
354
- };
355
-
356
- return (
357
- <Popover open={open} onOpenChange={setOpen}>
358
- <PopoverTrigger asChild>
359
- <Button
360
- type="button"
361
- variant="ghost"
362
- size="sm"
363
- className="h-6 cursor-pointer gap-1 px-2 !text-[11px] text-muted-foreground hover:text-foreground"
364
- >
365
- <IconPlus className="size-3" />
366
- {t("designEditor.tokens.newToken")}
367
- </Button>
368
- </PopoverTrigger>
369
- <PopoverContent align="end" className="w-56 p-3 text-[12px]">
370
- <div className="space-y-2">
371
- <div className="space-y-1">
372
- <label className="text-[10px] font-medium text-muted-foreground">
373
- {t("designEditor.tokens.cssVar")}
374
- </label>
375
- <Input
376
- value={cssVar}
377
- onChange={(e) => setCssVar(e.target.value)}
378
- className="h-6 font-mono !text-[11px] md:!text-[11px]"
379
- placeholder="--my-token"
380
- />
381
- </div>
382
- <div className="space-y-1">
383
- <label className="text-[10px] font-medium text-muted-foreground">
384
- {t("designEditor.tokens.value")}
385
- </label>
386
- <Input
387
- value={value}
388
- onChange={(e) => setValue(e.target.value)}
389
- className="h-6 font-mono !text-[11px] md:!text-[11px]"
390
- placeholder="#3B82F6"
391
- />
392
- </div>
393
- <Button
394
- type="button"
395
- className="h-7 w-full cursor-pointer !text-[11px]"
396
- onClick={handleAdd}
397
- disabled={!cssVar || !value}
398
- >
399
- {t("designEditor.tokens.add")}
400
- </Button>
401
- </div>
402
- </PopoverContent>
403
- </Popover>
404
- );
405
- }
406
-
407
- // ---------------------------------------------------------------------------
408
- // Import popover
409
- // ---------------------------------------------------------------------------
410
-
411
- const TOKEN_IMPORT_ACCEPT = [
412
- ".css",
413
- ".scss",
414
- ".sass",
415
- ".less",
416
- ".json",
417
- ".ts",
418
- ".tsx",
419
- ".js",
420
- ".jsx",
421
- ".md",
422
- ".mdx",
423
- ".txt",
424
- ].join(",");
425
-
426
- async function readImportFiles(fileList: FileList): Promise<TokenImportFile[]> {
427
- const selected = [...fileList].slice(0, 20);
428
- const files = await Promise.all(
429
- selected.map(async (file) => ({
430
- filename:
431
- (file as File & { webkitRelativePath?: string }).webkitRelativePath ||
432
- file.name,
433
- content: await file.text(),
434
- })),
435
- );
436
-
437
- return files.filter((file) => file.content.trim().length > 0);
438
- }
439
-
440
- interface ImportTokensPopoverProps {
441
341
  onImportFiles: (
442
342
  files: TokenImportFile[],
443
343
  ) => Promise<ImportDesignTokensResult>;
@@ -446,18 +346,31 @@ interface ImportTokensPopoverProps {
446
346
  isPending: boolean;
447
347
  }
448
348
 
449
- function ImportTokensPopover({
349
+ function NewTokenPopover({
350
+ onAdd,
450
351
  onImportFiles,
451
352
  onImportText,
452
353
  onImportCurrentDesign,
453
354
  isPending,
454
- }: ImportTokensPopoverProps) {
455
- const t = useT();
456
- const fileInputRef = useRef<HTMLInputElement>(null);
457
- const folderInputRef = useRef<HTMLInputElement>(null);
355
+ }: NewTokenPopoverProps) {
458
356
  const [open, setOpen] = useState(false);
357
+ const [mode, setMode] = useState<TokenCreateMode>("menu");
358
+ const [cssVar, setCssVar] = useState("--my-token");
359
+ const [value, setValue] = useState("#000000");
459
360
  const [text, setText] = useState("");
460
361
  const [status, setStatus] = useState<string | null>(null);
362
+ const fileInputRef = useRef<HTMLInputElement>(null);
363
+ const folderInputRef = useRef<HTMLInputElement>(null);
364
+ const t = useT();
365
+
366
+ const handleAdd = () => {
367
+ const cleanVar = cssVar.startsWith("--") ? cssVar : `--${cssVar}`;
368
+ onAdd(cleanVar, value);
369
+ setOpen(false);
370
+ setMode("menu");
371
+ setCssVar("--my-token");
372
+ setValue("#000000");
373
+ };
461
374
 
462
375
  const runImport = async (
463
376
  importer: () => Promise<ImportDesignTokensResult>,
@@ -478,6 +391,14 @@ function ImportTokensPopover({
478
391
  }
479
392
  };
480
393
 
394
+ const closeOrReset = (nextOpen: boolean) => {
395
+ setOpen(nextOpen);
396
+ if (!nextOpen) {
397
+ setMode("menu");
398
+ setStatus(null);
399
+ }
400
+ };
401
+
481
402
  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
482
403
  const files = event.currentTarget.files;
483
404
  event.currentTarget.value = "";
@@ -486,91 +407,98 @@ function ImportTokensPopover({
486
407
  };
487
408
 
488
409
  return (
489
- <Popover open={open} onOpenChange={setOpen}>
490
- <Tooltip>
491
- <TooltipTrigger asChild>
492
- <PopoverTrigger asChild>
493
- <Button
494
- type="button"
495
- variant="ghost"
496
- size="icon"
497
- className="size-6 cursor-pointer text-muted-foreground/60 hover:text-foreground"
498
- aria-label={t("designEditor.tokens.import")}
499
- >
500
- <IconUpload className="size-3" />
501
- </Button>
502
- </PopoverTrigger>
503
- </TooltipTrigger>
504
- <TooltipContent>{t("designEditor.tokens.import")}</TooltipContent>
505
- </Tooltip>
506
- <PopoverContent align="end" className="w-72 p-3 text-[12px]">
507
- <div className="space-y-3">
508
- <div className="space-y-1">
509
- <p className="!text-[11px] font-medium text-foreground">
510
- {t("designEditor.tokens.importTitle")}
511
- </p>
512
- <p className="text-[10px] leading-snug text-muted-foreground">
513
- {t("designEditor.tokens.importHint")}
514
- </p>
515
- </div>
516
-
517
- <div className="grid grid-cols-3 gap-1.5">
518
- <Button
519
- type="button"
520
- variant="outline"
521
- size="sm"
522
- className="h-7 cursor-pointer gap-1 px-2 text-[10px]"
410
+ <Popover open={open} onOpenChange={closeOrReset}>
411
+ <PopoverTrigger asChild>
412
+ <Button
413
+ type="button"
414
+ variant="ghost"
415
+ size="sm"
416
+ className="h-6 cursor-pointer gap-1 px-2 !text-[11px] text-muted-foreground hover:text-foreground"
417
+ >
418
+ <IconPlus className="size-3" />
419
+ {t("designEditor.tokens.newToken")}
420
+ </Button>
421
+ </PopoverTrigger>
422
+ <PopoverContent align="end" className="w-72 p-2 text-[12px]">
423
+ {mode === "menu" ? (
424
+ <div className="space-y-0.5">
425
+ <TokenCreateOption
426
+ icon={<IconPlus className="size-3.5" />}
427
+ title="Add one token"
428
+ description="Create a single CSS variable by hand."
429
+ onClick={() => setMode("add")}
523
430
  disabled={isPending}
431
+ />
432
+ <TokenCreateOption
433
+ icon={<IconUpload className="size-3.5" />}
434
+ title="Import a set from text"
435
+ description="Paste CSS variables, theme notes, or token JSON."
436
+ onClick={() => setMode("text")}
437
+ disabled={isPending}
438
+ />
439
+ <TokenCreateOption
440
+ icon={<IconFileText className="size-3.5" />}
441
+ title="Import from a file"
442
+ description="Read colors, spacing, and type from selected files."
524
443
  onClick={() => fileInputRef.current?.click()}
525
- >
526
- <IconFileText className="size-3" />
527
- {t("designEditor.tokens.files")}
528
- </Button>
529
- <Button
530
- type="button"
531
- variant="outline"
532
- size="sm"
533
- className="h-7 cursor-pointer gap-1 px-2 text-[10px]"
534
444
  disabled={isPending}
445
+ />
446
+ <TokenCreateOption
447
+ icon={<IconFolder className="size-3.5" />}
448
+ title="Import from a folder"
449
+ description="Scan a small source folder for token definitions."
535
450
  onClick={() => folderInputRef.current?.click()}
536
- >
537
- <IconFolder className="size-3" />
538
- {t("designEditor.tokens.folder")}
539
- </Button>
540
- <Button
541
- type="button"
542
- variant="outline"
543
- size="sm"
544
- className="h-7 cursor-pointer px-2 text-[10px]"
545
451
  disabled={isPending}
452
+ />
453
+ <TokenCreateOption
454
+ icon={<IconPalette className="size-3.5" />}
455
+ title="Import from current design"
456
+ description="Extract reusable tokens already used on the canvas."
546
457
  onClick={() => void runImport(onImportCurrentDesign)}
458
+ disabled={isPending}
459
+ />
460
+ </div>
461
+ ) : mode === "add" ? (
462
+ <div className="space-y-2 p-1">
463
+ <TokenCreateBackButton onClick={() => setMode("menu")} />
464
+ <div className="space-y-1">
465
+ <label className="text-[10px] font-medium text-muted-foreground">
466
+ {t("designEditor.tokens.cssVar")}
467
+ </label>
468
+ <Input
469
+ value={cssVar}
470
+ onChange={(e) => setCssVar(e.target.value)}
471
+ className="h-6 font-mono !text-[11px] md:!text-[11px]"
472
+ placeholder="--my-token"
473
+ />
474
+ </div>
475
+ <div className="space-y-1">
476
+ <label className="text-[10px] font-medium text-muted-foreground">
477
+ {t("designEditor.tokens.value")}
478
+ </label>
479
+ <Input
480
+ value={value}
481
+ onChange={(e) => setValue(e.target.value)}
482
+ className="h-6 font-mono !text-[11px] md:!text-[11px]"
483
+ placeholder="#3B82F6"
484
+ />
485
+ </div>
486
+ <Button
487
+ type="button"
488
+ className="h-7 w-full cursor-pointer !text-[11px]"
489
+ onClick={handleAdd}
490
+ disabled={!cssVar || !value}
547
491
  >
548
- {t("designEditor.tokens.current")}
492
+ {t("designEditor.tokens.add")}
549
493
  </Button>
550
494
  </div>
551
-
552
- <input
553
- ref={fileInputRef}
554
- type="file"
555
- multiple
556
- accept={TOKEN_IMPORT_ACCEPT}
557
- className="hidden"
558
- onChange={handleFileChange}
559
- />
560
- <input
561
- ref={folderInputRef}
562
- type="file"
563
- multiple
564
- accept={TOKEN_IMPORT_ACCEPT}
565
- className="hidden"
566
- onChange={handleFileChange}
567
- {...({ directory: "", webkitdirectory: "" } as Record<
568
- string,
569
- string
570
- >)}
571
- />
572
-
573
- <div className="space-y-1.5">
495
+ ) : (
496
+ <div className="space-y-2 p-1">
497
+ <TokenCreateBackButton onClick={() => setMode("menu")} />
498
+ <p className="text-[10px] leading-snug text-muted-foreground">
499
+ Paste a token set from CSS, JSON, Tailwind config, or design
500
+ notes.
501
+ </p>
574
502
  <Textarea
575
503
  value={text}
576
504
  onChange={(event) => setText(event.target.value)}
@@ -586,18 +514,119 @@ function ImportTokensPopover({
586
514
  {t("designEditor.tokens.importPasted")}
587
515
  </Button>
588
516
  </div>
517
+ )}
589
518
 
590
- {status && (
591
- <p className="text-[10px] leading-snug text-muted-foreground">
592
- {status}
593
- </p>
594
- )}
595
- </div>
519
+ <input
520
+ ref={fileInputRef}
521
+ type="file"
522
+ multiple
523
+ accept={TOKEN_IMPORT_ACCEPT}
524
+ className="hidden"
525
+ onChange={handleFileChange}
526
+ />
527
+ <input
528
+ ref={folderInputRef}
529
+ type="file"
530
+ multiple
531
+ accept={TOKEN_IMPORT_ACCEPT}
532
+ className="hidden"
533
+ onChange={handleFileChange}
534
+ {...({ directory: "", webkitdirectory: "" } as Record<
535
+ string,
536
+ string
537
+ >)}
538
+ />
539
+
540
+ {status && (
541
+ <p className="mt-2 px-1 text-[10px] leading-snug text-muted-foreground">
542
+ {status}
543
+ </p>
544
+ )}
596
545
  </PopoverContent>
597
546
  </Popover>
598
547
  );
599
548
  }
600
549
 
550
+ function TokenCreateBackButton({ onClick }: { onClick: () => void }) {
551
+ return (
552
+ <button
553
+ type="button"
554
+ onClick={onClick}
555
+ className="mb-1 text-[10px] font-medium text-muted-foreground hover:text-foreground"
556
+ >
557
+ Back
558
+ </button>
559
+ );
560
+ }
561
+
562
+ function TokenCreateOption({
563
+ icon,
564
+ title,
565
+ description,
566
+ onClick,
567
+ disabled,
568
+ }: {
569
+ icon: React.ReactNode;
570
+ title: string;
571
+ description: string;
572
+ onClick: () => void;
573
+ disabled?: boolean;
574
+ }) {
575
+ return (
576
+ <button
577
+ type="button"
578
+ disabled={disabled}
579
+ onClick={onClick}
580
+ className="group flex w-full cursor-pointer items-center gap-2.5 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-accent/60 disabled:cursor-not-allowed disabled:opacity-50"
581
+ >
582
+ <span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border/70 bg-muted/70 text-muted-foreground transition-colors group-hover:border-border group-hover:bg-muted">
583
+ {icon}
584
+ </span>
585
+ <span className="min-w-0 flex-1">
586
+ <span className="block truncate text-[13px] font-medium leading-tight text-foreground">
587
+ {title}
588
+ </span>
589
+ <span className="mt-0.5 line-clamp-1 text-[11px] leading-snug text-muted-foreground">
590
+ {description}
591
+ </span>
592
+ </span>
593
+ </button>
594
+ );
595
+ }
596
+
597
+ // ---------------------------------------------------------------------------
598
+ // Import helpers
599
+ // ---------------------------------------------------------------------------
600
+
601
+ const TOKEN_IMPORT_ACCEPT = [
602
+ ".css",
603
+ ".scss",
604
+ ".sass",
605
+ ".less",
606
+ ".json",
607
+ ".ts",
608
+ ".tsx",
609
+ ".js",
610
+ ".jsx",
611
+ ".md",
612
+ ".mdx",
613
+ ".txt",
614
+ ].join(",");
615
+
616
+ async function readImportFiles(fileList: FileList): Promise<TokenImportFile[]> {
617
+ const selected = [...fileList].slice(0, 20);
618
+ const files = await Promise.all(
619
+ selected.map(async (file) => ({
620
+ filename:
621
+ (file as File & { webkitRelativePath?: string }).webkitRelativePath ||
622
+ file.name,
623
+ content: await file.text(),
624
+ })),
625
+ );
626
+
627
+ return files.filter((file) => file.content.trim().length > 0);
628
+ }
629
+
601
630
  // ---------------------------------------------------------------------------
602
631
  // Main panel
603
632
  // ---------------------------------------------------------------------------
@@ -719,7 +748,6 @@ export function TokensPanel({ designId, onTokensApplied }: TokensPanelProps) {
719
748
  {/* Header */}
720
749
  <div className="flex items-center justify-between border-b border-border/60 px-3 py-2">
721
750
  <div className="flex items-center gap-1.5">
722
- <IconCode className="size-3.5 text-muted-foreground/60" />
723
751
  <span className="!text-[11px] font-semibold text-foreground">
724
752
  {t("designEditor.tokens.title")}
725
753
  </span>
@@ -745,13 +773,13 @@ export function TokensPanel({ designId, onTokensApplied }: TokensPanelProps) {
745
773
  </TooltipTrigger>
746
774
  <TooltipContent>{t("designEditor.tokens.refresh")}</TooltipContent>
747
775
  </Tooltip>
748
- <ImportTokensPopover
776
+ <NewTokenPopover
777
+ onAdd={handleNewToken}
749
778
  onImportFiles={importFiles}
750
779
  onImportText={importText}
751
780
  onImportCurrentDesign={importCurrentDesign}
752
781
  isPending={importMutation.isPending}
753
782
  />
754
- <NewTokenPopover onAdd={handleNewToken} />
755
783
  </div>
756
784
  </div>
757
785
 
@@ -0,0 +1,150 @@
1
+ export interface CodeWorkbenchTheme {
2
+ colorScheme: "light" | "dark";
3
+ values: Record<string, string>;
4
+ }
5
+
6
+ const WORKBENCH_THEME_VARS: Record<string, string[]> = {
7
+ "--workbench-bg": ["--design-editor-panel-bg", "--background"],
8
+ "--workbench-sidebar-bg": ["--design-editor-panel-bg", "--card"],
9
+ "--workbench-editor-bg": ["--design-editor-panel-bg", "--background"],
10
+ "--workbench-surface-bg": ["--design-editor-control-bg", "--muted"],
11
+ "--workbench-border": ["--design-editor-control-border", "--border"],
12
+ "--workbench-fg": ["--foreground"],
13
+ "--workbench-muted-fg": ["--muted-foreground"],
14
+ "--workbench-hover-bg": ["--design-editor-layer-hover-color", "--accent"],
15
+ "--workbench-active-bg": ["--design-editor-selection-color", "--accent"],
16
+ "--workbench-active-fg": [
17
+ "--design-editor-accent-color",
18
+ "--accent-foreground",
19
+ ],
20
+ "--workbench-accent": ["--design-editor-accent-color", "--primary"],
21
+ "--workbench-button-bg": ["--design-editor-control-bg", "--background"],
22
+ "--workbench-button-fg": ["--foreground"],
23
+ "--workbench-selection-bg": ["--design-editor-selection-color", "--accent"],
24
+ "--workbench-dirty": ["--warning", "--destructive"],
25
+ };
26
+
27
+ const HEX_COLOR_RE = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
28
+
29
+ function normalizeHexColor(value: string): string {
30
+ const hex = value.toLowerCase();
31
+ if (hex.length !== 4 && hex.length !== 5) return hex;
32
+ const [, r, g, b, a] = hex;
33
+ return `#${r}${r}${g}${g}${b}${b}${a ? `${a}${a}` : ""}`;
34
+ }
35
+
36
+ function parseRgbChannel(value: string): number | undefined {
37
+ const trimmed = value.trim();
38
+ const parsed = trimmed.endsWith("%")
39
+ ? (Number.parseFloat(trimmed.slice(0, -1)) * 255) / 100
40
+ : Number.parseFloat(trimmed);
41
+ if (!Number.isFinite(parsed)) return undefined;
42
+ return Math.min(255, Math.max(0, Math.round(parsed)));
43
+ }
44
+
45
+ function parseRgbAlpha(value: string | undefined): number {
46
+ if (!value) return 255;
47
+ const trimmed = value.trim();
48
+ const parsed = trimmed.endsWith("%")
49
+ ? Number.parseFloat(trimmed.slice(0, -1)) / 100
50
+ : Number.parseFloat(trimmed);
51
+ if (!Number.isFinite(parsed)) return 255;
52
+ return Math.min(255, Math.max(0, Math.round(parsed * 255)));
53
+ }
54
+
55
+ function toHexByte(value: number): string {
56
+ return value.toString(16).padStart(2, "0");
57
+ }
58
+
59
+ function parseRgbColor(value: string): string | undefined {
60
+ const match = /^rgba?\((.*)\)$/i.exec(value.trim());
61
+ if (!match) return undefined;
62
+ const body = match[1]?.trim();
63
+ if (!body) return undefined;
64
+ const parts = body.includes(",")
65
+ ? body.split(",").map((part) => part.trim())
66
+ : body.replace(/\//g, " / ").split(/\s+/).filter(Boolean);
67
+ const slashIndex = parts.indexOf("/");
68
+ const colorParts = slashIndex === -1 ? parts : parts.slice(0, slashIndex);
69
+ const alphaPart =
70
+ slashIndex === -1 ? colorParts[3] : parts.slice(slashIndex + 1).join(" ");
71
+ if (colorParts.length < 3) return undefined;
72
+ const red = parseRgbChannel(colorParts[0] ?? "");
73
+ const green = parseRgbChannel(colorParts[1] ?? "");
74
+ const blue = parseRgbChannel(colorParts[2] ?? "");
75
+ if (red === undefined || green === undefined || blue === undefined) {
76
+ return undefined;
77
+ }
78
+ const alpha = parseRgbAlpha(alphaPart);
79
+ const hex = `#${toHexByte(red)}${toHexByte(green)}${toHexByte(blue)}`;
80
+ return alpha < 255 ? `${hex}${toHexByte(alpha)}` : hex;
81
+ }
82
+
83
+ export function normalizeThemeColorValue(value: string): string {
84
+ const trimmed = value.trim();
85
+ if (!trimmed) return "";
86
+ if (/^-?\d+(\.\d+)?\s+-?\d+(\.\d+)?%/.test(trimmed)) {
87
+ return `hsl(${trimmed})`;
88
+ }
89
+ return trimmed;
90
+ }
91
+
92
+ export function normalizeMonacoThemeColor(
93
+ value: string | undefined,
94
+ ): string | undefined {
95
+ const normalized = normalizeThemeColorValue(value ?? "");
96
+ if (!normalized) return undefined;
97
+ if (HEX_COLOR_RE.test(normalized)) return normalizeHexColor(normalized);
98
+ return parseRgbColor(normalized);
99
+ }
100
+
101
+ export function resolveCssColorValue(value: string): string {
102
+ if (typeof window === "undefined" || typeof document === "undefined") {
103
+ return value;
104
+ }
105
+ const probe = document.createElement("span");
106
+ probe.style.color = value;
107
+ probe.style.position = "fixed";
108
+ probe.style.pointerEvents = "none";
109
+ probe.style.visibility = "hidden";
110
+ document.body.appendChild(probe);
111
+ const resolved = window.getComputedStyle(probe).color;
112
+ probe.remove();
113
+ return resolved || value;
114
+ }
115
+
116
+ function readThemeVar(
117
+ elementStyles: CSSStyleDeclaration,
118
+ rootStyles: CSSStyleDeclaration,
119
+ names: string[],
120
+ ): string | undefined {
121
+ for (const name of names) {
122
+ const value =
123
+ elementStyles.getPropertyValue(name) || rootStyles.getPropertyValue(name);
124
+ const normalized = normalizeThemeColorValue(value);
125
+ if (normalized) return resolveCssColorValue(normalized);
126
+ }
127
+ return undefined;
128
+ }
129
+
130
+ export function readCodeWorkbenchTheme(
131
+ element: HTMLElement | null,
132
+ ): CodeWorkbenchTheme {
133
+ if (typeof window === "undefined" || !element) {
134
+ return { colorScheme: "light", values: {} };
135
+ }
136
+ const elementStyles = window.getComputedStyle(element);
137
+ const rootStyles = window.getComputedStyle(document.documentElement);
138
+ const values: Record<string, string> = {};
139
+ for (const [targetVar, sourceVars] of Object.entries(WORKBENCH_THEME_VARS)) {
140
+ const value = readThemeVar(elementStyles, rootStyles, sourceVars);
141
+ if (value) values[targetVar] = value;
142
+ }
143
+ const colorScheme =
144
+ document.documentElement.classList.contains("dark") ||
145
+ elementStyles.colorScheme.includes("dark") ||
146
+ rootStyles.colorScheme.includes("dark")
147
+ ? "dark"
148
+ : "light";
149
+ return { colorScheme, values };
150
+ }
@@ -279,8 +279,8 @@ const messages = {
279
279
  "將 Figma 選取內容、.fig 匯出檔或獨立 HTML 帶入為 Design 螢幕。",
280
280
  figmaPasteTitle: "從 Figma 貼上",
281
281
  figmaPasteDescription:
282
- "在 Figma 複製畫框或圖層,然後聚焦此目標並貼上。可用時會直接匯入 Figma 中繼資料。",
283
- figmaPasteTarget: "在此貼上 Figma 內容",
282
+ "在 Figma 複製畫框或圖層,然後貼到 Design 畫布。可用時會直接匯入 Figma 中繼資料。",
283
+ figmaPasteTarget: "貼到畫布",
284
284
  figUploadTitle: "上傳 .fig",
285
285
  figUploadDescription:
286
286
  "只匯出需要的畫框。含有許多嵌入圖片的大型檔案可能超過匯入限制。",
@@ -293,17 +293,15 @@ const messages = {
293
293
  chooseHtmlFile: "選擇 HTML 檔案",
294
294
  githubTitle: "GitHub",
295
295
  githubDescription: "即將推出:直接從儲存庫匯入螢幕和元件。",
296
- localTitle: "本機 app / VS Code",
297
- localDescription: "使用 visual-edit 連接執行中的本機 app。",
296
+ localTitle: "本機 app",
297
+ localDescription: "使用 /visual-edit 連接任何執行中的本機 app。",
298
298
  visualEditGuidance:
299
- "啟動 app,在 app repo 中執行下方命令,然後請代理使用 visual-edit skill 新增 URL 螢幕。",
300
- useVisualEditNow: "立即使用 visual-edit",
299
+ "安裝 /visual-edit skill,啟動任意本機 app,然後在該 app repo 中執行橋接命令,讓代理新增 URL 螢幕。",
301
300
  comingSoon: "即將推出",
302
301
  warningsToast: "匯入完成但有警告",
303
302
  figmaSuccess: "已匯入 Figma 貼上內容",
304
303
  htmlSuccess: "已匯入 HTML",
305
304
  uploadSuccess: "已匯入檔案",
306
- visualEditSent: "已將 visual-edit 請求傳送給代理",
307
305
  lastImport: "上次匯入",
308
306
  errors: {
309
307
  notHtml: "請貼上或選擇有效的 HTML 以匯入。",