@agent-native/toolkit 0.14.2 → 0.15.0

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 (79) hide show
  1. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -1
  2. package/dist/composer/ComposerPlusMenu.js +131 -35
  3. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  4. package/dist/composer/MentionPopover.d.ts.map +1 -1
  5. package/dist/composer/MentionPopover.js +40 -5
  6. package/dist/composer/MentionPopover.js.map +1 -1
  7. package/dist/composer/PastedTextChip.d.ts.map +1 -1
  8. package/dist/composer/PastedTextChip.js +26 -2
  9. package/dist/composer/PastedTextChip.js.map +1 -1
  10. package/dist/composer/PastedTextChip.spec.d.ts +2 -0
  11. package/dist/composer/PastedTextChip.spec.d.ts.map +1 -0
  12. package/dist/composer/PastedTextChip.spec.js +42 -0
  13. package/dist/composer/PastedTextChip.spec.js.map +1 -0
  14. package/dist/composer/PromptComposer.d.ts.map +1 -1
  15. package/dist/composer/PromptComposer.js +26 -5
  16. package/dist/composer/PromptComposer.js.map +1 -1
  17. package/dist/composer/TiptapComposer.d.ts +5 -3
  18. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  19. package/dist/composer/TiptapComposer.js +291 -80
  20. package/dist/composer/TiptapComposer.js.map +1 -1
  21. package/dist/composer/TiptapComposer.spec.js +60 -3
  22. package/dist/composer/TiptapComposer.spec.js.map +1 -1
  23. package/dist/composer/VoiceButton.d.ts.map +1 -1
  24. package/dist/composer/VoiceButton.js +24 -5
  25. package/dist/composer/VoiceButton.js.map +1 -1
  26. package/dist/composer/runtime-adapters.d.ts +1 -0
  27. package/dist/composer/runtime-adapters.d.ts.map +1 -1
  28. package/dist/composer/runtime-adapters.js +8 -1
  29. package/dist/composer/runtime-adapters.js.map +1 -1
  30. package/dist/composer/runtime-adapters.spec.js +15 -0
  31. package/dist/composer/runtime-adapters.spec.js.map +1 -1
  32. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  33. package/dist/composer/useRealtimeVoiceMode.js +204 -68
  34. package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
  35. package/dist/composer/useRealtimeVoiceMode.spec.js +27 -1
  36. package/dist/composer/useRealtimeVoiceMode.spec.js.map +1 -1
  37. package/dist/context-ui/ContextMeter.d.ts +3 -2
  38. package/dist/context-ui/ContextMeter.d.ts.map +1 -1
  39. package/dist/context-ui/ContextMeter.js +23 -3
  40. package/dist/context-ui/ContextMeter.js.map +1 -1
  41. package/dist/context-ui/ContextMeter.spec.d.ts +2 -0
  42. package/dist/context-ui/ContextMeter.spec.d.ts.map +1 -0
  43. package/dist/context-ui/ContextMeter.spec.js +52 -0
  44. package/dist/context-ui/ContextMeter.spec.js.map +1 -0
  45. package/dist/context-ui/ContextSegmentRow.d.ts +5 -3
  46. package/dist/context-ui/ContextSegmentRow.d.ts.map +1 -1
  47. package/dist/context-ui/ContextSegmentRow.js +74 -7
  48. package/dist/context-ui/ContextSegmentRow.js.map +1 -1
  49. package/dist/context-ui/ContextTreemap.d.ts +3 -2
  50. package/dist/context-ui/ContextTreemap.d.ts.map +1 -1
  51. package/dist/context-ui/ContextTreemap.js +5 -2
  52. package/dist/context-ui/ContextTreemap.js.map +1 -1
  53. package/dist/context-ui/ContextXRayPanel.d.ts +3 -2
  54. package/dist/context-ui/ContextXRayPanel.d.ts.map +1 -1
  55. package/dist/context-ui/ContextXRayPanel.js +112 -23
  56. package/dist/context-ui/ContextXRayPanel.js.map +1 -1
  57. package/dist/context-ui/types.d.ts +2 -0
  58. package/dist/context-ui/types.d.ts.map +1 -1
  59. package/dist/context-ui/types.js +7 -1
  60. package/dist/context-ui/types.js.map +1 -1
  61. package/package.json +2 -2
  62. package/src/composer/ComposerPlusMenu.tsx +170 -52
  63. package/src/composer/MentionPopover.tsx +43 -7
  64. package/src/composer/PastedTextChip.spec.tsx +58 -0
  65. package/src/composer/PastedTextChip.tsx +35 -7
  66. package/src/composer/PromptComposer.tsx +28 -7
  67. package/src/composer/TiptapComposer.spec.ts +90 -3
  68. package/src/composer/TiptapComposer.tsx +341 -82
  69. package/src/composer/VoiceButton.tsx +36 -11
  70. package/src/composer/runtime-adapters.spec.tsx +19 -0
  71. package/src/composer/runtime-adapters.tsx +12 -2
  72. package/src/composer/useRealtimeVoiceMode.spec.ts +34 -0
  73. package/src/composer/useRealtimeVoiceMode.tsx +219 -69
  74. package/src/context-ui/ContextMeter.spec.tsx +73 -0
  75. package/src/context-ui/ContextMeter.tsx +34 -4
  76. package/src/context-ui/ContextSegmentRow.tsx +111 -22
  77. package/src/context-ui/ContextTreemap.tsx +7 -1
  78. package/src/context-ui/ContextXRayPanel.tsx +138 -24
  79. package/src/context-ui/types.ts +14 -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,23 +1048,74 @@ 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];
936
1058
  if (gpt56Variant) {
937
- return gpt56Variant[0].toUpperCase() + gpt56Variant.slice(1).toLowerCase();
1059
+ const variant =
1060
+ gpt56Variant[0].toUpperCase() + gpt56Variant.slice(1).toLowerCase();
1061
+ return `GPT-5.6 ${variant}`;
938
1062
  }
939
- return friendlyModelName(model);
1063
+ return friendlyModelName(model, t);
940
1064
  }
941
1065
 
942
1066
  export function compactComposerReasoningEffortLabel(
943
1067
  effort: ReasoningEffort,
1068
+ t?: ComposerTranslate,
944
1069
  ): string {
945
- if (effort === "medium" || effort === "auto") return "Med";
946
- if (effort === "minimal") return "Min";
947
- if (effort === "xhigh") return "XHigh";
948
- 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
+ }
949
1119
  }
950
1120
 
951
1121
  /**
@@ -997,16 +1167,21 @@ const MODEL_COST_TIERS: ReadonlyArray<readonly [string, 1 | 2 | 3]> = [
997
1167
  ["pro", 3],
998
1168
  ];
999
1169
 
1000
- const COST_TIER_LABELS = { 1: "Lower", 2: "Medium", 3: "Higher" } as const;
1001
-
1002
1170
  export function composerModelCostTier(model: string): 1 | 2 | 3 | undefined {
1003
1171
  const normalized = model.toLowerCase();
1004
1172
  return MODEL_COST_TIERS.find(([token]) => normalized.includes(token))?.[1];
1005
1173
  }
1006
1174
 
1007
1175
  function ModelCostTier({ model }: { model: string }) {
1176
+ const t = useComposerRuntimeAdapters().translate!;
1008
1177
  const tier = composerModelCostTier(model);
1009
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" });
1010
1185
  return (
1011
1186
  <>
1012
1187
  <span
@@ -1015,7 +1190,7 @@ function ModelCostTier({ model }: { model: string }) {
1015
1190
  >
1016
1191
  {"$".repeat(tier)}
1017
1192
  </span>
1018
- <span className="sr-only">{COST_TIER_LABELS[tier]} cost</span>
1193
+ <span className="sr-only">{costLabel}</span>
1019
1194
  </>
1020
1195
  );
1021
1196
  }
@@ -1102,7 +1277,15 @@ function ModelSelector({
1102
1277
  const selectedEffort =
1103
1278
  reasoning?.resolve?.(model, effort) ??
1104
1279
  resolveReasoningEffortSelection(model, effort ?? defaultEffort);
1105
- 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
+ );
1106
1289
 
1107
1290
  // Collapse non-selected families by default. The family containing the
1108
1291
  // currently-selected model stays expanded so the user sees their pick at
@@ -1200,19 +1383,23 @@ function ModelSelector({
1200
1383
  <button
1201
1384
  type="button"
1202
1385
  data-agent-composer-slot="model-button"
1203
- aria-label={`Model: ${friendlyModelName(model)}${
1386
+ aria-label={`${t("agentChat.composer.model", {
1387
+ defaultValue: "Model",
1388
+ })}: ${friendlyModelName(model, t)}${
1204
1389
  effortOptions.length > 0
1205
- ? `. Effort: ${effortLabel(selectedEffort)}`
1390
+ ? `. ${t("agentChat.composer.reasoning", {
1391
+ defaultValue: "Reasoning",
1392
+ })}: ${effortLabel(selectedEffort)}`
1206
1393
  : ""
1207
1394
  }`}
1208
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"
1209
1396
  >
1210
1397
  <span className="min-w-0 truncate">
1211
- {compactComposerModelName(model)}
1398
+ {compactComposerModelName(model, t)}
1212
1399
  </span>
1213
1400
  {effortOptions.length > 0 && (
1214
1401
  <span className="agent-composer-model-effort min-w-0 shrink truncate text-muted-foreground/70">
1215
- · {compactComposerReasoningEffortLabel(selectedEffort)}
1402
+ · {compactComposerReasoningEffortLabel(selectedEffort, t)}
1216
1403
  </span>
1217
1404
  )}
1218
1405
  <IconChevronDown className="h-3 w-3 shrink-0 opacity-60" />
@@ -1306,7 +1493,10 @@ function ModelSelector({
1306
1493
  <IconChevronRight className="h-3 w-3 shrink-0 text-muted-foreground rtl:-scale-x-100" />
1307
1494
  )}
1308
1495
  <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
1309
- {imageModel.label ?? "Image model"}
1496
+ {imageModel.label ??
1497
+ t("agentChat.composer.imageModel", {
1498
+ defaultValue: "Image model",
1499
+ })}
1310
1500
  </span>
1311
1501
  {!imageExpanded && imageModelLabel && (
1312
1502
  <span className="text-[11px] text-muted-foreground/80 truncate">
@@ -1348,7 +1538,7 @@ function ModelSelector({
1348
1538
  className="flex w-full items-center gap-3 px-3 py-1.5 text-start hover:bg-accent/50"
1349
1539
  >
1350
1540
  <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
1351
- Auto
1541
+ {t("agentChat.composer.auto", { defaultValue: "Auto" })}
1352
1542
  </span>
1353
1543
  {model === "auto" && (
1354
1544
  <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
@@ -1374,8 +1564,10 @@ function ModelSelector({
1374
1564
  group.statusLabel ??
1375
1565
  (!group.configured
1376
1566
  ? canConnectLocalRuntime
1377
- ? "sign in"
1378
- : "needs API key"
1567
+ ? t("agentChat.auth.logIn", { defaultValue: "Sign in" })
1568
+ : t("agentChat.composer.needsApiKey", {
1569
+ defaultValue: "needs API key",
1570
+ })
1379
1571
  : undefined);
1380
1572
  return (
1381
1573
  <div key={groupKey}>
@@ -1392,7 +1584,7 @@ function ModelSelector({
1392
1584
  </span>
1393
1585
  {!isExpanded && groupKey === selectedGroupKey && (
1394
1586
  <span className="text-[11px] text-muted-foreground/80 truncate">
1395
- {friendlyModelName(model)}
1587
+ {friendlyModelName(model, t)}
1396
1588
  </span>
1397
1589
  )}
1398
1590
  </button>
@@ -1447,7 +1639,7 @@ function ModelSelector({
1447
1639
  }`}
1448
1640
  >
1449
1641
  <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
1450
- {friendlyModelName(m)}
1642
+ {friendlyModelName(m, t)}
1451
1643
  </span>
1452
1644
  <ModelCostTier model={m} />
1453
1645
  {m === model && group.configured && (
@@ -1476,7 +1668,9 @@ function ModelSelector({
1476
1668
  <IconChevronRight className="h-3 w-3 shrink-0 text-muted-foreground rtl:-scale-x-100" />
1477
1669
  )}
1478
1670
  <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
1479
- Effort
1671
+ {t("agentChat.composer.reasoning", {
1672
+ defaultValue: "Reasoning",
1673
+ })}
1480
1674
  </span>
1481
1675
  {!effortExpanded && (
1482
1676
  <span className="text-[11px] text-muted-foreground/80 truncate">
@@ -1509,13 +1703,20 @@ function ModelSelector({
1509
1703
  }
1510
1704
 
1511
1705
  function ModelSelectorSkeleton() {
1706
+ const t = useComposerRuntimeAdapters().translate!;
1512
1707
  return (
1513
1708
  <div
1514
1709
  className="space-y-1 px-2 py-2"
1515
1710
  role="status"
1516
- aria-label="Loading models"
1711
+ aria-label={t("agentChat.composer.loadingModels", {
1712
+ defaultValue: "Loading models",
1713
+ })}
1517
1714
  >
1518
- <span className="sr-only">Loading models…</span>
1715
+ <span className="sr-only">
1716
+ {t("agentChat.composer.loadingModelsProgress", {
1717
+ defaultValue: "Loading models…",
1718
+ })}
1719
+ </span>
1519
1720
  {["w-24", "w-32", "w-20", "w-28"].map((width, index) => (
1520
1721
  <div key={index} className="flex items-center gap-1.5 px-1 py-1.5">
1521
1722
  <Skeleton className="size-3 rounded-sm" />
@@ -1534,7 +1735,7 @@ type PopoverState = {
1534
1735
  } | null;
1535
1736
 
1536
1737
  export function TiptapComposer({
1537
- placeholder = "Message agent...",
1738
+ placeholder,
1538
1739
  disabled = false,
1539
1740
  maxDocumentAttachmentBytes = MAX_DOCUMENT_ATTACHMENT_BYTES,
1540
1741
  documentAttachmentLimitLabel = "PDFs",
@@ -1649,10 +1850,10 @@ export function TiptapComposer({
1649
1850
  const allSlashCommands = useMemo(
1650
1851
  () =>
1651
1852
  mergeSlashCommands([
1652
- ...(includeDefaultSlashCommands ? BUILT_IN_COMMANDS : []),
1853
+ ...(includeDefaultSlashCommands ? builtInCommands(t) : []),
1653
1854
  ...slashCommands,
1654
1855
  ]),
1655
- [includeDefaultSlashCommands, slashCommands],
1856
+ [includeDefaultSlashCommands, slashCommands, t],
1656
1857
  );
1657
1858
 
1658
1859
  const allSlashSkills = useMemo(
@@ -1734,12 +1935,14 @@ export function TiptapComposer({
1734
1935
  }, [composerRuntime]);
1735
1936
  // Tiptap reads extension config once at init; ref keeps runtime prop
1736
1937
  // changes visible to Placeholder's function form.
1737
- const placeholderRef = useRef(placeholder);
1738
- useEffect(() => {
1739
- placeholderRef.current = composerMode
1740
- ? COMPOSER_MODE_CONFIGS[composerMode].placeholder
1741
- : placeholder;
1742
- }, [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;
1743
1946
 
1744
1947
  const editor = useEditor({
1745
1948
  extensions: createTiptapComposerExtensions(() => placeholderRef.current),
@@ -1828,7 +2031,10 @@ export function TiptapComposer({
1828
2031
  const msg =
1829
2032
  error instanceof Error
1830
2033
  ? error.message
1831
- : "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
+ });
1832
2038
  onAttachmentErrorRef.current?.(msg);
1833
2039
  });
1834
2040
  return true;
@@ -1849,7 +2055,9 @@ export function TiptapComposer({
1849
2055
  const msg =
1850
2056
  error instanceof Error
1851
2057
  ? error.message
1852
- : "Could not attach the pasted text.";
2058
+ : t("agentChat.composer.pastedTextError", {
2059
+ defaultValue: "Could not attach the pasted text.",
2060
+ });
1853
2061
  onAttachmentErrorRef.current?.(msg);
1854
2062
  });
1855
2063
  return true;
@@ -1868,7 +2076,10 @@ export function TiptapComposer({
1868
2076
  const msg =
1869
2077
  error instanceof Error
1870
2078
  ? error.message
1871
- : "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
+ });
1872
2083
  onAttachmentErrorRef.current?.(msg);
1873
2084
  },
1874
2085
  });
@@ -2047,6 +2258,13 @@ export function TiptapComposer({
2047
2258
  },
2048
2259
  });
2049
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
+
2050
2268
  // A tab can stay mounted while becoming the active composer later. Publish
2051
2269
  // its existing draft when the host starts observing it so contextual UI is
2052
2270
  // correct immediately after a tab switch, not only after the next keystroke.
@@ -2287,7 +2505,12 @@ export function TiptapComposer({
2287
2505
  3200,
2288
2506
  );
2289
2507
  if (activeContext) {
2290
- 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
+ });
2291
2514
  }
2292
2515
 
2293
2516
  const selectedReferences = trimVoiceContextValue(
@@ -2296,7 +2519,9 @@ export function TiptapComposer({
2296
2519
  );
2297
2520
  if (selectedReferences) {
2298
2521
  snippets.push({
2299
- label: "Selected references",
2522
+ label: t("agentChat.composer.selectedReferences", {
2523
+ defaultValue: "Selected references",
2524
+ }),
2300
2525
  value: selectedReferences,
2301
2526
  });
2302
2527
  }
@@ -2304,16 +2529,35 @@ export function TiptapComposer({
2304
2529
  const draft = isComposerEditorUsable(editor)
2305
2530
  ? trimVoiceContextValue(editor.state.doc.textContent, 1200)
2306
2531
  : null;
2307
- 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
+ }
2308
2540
 
2309
2541
  if (typeof document !== "undefined") {
2310
2542
  const title = trimVoiceContextValue(document.title, 160);
2311
- 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
+ }
2312
2551
  }
2313
2552
 
2314
2553
  if (typeof window !== "undefined") {
2315
2554
  const route = trimVoiceContextValue(window.location.pathname, 240);
2316
- 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
+ }
2317
2561
  }
2318
2562
 
2319
2563
  if (snippets.length === 0) return undefined;
@@ -2322,7 +2566,7 @@ export function TiptapComposer({
2322
2566
  mode: "dictation",
2323
2567
  snippets,
2324
2568
  };
2325
- }, [adapters, contextItems, editor, slotReferences]);
2569
+ }, [adapters, contextItems, editor, slotReferences, t]);
2326
2570
 
2327
2571
  const voice = useVoiceDictation({
2328
2572
  onTranscript: insertTranscript,
@@ -2552,6 +2796,7 @@ export function TiptapComposer({
2552
2796
  {
2553
2797
  maxBytes: maxDocumentAttachmentBytes,
2554
2798
  label: documentAttachmentLimitLabel,
2799
+ translate: t,
2555
2800
  },
2556
2801
  );
2557
2802
  if (oversizedDocumentError) {
@@ -2609,16 +2854,23 @@ export function TiptapComposer({
2609
2854
 
2610
2855
  // Composer mode: send with context via agent chat bridge
2611
2856
  if (composerMode) {
2612
- const config = COMPOSER_MODE_CONFIGS[composerMode];
2857
+ const config = localizedComposerModeConfig(composerMode, t);
2613
2858
  config.beforeSend?.();
2614
2859
  const message = displayableComposerModeMessage({
2615
2860
  messagePrefix: config.messagePrefix,
2616
2861
  trimmedText: trimmed,
2617
2862
  attachmentCount: attachments.length,
2863
+ attachedContextFallback: t("agentChat.composer.useAttachedContext", {
2864
+ defaultValue: "Use the attached context.",
2865
+ }),
2618
2866
  });
2619
2867
  const modePrompt =
2620
2868
  trimmed ||
2621
- (attachments.length > 0 ? "Use the attached context." : "");
2869
+ (attachments.length > 0
2870
+ ? t("agentChat.composer.useAttachedContext", {
2871
+ defaultValue: "Use the attached context.",
2872
+ })
2873
+ : "");
2622
2874
  if (attachments.length > 0) {
2623
2875
  composerRuntime.setText(
2624
2876
  `${message}\n\n<context>\n${config.getContext(modePrompt)}\n</context>`,
@@ -2693,6 +2945,7 @@ export function TiptapComposer({
2693
2945
  syncComposerState,
2694
2946
  voice,
2695
2947
  allSlashCommands,
2948
+ t,
2696
2949
  ],
2697
2950
  );
2698
2951
 
@@ -2944,7 +3197,10 @@ export function TiptapComposer({
2944
3197
  editor.commands.focus("end");
2945
3198
  }
2946
3199
  }}
2947
- aria-label={`Remove ${slotReferenceTitle(ref)} reference`}
3200
+ aria-label={t("agentChat.composer.removeReference", {
3201
+ defaultValue: "Remove {{name}} reference",
3202
+ name: slotReferenceTitle(ref),
3203
+ })}
2948
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"
2949
3205
  >
2950
3206
  <IconX className="h-3 w-3" />
@@ -2972,7 +3228,10 @@ export function TiptapComposer({
2972
3228
  setSelectedContextItemKey(null);
2973
3229
  onRemoveContextItem?.(item.key);
2974
3230
  }}
2975
- aria-label={`Remove ${item.title} context`}
3231
+ aria-label={t("agentChat.composer.removeContext", {
3232
+ defaultValue: "Remove {{name}} context",
3233
+ name: item.title,
3234
+ })}
2976
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"
2977
3236
  >
2978
3237
  <IconX className="h-3 w-3" />