@agent-native/toolkit 0.14.3 → 0.15.1

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 (106) hide show
  1. package/agent-native.eject.json +19 -1
  2. package/dist/chat-history.css +3 -1
  3. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -1
  4. package/dist/composer/ComposerPlusMenu.js +131 -35
  5. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  6. package/dist/composer/MentionPopover.d.ts.map +1 -1
  7. package/dist/composer/MentionPopover.js +40 -5
  8. package/dist/composer/MentionPopover.js.map +1 -1
  9. package/dist/composer/PastedTextChip.d.ts.map +1 -1
  10. package/dist/composer/PastedTextChip.js +26 -2
  11. package/dist/composer/PastedTextChip.js.map +1 -1
  12. package/dist/composer/PastedTextChip.spec.d.ts +2 -0
  13. package/dist/composer/PastedTextChip.spec.d.ts.map +1 -0
  14. package/dist/composer/PastedTextChip.spec.js +42 -0
  15. package/dist/composer/PastedTextChip.spec.js.map +1 -0
  16. package/dist/composer/PromptComposer.d.ts.map +1 -1
  17. package/dist/composer/PromptComposer.js +26 -5
  18. package/dist/composer/PromptComposer.js.map +1 -1
  19. package/dist/composer/TiptapComposer.d.ts +5 -3
  20. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  21. package/dist/composer/TiptapComposer.js +289 -79
  22. package/dist/composer/TiptapComposer.js.map +1 -1
  23. package/dist/composer/TiptapComposer.spec.js +57 -0
  24. package/dist/composer/TiptapComposer.spec.js.map +1 -1
  25. package/dist/composer/VoiceButton.d.ts.map +1 -1
  26. package/dist/composer/VoiceButton.js +24 -5
  27. package/dist/composer/VoiceButton.js.map +1 -1
  28. package/dist/composer/runtime-adapters.d.ts +1 -0
  29. package/dist/composer/runtime-adapters.d.ts.map +1 -1
  30. package/dist/composer/runtime-adapters.js +8 -1
  31. package/dist/composer/runtime-adapters.js.map +1 -1
  32. package/dist/composer/runtime-adapters.spec.js +15 -0
  33. package/dist/composer/runtime-adapters.spec.js.map +1 -1
  34. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  35. package/dist/composer/useRealtimeVoiceMode.js +204 -68
  36. package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
  37. package/dist/composer/useRealtimeVoiceMode.spec.js +27 -1
  38. package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -1
  39. package/dist/context-ui/ContextMeter.d.ts +3 -2
  40. package/dist/context-ui/ContextMeter.d.ts.map +1 -1
  41. package/dist/context-ui/ContextMeter.js +23 -3
  42. package/dist/context-ui/ContextMeter.js.map +1 -1
  43. package/dist/context-ui/ContextMeter.spec.d.ts +2 -0
  44. package/dist/context-ui/ContextMeter.spec.d.ts.map +1 -0
  45. package/dist/context-ui/ContextMeter.spec.js +52 -0
  46. package/dist/context-ui/ContextMeter.spec.js.map +1 -0
  47. package/dist/context-ui/ContextSegmentRow.d.ts +5 -3
  48. package/dist/context-ui/ContextSegmentRow.d.ts.map +1 -1
  49. package/dist/context-ui/ContextSegmentRow.js +74 -7
  50. package/dist/context-ui/ContextSegmentRow.js.map +1 -1
  51. package/dist/context-ui/ContextTreemap.d.ts +3 -2
  52. package/dist/context-ui/ContextTreemap.d.ts.map +1 -1
  53. package/dist/context-ui/ContextTreemap.js +5 -2
  54. package/dist/context-ui/ContextTreemap.js.map +1 -1
  55. package/dist/context-ui/ContextXRayPanel.d.ts +3 -2
  56. package/dist/context-ui/ContextXRayPanel.d.ts.map +1 -1
  57. package/dist/context-ui/ContextXRayPanel.js +112 -23
  58. package/dist/context-ui/ContextXRayPanel.js.map +1 -1
  59. package/dist/context-ui/types.d.ts +2 -0
  60. package/dist/context-ui/types.d.ts.map +1 -1
  61. package/dist/context-ui/types.js +7 -1
  62. package/dist/context-ui/types.js.map +1 -1
  63. package/dist/design-system/default-adapter.d.ts.map +1 -1
  64. package/dist/design-system/default-adapter.js +4 -1
  65. package/dist/design-system/default-adapter.js.map +1 -1
  66. package/dist/design-system/default-adapter.spec.js +12 -0
  67. package/dist/design-system/default-adapter.spec.js.map +1 -1
  68. package/dist/design-system/types.d.ts +2 -0
  69. package/dist/design-system/types.d.ts.map +1 -1
  70. package/dist/provider.d.ts.map +1 -1
  71. package/dist/provider.js +5 -2
  72. package/dist/provider.js.map +1 -1
  73. package/dist/sharing/SharePrimitives.d.ts +31 -0
  74. package/dist/sharing/SharePrimitives.d.ts.map +1 -0
  75. package/dist/sharing/SharePrimitives.js +45 -0
  76. package/dist/sharing/SharePrimitives.js.map +1 -0
  77. package/dist/sharing/index.d.ts +1 -0
  78. package/dist/sharing/index.d.ts.map +1 -1
  79. package/dist/sharing/index.js +1 -0
  80. package/dist/sharing/index.js.map +1 -1
  81. package/package.json +2 -2
  82. package/src/chat-history.css +3 -1
  83. package/src/composer/ComposerPlusMenu.tsx +170 -52
  84. package/src/composer/MentionPopover.tsx +43 -7
  85. package/src/composer/PastedTextChip.spec.tsx +58 -0
  86. package/src/composer/PastedTextChip.tsx +35 -7
  87. package/src/composer/PromptComposer.tsx +28 -7
  88. package/src/composer/TiptapComposer.spec.ts +85 -0
  89. package/src/composer/TiptapComposer.tsx +338 -81
  90. package/src/composer/VoiceButton.tsx +36 -11
  91. package/src/composer/runtime-adapters.spec.tsx +19 -0
  92. package/src/composer/runtime-adapters.tsx +12 -2
  93. package/src/composer/useRealtimeVoiceMode.spec.ts +34 -0
  94. package/src/composer/useRealtimeVoiceMode.tsx +219 -69
  95. package/src/context-ui/ContextMeter.spec.tsx +73 -0
  96. package/src/context-ui/ContextMeter.tsx +34 -4
  97. package/src/context-ui/ContextSegmentRow.tsx +111 -22
  98. package/src/context-ui/ContextTreemap.tsx +7 -1
  99. package/src/context-ui/ContextXRayPanel.tsx +138 -24
  100. package/src/context-ui/types.ts +14 -0
  101. package/src/design-system/default-adapter.spec.tsx +15 -0
  102. package/src/design-system/default-adapter.tsx +5 -1
  103. package/src/design-system/types.ts +2 -0
  104. package/src/provider.tsx +9 -1
  105. package/src/sharing/SharePrimitives.tsx +185 -0
  106. package/src/sharing/index.ts +8 -0
@@ -52,6 +52,7 @@ import {
52
52
  type AgentChatContextItem,
53
53
  type AgentComposerReference,
54
54
  type AgentComposerReferenceInsertPayload,
55
+ type ComposerTranslate,
55
56
  type ReasoningEffort,
56
57
  type VoiceContextPack,
57
58
  useComposerRuntimeAdapters,
@@ -289,10 +290,12 @@ export function getOversizedDocumentAttachmentError(
289
290
  options: {
290
291
  maxBytes?: number;
291
292
  label?: string;
293
+ translate?: ComposerTranslate;
292
294
  } = {},
293
295
  ): string | null {
294
296
  const maxBytes = options.maxBytes ?? MAX_DOCUMENT_ATTACHMENT_BYTES;
295
297
  const label = options.label ?? "PDFs";
298
+ const t = options.translate;
296
299
  for (const attachment of attachments) {
297
300
  if (!attachment || typeof attachment !== "object") continue;
298
301
  const candidate = attachment as Record<string, unknown>;
@@ -306,7 +309,17 @@ export function getOversizedDocumentAttachmentError(
306
309
  : file.name;
307
310
  const mb = (file.size / 1024 / 1024).toFixed(1);
308
311
  const maxMb = (maxBytes / 1024 / 1024).toFixed(0);
309
- return `"${name}" is ${mb} MB. ${label} are capped at ${maxMb} MB to stay within message limits. Please reduce the file size or split it into smaller parts.`;
312
+ return (
313
+ t?.("agentChat.composer.documentTooLarge", {
314
+ defaultValue:
315
+ '"{{name}}" is {{size}} MB. {{label}} are capped at {{maxSize}} MB to stay within message limits. Please reduce the file size or split it into smaller parts.',
316
+ name,
317
+ size: mb,
318
+ label,
319
+ maxSize: maxMb,
320
+ }) ??
321
+ `"${name}" is ${mb} MB. ${label} are capped at ${maxMb} MB to stay within message limits. Please reduce the file size or split it into smaller parts.`
322
+ );
310
323
  }
311
324
  return null;
312
325
  }
@@ -356,10 +369,13 @@ export function displayableComposerModeMessage(options: {
356
369
  messagePrefix: string;
357
370
  trimmedText: string;
358
371
  attachmentCount: number;
372
+ attachedContextFallback?: string;
359
373
  }): string {
360
374
  const modePrompt =
361
375
  options.trimmedText ||
362
- (options.attachmentCount > 0 ? "Use the attached context." : "");
376
+ (options.attachmentCount > 0
377
+ ? (options.attachedContextFallback ?? "Use the attached context.")
378
+ : "");
363
379
  return `${options.messagePrefix}${modePrompt}`;
364
380
  }
365
381
 
@@ -408,14 +424,52 @@ export function handleComposerFileDrop(options: {
408
424
  return true;
409
425
  }
410
426
 
411
- const BUILT_IN_COMMANDS: SlashCommand[] = [
412
- { name: "clear", description: "Start a new chat", icon: "clear" },
413
- { name: "new", description: "Start a new chat", icon: "new" },
414
- { name: "history", description: "Browse all chats", icon: "history" },
415
- { name: "plan", description: "Switch to read-only planning", icon: "plan" },
416
- { name: "act", description: "Switch back to acting", icon: "act" },
417
- { name: "help", description: "Show available commands", icon: "help" },
418
- ];
427
+ function builtInCommands(t: ComposerTranslate): SlashCommand[] {
428
+ return [
429
+ {
430
+ name: "clear",
431
+ description: t("agentChat.commands.clearShort", {
432
+ defaultValue: "Start a new chat",
433
+ }),
434
+ icon: "clear",
435
+ },
436
+ {
437
+ name: "new",
438
+ description: t("agentChat.commands.newShort", {
439
+ defaultValue: "Start a new chat",
440
+ }),
441
+ icon: "new",
442
+ },
443
+ {
444
+ name: "history",
445
+ description: t("agentChat.commands.history", {
446
+ defaultValue: "Browse all chats",
447
+ }),
448
+ icon: "history",
449
+ },
450
+ {
451
+ name: "plan",
452
+ description: t("agentChat.commands.plan", {
453
+ defaultValue: "Switch to read-only planning",
454
+ }),
455
+ icon: "plan",
456
+ },
457
+ {
458
+ name: "act",
459
+ description: t("agentChat.commands.act", {
460
+ defaultValue: "Switch back to acting",
461
+ }),
462
+ icon: "act",
463
+ },
464
+ {
465
+ name: "help",
466
+ description: t("agentChat.commands.help", {
467
+ defaultValue: "Show available commands",
468
+ }),
469
+ icon: "help",
470
+ },
471
+ ];
472
+ }
419
473
 
420
474
  function normalizeSlashCommandName(name: string): string {
421
475
  return name.replace(/^\/+/, "").trim().toLowerCase();
@@ -448,19 +502,13 @@ function mergeSlashSkills(skills: SkillResult[]): SkillResult[] {
448
502
  const COMPOSER_MODE_CONFIGS: Record<
449
503
  ComposerMode,
450
504
  {
451
- label: string;
452
505
  icon: React.ComponentType<{ className?: string }>;
453
- placeholder: string;
454
- messagePrefix: string;
455
506
  getContext: (prompt: string) => string;
456
507
  beforeSend?: () => void;
457
508
  }
458
509
  > = {
459
510
  skill: {
460
- label: "Create Skill",
461
511
  icon: IconBulb,
462
- placeholder: "Describe the skill you want to create...",
463
- messagePrefix: "Create a skill: ",
464
512
  getContext: (prompt) =>
465
513
  `The user wants to create an agent skill. Their description: "${prompt}"
466
514
 
@@ -482,10 +530,7 @@ After creating, update the shared AGENTS.md resource to reference the new skill
482
530
  Keep the skill concise (under 500 lines) and actionable.`,
483
531
  },
484
532
  job: {
485
- label: "Schedule Task",
486
533
  icon: IconClock,
487
- placeholder: "Describe what should happen and when...",
488
- messagePrefix: "Create a recurring job: ",
489
534
  getContext: (prompt) =>
490
535
  `The user wants to create a recurring job. Their description: "${prompt}"
491
536
 
@@ -498,10 +543,7 @@ Use the manage-jobs tool with action "create" to create this. You need to:
498
543
  The job will run automatically on the schedule. Make the instructions specific — include which actions to call and what to do with results.`,
499
544
  },
500
545
  automation: {
501
- label: "Create Automation",
502
546
  icon: IconBolt,
503
- placeholder: "Describe what you want to automate...",
504
- messagePrefix: "Create an automation: ",
505
547
  beforeSend: () => {
506
548
  window.dispatchEvent(
507
549
  new CustomEvent("agent-panel:set-mode", {
@@ -515,10 +557,7 @@ The job will run automatically on the schedule. Make the instructions specific
515
557
  Use manage-automations with action=define to create it. Ask clarifying questions if needed about what event to trigger on, conditions, and what actions to take.`,
516
558
  },
517
559
  extension: {
518
- label: "Create Extension",
519
560
  icon: IconTool,
520
- placeholder: "Describe the interactive extension you want to build...",
521
- messagePrefix: "Create an extension: ",
522
561
  getContext: (prompt) =>
523
562
  `The user wants to create an interactive extension (sandboxed mini-app). Their description: "${prompt}"
524
563
 
@@ -532,6 +571,60 @@ Prefer appAction()/appFetch() for app data. Some actions return JSON strings for
532
571
  },
533
572
  };
534
573
 
574
+ function localizedComposerModeConfig(mode: ComposerMode, t: ComposerTranslate) {
575
+ const config = COMPOSER_MODE_CONFIGS[mode];
576
+ const copies: Record<
577
+ ComposerMode,
578
+ { label: string; placeholder: string; messagePrefix: string }
579
+ > = {
580
+ skill: {
581
+ label: t("agentChat.composer.createSkill", {
582
+ defaultValue: "Create Skill",
583
+ }),
584
+ placeholder: t("agentChat.composer.describeSkill", {
585
+ defaultValue: "Describe the skill you want to create...",
586
+ }),
587
+ messagePrefix: t("agentChat.composer.createSkillPrefix", {
588
+ defaultValue: "Create a skill: ",
589
+ }),
590
+ },
591
+ job: {
592
+ label: t("agentChat.composer.scheduleTask", {
593
+ defaultValue: "Schedule Task",
594
+ }),
595
+ placeholder: t("agentChat.composer.describeSchedule", {
596
+ defaultValue: "Describe what should happen and when...",
597
+ }),
598
+ messagePrefix: t("agentChat.composer.scheduleTaskPrefix", {
599
+ defaultValue: "Create a recurring job: ",
600
+ }),
601
+ },
602
+ automation: {
603
+ label: t("agentChat.composer.createAutomation", {
604
+ defaultValue: "Create Automation",
605
+ }),
606
+ placeholder: t("agentChat.composer.describeAutomation", {
607
+ defaultValue: "Describe what you want to automate...",
608
+ }),
609
+ messagePrefix: t("agentChat.composer.createAutomationPrefix", {
610
+ defaultValue: "Create an automation: ",
611
+ }),
612
+ },
613
+ extension: {
614
+ label: t("agentChat.composer.createExtension", {
615
+ defaultValue: "Create Extension",
616
+ }),
617
+ placeholder: t("agentChat.composer.describeExtension", {
618
+ defaultValue: "Describe the interactive extension you want to build...",
619
+ }),
620
+ messagePrefix: t("agentChat.composer.createExtensionPrefix", {
621
+ defaultValue: "Create an extension: ",
622
+ }),
623
+ },
624
+ };
625
+ return { ...config, ...copies[mode] };
626
+ }
627
+
535
628
  function ComposerModeChip({
536
629
  mode,
537
630
  onRemove,
@@ -539,7 +632,8 @@ function ComposerModeChip({
539
632
  mode: ComposerMode;
540
633
  onRemove: () => void;
541
634
  }) {
542
- const config = COMPOSER_MODE_CONFIGS[mode];
635
+ const t = useComposerRuntimeAdapters().translate!;
636
+ const config = localizedComposerModeConfig(mode, t);
543
637
  const Icon = config.icon;
544
638
  return (
545
639
  <span className="inline-flex items-center gap-1 rounded-md border border-border bg-muted/50 px-1.5 py-0.5 text-xs font-medium text-foreground">
@@ -761,25 +855,37 @@ function ModeSelector({
761
855
  mode,
762
856
  onChange,
763
857
  planModeDisabled = false,
764
- planModeDisabledReason = "Open Agent Native Desktop to use Plan mode.",
858
+ planModeDisabledReason,
765
859
  }: {
766
860
  mode: ExecMode;
767
861
  onChange: (mode: ExecMode) => void;
768
862
  planModeDisabled?: boolean;
769
863
  planModeDisabledReason?: string;
770
864
  }) {
865
+ const t = useComposerRuntimeAdapters().translate!;
771
866
  const [open, setOpen] = useState(false);
867
+ const resolvedPlanModeDisabledReason =
868
+ planModeDisabledReason ??
869
+ t("agentChat.composer.planDesktopRequired", {
870
+ defaultValue: "Open Agent Native Desktop to use Plan mode.",
871
+ });
772
872
 
773
873
  return (
774
874
  <Popover open={open} onOpenChange={setOpen}>
775
875
  <PopoverTrigger asChild>
776
876
  <button
777
877
  type="button"
778
- aria-label={mode === "build" ? "Act mode" : "Plan mode"}
878
+ aria-label={
879
+ mode === "build"
880
+ ? t("agentChat.composer.actMode", { defaultValue: "Act mode" })
881
+ : t("agentChat.plan.mode", { defaultValue: "Plan mode" })
882
+ }
779
883
  data-agent-composer-slot="mode-button"
780
884
  className="agent-composer-mode-button shrink-0 flex items-center gap-1 rounded-md px-2 py-1 text-[12px] font-medium text-muted-foreground hover:bg-accent/50 hover:text-foreground"
781
885
  >
782
- {mode === "build" ? "Act" : "Plan"}
886
+ {mode === "build"
887
+ ? t("agentChat.plan.act", { defaultValue: "Act" })
888
+ : t("agentChat.composer.plan", { defaultValue: "Plan" })}
783
889
  <IconChevronDown className="h-3 w-3 opacity-60" />
784
890
  </button>
785
891
  </PopoverTrigger>
@@ -802,9 +908,13 @@ function ModeSelector({
802
908
  >
803
909
  <IconPencil className="h-4 w-4 shrink-0 text-muted-foreground" />
804
910
  <div className="flex-1 min-w-0">
805
- <span className="font-medium text-foreground text-[13px]">Act</span>
911
+ <span className="font-medium text-foreground text-[13px]">
912
+ {t("agentChat.plan.act", { defaultValue: "Act" })}
913
+ </span>
806
914
  <p className="text-[11px] text-muted-foreground mt-0.5">
807
- Use tools and make approved changes
915
+ {t("agentChat.composer.actDescription", {
916
+ defaultValue: "Use tools and make approved changes",
917
+ })}
808
918
  </p>
809
919
  </div>
810
920
  {mode === "build" && (
@@ -814,7 +924,7 @@ function ModeSelector({
814
924
  <button
815
925
  type="button"
816
926
  disabled={planModeDisabled}
817
- title={planModeDisabled ? planModeDisabledReason : undefined}
927
+ title={planModeDisabled ? resolvedPlanModeDisabledReason : undefined}
818
928
  onClick={() => {
819
929
  if (planModeDisabled) return;
820
930
  onChange("plan");
@@ -829,12 +939,14 @@ function ModeSelector({
829
939
  <IconClipboardList className="h-4 w-4 shrink-0 text-muted-foreground" />
830
940
  <div className="flex-1 min-w-0">
831
941
  <span className="font-medium text-foreground text-[13px]">
832
- Plan
942
+ {t("agentChat.composer.plan", { defaultValue: "Plan" })}
833
943
  </span>
834
944
  <p className="text-[11px] text-muted-foreground mt-0.5">
835
945
  {planModeDisabled
836
- ? planModeDisabledReason
837
- : "Read-only research and approval first"}
946
+ ? resolvedPlanModeDisabledReason
947
+ : t("agentChat.composer.planDescription", {
948
+ defaultValue: "Read-only research and approval first",
949
+ })}
838
950
  </p>
839
951
  </div>
840
952
  {mode === "plan" && !planModeDisabled && (
@@ -879,7 +991,14 @@ export function shouldShowOnlyConnectPath(
879
991
  return showBuilderCta && groups.every((group) => !group.configured);
880
992
  }
881
993
 
882
- function friendlyModelName(model: string): string {
994
+ function friendlyModelName(model: string, t?: ComposerTranslate): string {
995
+ if (model === "auto") {
996
+ return (
997
+ t?.("agentChat.composer.defaultModel", {
998
+ defaultValue: "Default model",
999
+ }) ?? "Default model"
1000
+ );
1001
+ }
883
1002
  if (FRIENDLY_MODEL_NAMES[model]) return FRIENDLY_MODEL_NAMES[model];
884
1003
  // Claude: claude-{tier}-{major}[-minor][-dateYYYYMMDD] → Tier Major[.Minor]
885
1004
  const claude = model.match(
@@ -929,7 +1048,10 @@ function friendlyModelName(model: string): string {
929
1048
  return model;
930
1049
  }
931
1050
 
932
- export function compactComposerModelName(model: string): string {
1051
+ export function compactComposerModelName(
1052
+ model: string,
1053
+ t?: ComposerTranslate,
1054
+ ): string {
933
1055
  const gpt56Variant = model.match(
934
1056
  /^(?:openai\/)?gpt-5[.-]6[.-](sol|terra|luna)$/i,
935
1057
  )?.[1];
@@ -938,16 +1060,62 @@ export function compactComposerModelName(model: string): string {
938
1060
  gpt56Variant[0].toUpperCase() + gpt56Variant.slice(1).toLowerCase();
939
1061
  return `GPT-5.6 ${variant}`;
940
1062
  }
941
- return friendlyModelName(model);
1063
+ return friendlyModelName(model, t);
942
1064
  }
943
1065
 
944
1066
  export function compactComposerReasoningEffortLabel(
945
1067
  effort: ReasoningEffort,
1068
+ t?: ComposerTranslate,
946
1069
  ): string {
947
- if (effort === "medium" || effort === "auto") return "Med";
948
- if (effort === "minimal") return "Min";
949
- if (effort === "xhigh") return "XHigh";
950
- return reasoningEffortLabel(effort);
1070
+ if (effort === "medium" || effort === "auto") {
1071
+ return (
1072
+ t?.("agentChat.composer.reasoningMediumShort", {
1073
+ defaultValue: "Med",
1074
+ }) ?? "Med"
1075
+ );
1076
+ }
1077
+ if (effort === "minimal") {
1078
+ return (
1079
+ t?.("agentChat.composer.reasoningMinimalShort", {
1080
+ defaultValue: "Min",
1081
+ }) ?? "Min"
1082
+ );
1083
+ }
1084
+ if (effort === "xhigh") {
1085
+ return (
1086
+ t?.("agentChat.composer.reasoningExtraHighShort", {
1087
+ defaultValue: "XHigh",
1088
+ }) ?? "XHigh"
1089
+ );
1090
+ }
1091
+ return t
1092
+ ? localizedReasoningEffortLabel(t, effort, reasoningEffortLabel(effort))
1093
+ : reasoningEffortLabel(effort);
1094
+ }
1095
+
1096
+ function localizedReasoningEffortLabel(
1097
+ t: ComposerTranslate,
1098
+ effort: ReasoningEffort,
1099
+ defaultValue: string,
1100
+ ): string {
1101
+ switch (effort) {
1102
+ case "auto":
1103
+ return t("agentChat.composer.reasoningEffort.auto", { defaultValue });
1104
+ case "none":
1105
+ return t("agentChat.composer.reasoningEffort.none", { defaultValue });
1106
+ case "minimal":
1107
+ return t("agentChat.composer.reasoningEffort.minimal", { defaultValue });
1108
+ case "low":
1109
+ return t("agentChat.composer.reasoningEffort.low", { defaultValue });
1110
+ case "medium":
1111
+ return t("agentChat.composer.reasoningEffort.medium", { defaultValue });
1112
+ case "high":
1113
+ return t("agentChat.composer.reasoningEffort.high", { defaultValue });
1114
+ case "xhigh":
1115
+ return t("agentChat.composer.reasoningEffort.xhigh", { defaultValue });
1116
+ case "max":
1117
+ return t("agentChat.composer.reasoningEffort.max", { defaultValue });
1118
+ }
951
1119
  }
952
1120
 
953
1121
  /**
@@ -999,16 +1167,21 @@ const MODEL_COST_TIERS: ReadonlyArray<readonly [string, 1 | 2 | 3]> = [
999
1167
  ["pro", 3],
1000
1168
  ];
1001
1169
 
1002
- const COST_TIER_LABELS = { 1: "Lower", 2: "Medium", 3: "Higher" } as const;
1003
-
1004
1170
  export function composerModelCostTier(model: string): 1 | 2 | 3 | undefined {
1005
1171
  const normalized = model.toLowerCase();
1006
1172
  return MODEL_COST_TIERS.find(([token]) => normalized.includes(token))?.[1];
1007
1173
  }
1008
1174
 
1009
1175
  function ModelCostTier({ model }: { model: string }) {
1176
+ const t = useComposerRuntimeAdapters().translate!;
1010
1177
  const tier = composerModelCostTier(model);
1011
1178
  if (!tier) return null;
1179
+ const costLabel =
1180
+ tier === 1
1181
+ ? t("agentChat.composer.costLower", { defaultValue: "Lower cost" })
1182
+ : tier === 2
1183
+ ? t("agentChat.composer.costMedium", { defaultValue: "Medium cost" })
1184
+ : t("agentChat.composer.costHigher", { defaultValue: "Higher cost" });
1012
1185
  return (
1013
1186
  <>
1014
1187
  <span
@@ -1017,7 +1190,7 @@ function ModelCostTier({ model }: { model: string }) {
1017
1190
  >
1018
1191
  {"$".repeat(tier)}
1019
1192
  </span>
1020
- <span className="sr-only">{COST_TIER_LABELS[tier]} cost</span>
1193
+ <span className="sr-only">{costLabel}</span>
1021
1194
  </>
1022
1195
  );
1023
1196
  }
@@ -1104,7 +1277,15 @@ function ModelSelector({
1104
1277
  const selectedEffort =
1105
1278
  reasoning?.resolve?.(model, effort) ??
1106
1279
  resolveReasoningEffortSelection(model, effort ?? defaultEffort);
1107
- const effortLabel = reasoning?.label ?? reasoningEffortLabel;
1280
+ const effortLabel = useCallback(
1281
+ (value: ReasoningEffort) =>
1282
+ localizedReasoningEffortLabel(
1283
+ t,
1284
+ value,
1285
+ (reasoning?.label ?? reasoningEffortLabel)(value),
1286
+ ),
1287
+ [reasoning?.label, t],
1288
+ );
1108
1289
 
1109
1290
  // Collapse non-selected families by default. The family containing the
1110
1291
  // currently-selected model stays expanded so the user sees their pick at
@@ -1202,19 +1383,23 @@ function ModelSelector({
1202
1383
  <button
1203
1384
  type="button"
1204
1385
  data-agent-composer-slot="model-button"
1205
- aria-label={`Model: ${friendlyModelName(model)}${
1386
+ aria-label={`${t("agentChat.composer.model", {
1387
+ defaultValue: "Model",
1388
+ })}: ${friendlyModelName(model, t)}${
1206
1389
  effortOptions.length > 0
1207
- ? `. Effort: ${effortLabel(selectedEffort)}`
1390
+ ? `. ${t("agentChat.composer.reasoning", {
1391
+ defaultValue: "Reasoning",
1392
+ })}: ${effortLabel(selectedEffort)}`
1208
1393
  : ""
1209
1394
  }`}
1210
1395
  className="agent-composer-model-button flex min-w-0 max-w-[10.5rem] shrink items-center gap-1 rounded-md px-2 py-1 text-[12px] font-medium text-muted-foreground hover:bg-accent/50 hover:text-foreground"
1211
1396
  >
1212
1397
  <span className="min-w-0 truncate">
1213
- {compactComposerModelName(model)}
1398
+ {compactComposerModelName(model, t)}
1214
1399
  </span>
1215
1400
  {effortOptions.length > 0 && (
1216
1401
  <span className="agent-composer-model-effort min-w-0 shrink truncate text-muted-foreground/70">
1217
- · {compactComposerReasoningEffortLabel(selectedEffort)}
1402
+ · {compactComposerReasoningEffortLabel(selectedEffort, t)}
1218
1403
  </span>
1219
1404
  )}
1220
1405
  <IconChevronDown className="h-3 w-3 shrink-0 opacity-60" />
@@ -1308,7 +1493,10 @@ function ModelSelector({
1308
1493
  <IconChevronRight className="h-3 w-3 shrink-0 text-muted-foreground rtl:-scale-x-100" />
1309
1494
  )}
1310
1495
  <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
1311
- {imageModel.label ?? "Image model"}
1496
+ {imageModel.label ??
1497
+ t("agentChat.composer.imageModel", {
1498
+ defaultValue: "Image model",
1499
+ })}
1312
1500
  </span>
1313
1501
  {!imageExpanded && imageModelLabel && (
1314
1502
  <span className="text-[11px] text-muted-foreground/80 truncate">
@@ -1350,7 +1538,7 @@ function ModelSelector({
1350
1538
  className="flex w-full items-center gap-3 px-3 py-1.5 text-start hover:bg-accent/50"
1351
1539
  >
1352
1540
  <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
1353
- Auto
1541
+ {t("agentChat.composer.auto", { defaultValue: "Auto" })}
1354
1542
  </span>
1355
1543
  {model === "auto" && (
1356
1544
  <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
@@ -1376,8 +1564,10 @@ function ModelSelector({
1376
1564
  group.statusLabel ??
1377
1565
  (!group.configured
1378
1566
  ? canConnectLocalRuntime
1379
- ? "sign in"
1380
- : "needs API key"
1567
+ ? t("agentChat.auth.logIn", { defaultValue: "Sign in" })
1568
+ : t("agentChat.composer.needsApiKey", {
1569
+ defaultValue: "needs API key",
1570
+ })
1381
1571
  : undefined);
1382
1572
  return (
1383
1573
  <div key={groupKey}>
@@ -1394,7 +1584,7 @@ function ModelSelector({
1394
1584
  </span>
1395
1585
  {!isExpanded && groupKey === selectedGroupKey && (
1396
1586
  <span className="text-[11px] text-muted-foreground/80 truncate">
1397
- {friendlyModelName(model)}
1587
+ {friendlyModelName(model, t)}
1398
1588
  </span>
1399
1589
  )}
1400
1590
  </button>
@@ -1449,7 +1639,7 @@ function ModelSelector({
1449
1639
  }`}
1450
1640
  >
1451
1641
  <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
1452
- {friendlyModelName(m)}
1642
+ {friendlyModelName(m, t)}
1453
1643
  </span>
1454
1644
  <ModelCostTier model={m} />
1455
1645
  {m === model && group.configured && (
@@ -1478,7 +1668,9 @@ function ModelSelector({
1478
1668
  <IconChevronRight className="h-3 w-3 shrink-0 text-muted-foreground rtl:-scale-x-100" />
1479
1669
  )}
1480
1670
  <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
1481
- Effort
1671
+ {t("agentChat.composer.reasoning", {
1672
+ defaultValue: "Reasoning",
1673
+ })}
1482
1674
  </span>
1483
1675
  {!effortExpanded && (
1484
1676
  <span className="text-[11px] text-muted-foreground/80 truncate">
@@ -1511,13 +1703,20 @@ function ModelSelector({
1511
1703
  }
1512
1704
 
1513
1705
  function ModelSelectorSkeleton() {
1706
+ const t = useComposerRuntimeAdapters().translate!;
1514
1707
  return (
1515
1708
  <div
1516
1709
  className="space-y-1 px-2 py-2"
1517
1710
  role="status"
1518
- aria-label="Loading models"
1711
+ aria-label={t("agentChat.composer.loadingModels", {
1712
+ defaultValue: "Loading models",
1713
+ })}
1519
1714
  >
1520
- <span className="sr-only">Loading models…</span>
1715
+ <span className="sr-only">
1716
+ {t("agentChat.composer.loadingModelsProgress", {
1717
+ defaultValue: "Loading models…",
1718
+ })}
1719
+ </span>
1521
1720
  {["w-24", "w-32", "w-20", "w-28"].map((width, index) => (
1522
1721
  <div key={index} className="flex items-center gap-1.5 px-1 py-1.5">
1523
1722
  <Skeleton className="size-3 rounded-sm" />
@@ -1536,7 +1735,7 @@ type PopoverState = {
1536
1735
  } | null;
1537
1736
 
1538
1737
  export function TiptapComposer({
1539
- placeholder = "Message agent...",
1738
+ placeholder,
1540
1739
  disabled = false,
1541
1740
  maxDocumentAttachmentBytes = MAX_DOCUMENT_ATTACHMENT_BYTES,
1542
1741
  documentAttachmentLimitLabel = "PDFs",
@@ -1651,10 +1850,10 @@ export function TiptapComposer({
1651
1850
  const allSlashCommands = useMemo(
1652
1851
  () =>
1653
1852
  mergeSlashCommands([
1654
- ...(includeDefaultSlashCommands ? BUILT_IN_COMMANDS : []),
1853
+ ...(includeDefaultSlashCommands ? builtInCommands(t) : []),
1655
1854
  ...slashCommands,
1656
1855
  ]),
1657
- [includeDefaultSlashCommands, slashCommands],
1856
+ [includeDefaultSlashCommands, slashCommands, t],
1658
1857
  );
1659
1858
 
1660
1859
  const allSlashSkills = useMemo(
@@ -1736,12 +1935,14 @@ export function TiptapComposer({
1736
1935
  }, [composerRuntime]);
1737
1936
  // Tiptap reads extension config once at init; ref keeps runtime prop
1738
1937
  // changes visible to Placeholder's function form.
1739
- const placeholderRef = useRef(placeholder);
1740
- useEffect(() => {
1741
- placeholderRef.current = composerMode
1742
- ? COMPOSER_MODE_CONFIGS[composerMode].placeholder
1743
- : placeholder;
1744
- }, [placeholder, composerMode]);
1938
+ const resolvedPlaceholder = composerMode
1939
+ ? localizedComposerModeConfig(composerMode, t).placeholder
1940
+ : (placeholder ??
1941
+ t("agentChat.composer.messageAgent", {
1942
+ defaultValue: "Message agent...",
1943
+ }));
1944
+ const placeholderRef = useRef(resolvedPlaceholder);
1945
+ placeholderRef.current = resolvedPlaceholder;
1745
1946
 
1746
1947
  const editor = useEditor({
1747
1948
  extensions: createTiptapComposerExtensions(() => placeholderRef.current),
@@ -1830,7 +2031,10 @@ export function TiptapComposer({
1830
2031
  const msg =
1831
2032
  error instanceof Error
1832
2033
  ? error.message
1833
- : "Could not attach the pasted image. Try a different format.";
2034
+ : t("agentChat.composer.pastedImageError", {
2035
+ defaultValue:
2036
+ "Could not attach the pasted image. Try a different format.",
2037
+ });
1834
2038
  onAttachmentErrorRef.current?.(msg);
1835
2039
  });
1836
2040
  return true;
@@ -1851,7 +2055,9 @@ export function TiptapComposer({
1851
2055
  const msg =
1852
2056
  error instanceof Error
1853
2057
  ? error.message
1854
- : "Could not attach the pasted text.";
2058
+ : t("agentChat.composer.pastedTextError", {
2059
+ defaultValue: "Could not attach the pasted text.",
2060
+ });
1855
2061
  onAttachmentErrorRef.current?.(msg);
1856
2062
  });
1857
2063
  return true;
@@ -1870,7 +2076,10 @@ export function TiptapComposer({
1870
2076
  const msg =
1871
2077
  error instanceof Error
1872
2078
  ? error.message
1873
- : "Could not attach the dropped file. Try a different format.";
2079
+ : t("agentChat.composer.droppedFileError", {
2080
+ defaultValue:
2081
+ "Could not attach the dropped file. Try a different format.",
2082
+ });
1874
2083
  onAttachmentErrorRef.current?.(msg);
1875
2084
  },
1876
2085
  });
@@ -2049,6 +2258,13 @@ export function TiptapComposer({
2049
2258
  },
2050
2259
  });
2051
2260
 
2261
+ // Placeholder decorations are computed by ProseMirror. Dispatching an empty
2262
+ // transaction makes a locale or composer-mode change visible immediately.
2263
+ useEffect(() => {
2264
+ if (!isComposerEditorUsable(editor)) return;
2265
+ editor.view.dispatch(editor.state.tr.setSelection(editor.state.selection));
2266
+ }, [editor, resolvedPlaceholder]);
2267
+
2052
2268
  // A tab can stay mounted while becoming the active composer later. Publish
2053
2269
  // its existing draft when the host starts observing it so contextual UI is
2054
2270
  // correct immediately after a tab switch, not only after the next keystroke.
@@ -2289,7 +2505,12 @@ export function TiptapComposer({
2289
2505
  3200,
2290
2506
  );
2291
2507
  if (activeContext) {
2292
- snippets.push({ label: "Active app context", value: activeContext });
2508
+ snippets.push({
2509
+ label: t("agentChat.composer.activeAppContext", {
2510
+ defaultValue: "Active app context",
2511
+ }),
2512
+ value: activeContext,
2513
+ });
2293
2514
  }
2294
2515
 
2295
2516
  const selectedReferences = trimVoiceContextValue(
@@ -2298,7 +2519,9 @@ export function TiptapComposer({
2298
2519
  );
2299
2520
  if (selectedReferences) {
2300
2521
  snippets.push({
2301
- label: "Selected references",
2522
+ label: t("agentChat.composer.selectedReferences", {
2523
+ defaultValue: "Selected references",
2524
+ }),
2302
2525
  value: selectedReferences,
2303
2526
  });
2304
2527
  }
@@ -2306,16 +2529,35 @@ export function TiptapComposer({
2306
2529
  const draft = isComposerEditorUsable(editor)
2307
2530
  ? trimVoiceContextValue(editor.state.doc.textContent, 1200)
2308
2531
  : null;
2309
- if (draft) snippets.push({ label: "Current draft", value: draft });
2532
+ if (draft) {
2533
+ snippets.push({
2534
+ label: t("agentChat.composer.currentDraft", {
2535
+ defaultValue: "Current draft",
2536
+ }),
2537
+ value: draft,
2538
+ });
2539
+ }
2310
2540
 
2311
2541
  if (typeof document !== "undefined") {
2312
2542
  const title = trimVoiceContextValue(document.title, 160);
2313
- if (title) snippets.push({ label: "Page title", value: title });
2543
+ if (title) {
2544
+ snippets.push({
2545
+ label: t("agentChat.composer.pageTitle", {
2546
+ defaultValue: "Page title",
2547
+ }),
2548
+ value: title,
2549
+ });
2550
+ }
2314
2551
  }
2315
2552
 
2316
2553
  if (typeof window !== "undefined") {
2317
2554
  const route = trimVoiceContextValue(window.location.pathname, 240);
2318
- if (route) snippets.push({ label: "Route", value: route });
2555
+ if (route) {
2556
+ snippets.push({
2557
+ label: t("agentChat.composer.route", { defaultValue: "Route" }),
2558
+ value: route,
2559
+ });
2560
+ }
2319
2561
  }
2320
2562
 
2321
2563
  if (snippets.length === 0) return undefined;
@@ -2324,7 +2566,7 @@ export function TiptapComposer({
2324
2566
  mode: "dictation",
2325
2567
  snippets,
2326
2568
  };
2327
- }, [adapters, contextItems, editor, slotReferences]);
2569
+ }, [adapters, contextItems, editor, slotReferences, t]);
2328
2570
 
2329
2571
  const voice = useVoiceDictation({
2330
2572
  onTranscript: insertTranscript,
@@ -2554,6 +2796,7 @@ export function TiptapComposer({
2554
2796
  {
2555
2797
  maxBytes: maxDocumentAttachmentBytes,
2556
2798
  label: documentAttachmentLimitLabel,
2799
+ translate: t,
2557
2800
  },
2558
2801
  );
2559
2802
  if (oversizedDocumentError) {
@@ -2611,16 +2854,23 @@ export function TiptapComposer({
2611
2854
 
2612
2855
  // Composer mode: send with context via agent chat bridge
2613
2856
  if (composerMode) {
2614
- const config = COMPOSER_MODE_CONFIGS[composerMode];
2857
+ const config = localizedComposerModeConfig(composerMode, t);
2615
2858
  config.beforeSend?.();
2616
2859
  const message = displayableComposerModeMessage({
2617
2860
  messagePrefix: config.messagePrefix,
2618
2861
  trimmedText: trimmed,
2619
2862
  attachmentCount: attachments.length,
2863
+ attachedContextFallback: t("agentChat.composer.useAttachedContext", {
2864
+ defaultValue: "Use the attached context.",
2865
+ }),
2620
2866
  });
2621
2867
  const modePrompt =
2622
2868
  trimmed ||
2623
- (attachments.length > 0 ? "Use the attached context." : "");
2869
+ (attachments.length > 0
2870
+ ? t("agentChat.composer.useAttachedContext", {
2871
+ defaultValue: "Use the attached context.",
2872
+ })
2873
+ : "");
2624
2874
  if (attachments.length > 0) {
2625
2875
  composerRuntime.setText(
2626
2876
  `${message}\n\n<context>\n${config.getContext(modePrompt)}\n</context>`,
@@ -2695,6 +2945,7 @@ export function TiptapComposer({
2695
2945
  syncComposerState,
2696
2946
  voice,
2697
2947
  allSlashCommands,
2948
+ t,
2698
2949
  ],
2699
2950
  );
2700
2951
 
@@ -2946,7 +3197,10 @@ export function TiptapComposer({
2946
3197
  editor.commands.focus("end");
2947
3198
  }
2948
3199
  }}
2949
- aria-label={`Remove ${slotReferenceTitle(ref)} reference`}
3200
+ aria-label={t("agentChat.composer.removeReference", {
3201
+ defaultValue: "Remove {{name}} reference",
3202
+ name: slotReferenceTitle(ref),
3203
+ })}
2950
3204
  className="ms-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
2951
3205
  >
2952
3206
  <IconX className="h-3 w-3" />
@@ -2974,7 +3228,10 @@ export function TiptapComposer({
2974
3228
  setSelectedContextItemKey(null);
2975
3229
  onRemoveContextItem?.(item.key);
2976
3230
  }}
2977
- aria-label={`Remove ${item.title} context`}
3231
+ aria-label={t("agentChat.composer.removeContext", {
3232
+ defaultValue: "Remove {{name}} context",
3233
+ name: item.title,
3234
+ })}
2978
3235
  className="ms-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
2979
3236
  >
2980
3237
  <IconX className="h-3 w-3" />