@agent-native/toolkit 0.16.2 → 0.16.3

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.
@@ -12,6 +12,7 @@ import {
12
12
  IconArrowLeft,
13
13
  IconX,
14
14
  IconHelpCircle,
15
+ IconTerminal2,
15
16
  } from "@tabler/icons-react";
16
17
  import React, {
17
18
  useState,
@@ -23,6 +24,7 @@ import React, {
23
24
  import { createPortal } from "react-dom";
24
25
 
25
26
  import { Popover, PopoverTrigger, PopoverContent } from "../ui/popover.js";
27
+ import { Switch } from "../ui/switch.js";
26
28
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
27
29
  import { cn } from "../utils.js";
28
30
  import {
@@ -33,6 +35,12 @@ import {
33
35
  import { useComposerRuntimeAdapters } from "./runtime-adapters.js";
34
36
  import type { ComposerMode } from "./types.js";
35
37
 
38
+ export interface ComposerTerminalModeControl {
39
+ enabled: boolean;
40
+ onChange: (enabled: boolean) => void;
41
+ onNewTerminal?: () => void;
42
+ }
43
+
36
44
  interface ComposerPlusMenuProps {
37
45
  onSelectMode?: (mode: ComposerMode) => void;
38
46
  onAttachmentError?: (message: string) => void;
@@ -46,9 +54,11 @@ interface ComposerPlusMenuProps {
46
54
  * Automation, and MCP Server. Extension is included only when
47
55
  * `extensionTools` is true. "upload-only": clicking + opens the file picker
48
56
  * directly — no popover, no other modes. Use for prompt popovers where the
49
- * only thing to attach is a file.
57
+ * only thing to attach is a file. "terminal": one new-terminal action plus
58
+ * the Terminal mode switch.
50
59
  */
51
- mode?: "full" | "upload-only";
60
+ mode?: "full" | "upload-only" | "terminal";
61
+ terminalModeControl?: ComposerTerminalModeControl;
52
62
  }
53
63
 
54
64
  type View = "menu" | "skill-upload";
@@ -294,10 +304,16 @@ export function ComposerPlusMenu({
294
304
  onAttachmentError,
295
305
  extensionTools = false,
296
306
  mode = "full",
307
+ terminalModeControl,
297
308
  }: ComposerPlusMenuProps) {
298
309
  if (mode === "upload-only") {
299
310
  return <UploadOnlyAttachButton onAttachmentError={onAttachmentError} />;
300
311
  }
312
+ if (mode === "terminal" && terminalModeControl) {
313
+ return (
314
+ <ComposerPlusMenuTerminal terminalModeControl={terminalModeControl} />
315
+ );
316
+ }
301
317
  return (
302
318
  <ComposerPlusMenuFull
303
319
  onSelectMode={onSelectMode}
@@ -307,6 +323,73 @@ export function ComposerPlusMenu({
307
323
  );
308
324
  }
309
325
 
326
+ function ComposerPlusMenuTerminal({
327
+ terminalModeControl,
328
+ }: Pick<ComposerPlusMenuProps, "terminalModeControl">) {
329
+ const [open, setOpen] = useState(false);
330
+
331
+ if (!terminalModeControl) return null;
332
+
333
+ const handlePrimaryAction = () => {
334
+ if (!terminalModeControl.enabled) {
335
+ terminalModeControl.onChange(true);
336
+ setOpen(false);
337
+ return;
338
+ }
339
+ terminalModeControl.onNewTerminal?.();
340
+ setOpen(false);
341
+ };
342
+
343
+ return (
344
+ <>
345
+ <Popover open={open} onOpenChange={setOpen}>
346
+ <PopoverTrigger asChild>
347
+ <button
348
+ type="button"
349
+ className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-accent/50 hover:text-foreground"
350
+ aria-label="Terminal options" /* i18n-ignore -- portable toolkit label */
351
+ title="Terminal options" /* i18n-ignore -- portable toolkit label */
352
+ >
353
+ <IconPlus className="h-4 w-4" />
354
+ </button>
355
+ </PopoverTrigger>
356
+ <PopoverContent
357
+ align="start"
358
+ sideOffset={8}
359
+ className="w-56 border-input bg-muted p-1 text-foreground"
360
+ onOpenAutoFocus={(event) => event.preventDefault()}
361
+ >
362
+ <button
363
+ type="button"
364
+ className="flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-start text-[12px] font-medium text-foreground hover:bg-accent/60"
365
+ onClick={handlePrimaryAction}
366
+ >
367
+ <IconTerminal2
368
+ size={14}
369
+ className="shrink-0 text-muted-foreground"
370
+ aria-hidden="true"
371
+ />
372
+ <span>
373
+ {terminalModeControl.enabled ? "New terminal" : "Start terminal"}
374
+ </span>
375
+ </button>
376
+ <div className="my-1 border-t border-border/70" />
377
+ <div className="flex items-center justify-between gap-3 px-2.5 py-2">
378
+ <span className="text-[12px] font-medium text-foreground">
379
+ {"Terminal mode" /* i18n-ignore -- portable toolkit label */}
380
+ </span>
381
+ <Switch
382
+ checked={terminalModeControl.enabled}
383
+ onCheckedChange={terminalModeControl.onChange}
384
+ aria-label="Terminal mode" /* i18n-ignore -- portable toolkit label */
385
+ />
386
+ </div>
387
+ </PopoverContent>
388
+ </Popover>
389
+ </>
390
+ );
391
+ }
392
+
310
393
  function ComposerPlusMenuFull({
311
394
  onSelectMode,
312
395
  onAttachmentError,
@@ -631,7 +714,7 @@ function ComposerPlusMenuFull({
631
714
  align="start"
632
715
  sideOffset={8}
633
716
  className={cn(
634
- "p-0 rounded-lg",
717
+ "rounded-lg border-input bg-muted p-0 text-foreground",
635
718
  view === "skill-upload"
636
719
  ? "max-h-[70vh] w-[calc(100vw-24px)] max-w-[380px] overflow-y-auto"
637
720
  : "w-[260px]",
@@ -703,7 +786,7 @@ function ComposerPlusMenuFull({
703
786
  onMouseEnter={() => openSkillFlyout()}
704
787
  onMouseLeave={scheduleSkillFlyoutClose}
705
788
  className={cn(
706
- "absolute top-0 z-20 w-[240px] rounded-lg border border-border bg-popover py-1 shadow-md",
789
+ "absolute top-0 z-20 w-[240px] rounded-lg border border-input bg-muted py-1 text-foreground shadow-md",
707
790
  skillFlyoutSide === "right"
708
791
  ? "left-full ml-1"
709
792
  : "right-full mr-1",
@@ -36,6 +36,7 @@ import {
36
36
  PROMPT_DOCUMENT_ATTACHMENT_ACCEPT,
37
37
  TextAttachmentAdapter,
38
38
  } from "./attachment-accept.js";
39
+ import type { ComposerTerminalModeControl } from "./ComposerPlusMenu.js";
39
40
  import { isPastedTextAttachmentName } from "./pasted-text.js";
40
41
  import { PastedTextChip } from "./PastedTextChip.js";
41
42
  import { escapePromptAttachmentAttribute } from "./prompt-attachments.js";
@@ -85,6 +86,8 @@ export interface PromptComposerProps {
85
86
  ) => void | Promise<void>;
86
87
  placeholder?: string;
87
88
  disabled?: boolean;
89
+ /** Called when a host-gated composer is clicked while it is disabled. */
90
+ onDisabledClick?: () => void;
88
91
  /** Override the generic document attachment cap for a multipart host. */
89
92
  maxDocumentAttachmentBytes?: number;
90
93
  /** Label used in the visible document attachment limit error. */
@@ -112,7 +115,9 @@ export interface PromptComposerProps {
112
115
  * Controls the shared "+" affordance. Defaults to upload-only for standalone
113
116
  * prompt forms; chat surfaces can opt into the full sidebar menu.
114
117
  */
115
- plusMenuMode?: "full" | "upload-only" | "hidden";
118
+ plusMenuMode?: "full" | "upload-only" | "terminal" | "hidden";
119
+ /** Controls the terminal-specific plus menu when `plusMenuMode` is terminal. */
120
+ terminalModeControl?: ComposerTerminalModeControl;
116
121
  /**
117
122
  * Include extension creation in the full "+" menu. Defaults to false.
118
123
  */
@@ -156,6 +161,8 @@ export interface PromptComposerProps {
156
161
  availableAgents?: ComposerAgentOption[];
157
162
  /** Selected agent runtime identifier. */
158
163
  selectedAgent?: string;
164
+ /** Show only the selected agent in the model control. */
165
+ agentOnly?: boolean;
159
166
  /** Callback when the user picks an agent runtime. */
160
167
  onAgentChange?: (agent: string) => void;
161
168
  /** Called when the shared model picker opens or closes. */
@@ -491,6 +498,7 @@ function PromptComposerInner({
491
498
  onSubmit,
492
499
  placeholder,
493
500
  disabled,
501
+ onDisabledClick,
494
502
  maxDocumentAttachmentBytes,
495
503
  documentAttachmentLimitLabel,
496
504
  autoFocus,
@@ -506,6 +514,7 @@ function PromptComposerInner({
506
514
  voiceEnabled = DEFAULT_VOICE_DICTATION_ENABLED,
507
515
  attachmentsEnabled = true,
508
516
  plusMenuMode,
517
+ terminalModeControl,
509
518
  extensionTools = false,
510
519
  initialText,
511
520
  initialTextKey,
@@ -529,6 +538,7 @@ function PromptComposerInner({
529
538
  onEffortChange,
530
539
  availableAgents,
531
540
  selectedAgent,
541
+ agentOnly = false,
532
542
  onAgentChange,
533
543
  onModelSelectorOpenChange,
534
544
  modelStatusChecksEnabled,
@@ -643,12 +653,13 @@ function PromptComposerInner({
643
653
  <BuilderSetupCard
644
654
  onConnected={handleBuilderConnected}
645
655
  bouncePulse={missingKeyBouncePulse}
656
+ attached
646
657
  fullWidth
647
658
  layout="sidebar"
648
659
  />
649
660
  ) : null}
650
661
  {missingApiKey && useInlineMissingKeySetup && BuilderSetupContent ? (
651
- <div className="mb-2 rounded-md border border-border/80 bg-background/80 p-2.5 text-start shadow-sm">
662
+ <div className="agent-builder-setup-inline--attached mb-0 rounded-md border border-border/80 bg-background/80 p-2.5 text-start shadow-sm">
652
663
  <BuilderSetupContent
653
664
  onConnected={handleBuilderConnected}
654
665
  layout="sidebar"
@@ -659,13 +670,21 @@ function PromptComposerInner({
659
670
  className={cn(
660
671
  "text-start",
661
672
  gateComposer && "cursor-pointer",
673
+ (gateComposer || onDisabledClick) &&
674
+ "agent-composer-area--attached-above",
662
675
  className,
663
676
  )}
664
677
  rootClassName={rootClassName}
665
678
  style={style}
666
679
  rootStyle={rootStyle}
667
680
  layoutVariant={layoutVariant}
668
- onClick={gateComposer ? bounceMissingKeySetup : undefined}
681
+ onClick={
682
+ gateComposer
683
+ ? bounceMissingKeySetup
684
+ : onDisabledClick
685
+ ? () => onDisabledClick()
686
+ : undefined
687
+ }
669
688
  >
670
689
  <PromptAttachmentStrip />
671
690
  <TiptapComposer
@@ -687,6 +706,7 @@ function PromptComposerInner({
687
706
  plusMenuMode={
688
707
  plusMenuMode ?? (attachmentsEnabled ? "upload-only" : "hidden")
689
708
  }
709
+ terminalModeControl={terminalModeControl}
690
710
  extensionTools={extensionTools}
691
711
  attachButton={attachButton}
692
712
  modeControl={modeControl}
@@ -708,6 +728,7 @@ function PromptComposerInner({
708
728
  availableModels={composerModelGroups}
709
729
  availableAgents={availableAgents}
710
730
  selectedAgent={selectedAgent}
731
+ agentOnly={agentOnly}
711
732
  showAutoModelOption={showAutoModelOption}
712
733
  modelListLoading={composerModelListLoading}
713
734
  onModelChange={handleModelChange}
@@ -36,7 +36,10 @@ import {
36
36
  PopoverTrigger,
37
37
  } from "../ui/popover.js";
38
38
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
39
- import { ComposerPlusMenu } from "./ComposerPlusMenu.js";
39
+ import {
40
+ ComposerPlusMenu,
41
+ type ComposerTerminalModeControl,
42
+ } from "./ComposerPlusMenu.js";
40
43
  import { getComposerDraftKey } from "./draft-key.js";
41
44
  import { FileReference } from "./extensions/FileReference.js";
42
45
  import { MentionReference } from "./extensions/MentionReference.js";
@@ -668,6 +671,8 @@ export interface ComposerAgentOption {
668
671
  id: string;
669
672
  /** Human-readable runtime name shown in the picker. */
670
673
  label: string;
674
+ /** Optional icon shown beside the runtime name. */
675
+ icon?: React.ReactNode;
671
676
  /** Optional short detail shown below the runtime name. */
672
677
  description?: string;
673
678
  /** Whether this runtime can be selected right now. */
@@ -774,6 +779,8 @@ export interface TiptapComposerProps {
774
779
  availableAgents?: ComposerAgentOption[];
775
780
  /** Selected agent runtime identifier. Defaults to the built-in agent. */
776
781
  selectedAgent?: string;
782
+ /** Show only the selected agent in the model control. */
783
+ agentOnly?: boolean;
777
784
  /** Mark the selected runtime as the hosted tools-only harness mode. */
778
785
  hostedHarness?: boolean;
779
786
  /** Callback when the user picks an agent runtime. */
@@ -811,7 +818,9 @@ export interface TiptapComposerProps {
811
818
  * that opens the file picker directly. `"hidden"` hides attachment controls
812
819
  * for text-only prompt surfaces.
813
820
  */
814
- plusMenuMode?: "full" | "upload-only" | "hidden";
821
+ plusMenuMode?: "full" | "upload-only" | "terminal" | "hidden";
822
+ /** Controls the terminal-specific plus menu when `plusMenuMode` is terminal. */
823
+ terminalModeControl?: ComposerTerminalModeControl;
815
824
  /**
816
825
  * Include extension creation in the full "+" menu. Defaults to false so
817
826
  * apps opt into the extension capability deliberately.
@@ -1297,6 +1306,7 @@ function ModelSelector({
1297
1306
  engines,
1298
1307
  agents,
1299
1308
  selectedAgent,
1309
+ agentOnly = false,
1300
1310
  hostedHarness = false,
1301
1311
  showAutoModelOption = true,
1302
1312
  modelListLoading = false,
@@ -1323,6 +1333,7 @@ function ModelSelector({
1323
1333
  statusLabel?: string;
1324
1334
  isSubscription?: boolean;
1325
1335
  }>;
1336
+ agentOnly?: boolean;
1326
1337
  showAutoModelOption?: boolean;
1327
1338
  modelListLoading?: boolean;
1328
1339
  onChange: (model: string, engine: string) => void;
@@ -1411,9 +1422,10 @@ function ModelSelector({
1411
1422
  }
1412
1423
  onChange(preferredAgentModel.model, preferredAgentModel.engine);
1413
1424
  }, [engines, isClaudeCodeAgent, model, onChange, preferredAgentModel]);
1414
- const effortOptions =
1415
- reasoning?.getOptionsForModel?.(model) ??
1416
- getComposerReasoningEffortOptions(model);
1425
+ const effortOptions = agentOnly
1426
+ ? []
1427
+ : (reasoning?.getOptionsForModel?.(model) ??
1428
+ getComposerReasoningEffortOptions(model));
1417
1429
  const selectedEffort =
1418
1430
  reasoning?.resolve?.(model, effort) ??
1419
1431
  resolveReasoningEffortSelection(model, effort ?? defaultEffort);
@@ -1435,15 +1447,15 @@ function ModelSelector({
1435
1447
  const [detailSection, setDetailSection] = useState<
1436
1448
  "agent" | "model" | "effort" | null
1437
1449
  >(null);
1438
- const resolvedSection = detailSection ?? "model";
1450
+ const resolvedSection = detailSection ?? (agentOnly ? "agent" : "model");
1439
1451
 
1440
1452
  const setPickerOpen = useCallback(
1441
1453
  (nextOpen: boolean) => {
1442
1454
  if (controlledOpen === undefined) setInternalOpen(nextOpen);
1443
- if (nextOpen) setDetailSection(null);
1455
+ if (nextOpen) setDetailSection(agentOnly ? "agent" : null);
1444
1456
  onModelSelectorOpenChange?.(nextOpen);
1445
1457
  },
1446
- [controlledOpen, onModelSelectorOpenChange],
1458
+ [agentOnly, controlledOpen, onModelSelectorOpenChange],
1447
1459
  );
1448
1460
 
1449
1461
  const visibleProviderGroups = modelProviderGroups;
@@ -1518,6 +1530,11 @@ function ModelSelector({
1518
1530
  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"
1519
1531
  >
1520
1532
  <span className="min-w-0 truncate">
1533
+ {selectedAgentOption?.icon ? (
1534
+ <span className="me-1 inline-flex shrink-0 align-[-2px] text-muted-foreground">
1535
+ {selectedAgentOption.icon}
1536
+ </span>
1537
+ ) : null}
1521
1538
  {selectedAgentOption && selectedAgentOption.id !== "default"
1522
1539
  ? selectedAgentLabel
1523
1540
  : compactComposerModelName(model, t)}
@@ -1613,25 +1630,29 @@ function ModelSelector({
1613
1630
  <IconChevronRight className="h-3 w-3 shrink-0 opacity-60 rtl:-scale-x-100" />
1614
1631
  </button>
1615
1632
  )}
1616
- <button
1617
- type="button"
1618
- role="tab"
1619
- aria-selected={resolvedSection === "model"}
1620
- onClick={() => setDetailSection("model")}
1621
- onMouseEnter={() => setDetailSection("model")}
1622
- className={`flex w-full min-w-0 items-center gap-1 rounded-md px-2 py-2 text-start transition-colors ${
1623
- resolvedSection === "model"
1624
- ? "bg-accent text-foreground"
1625
- : "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
1626
- }`}
1627
- >
1628
- <span className="shrink-0 text-[12px] font-medium">Model</span>
1629
- <span className="ms-auto min-w-0 max-w-[6rem] truncate text-end text-[11px] text-muted-foreground/80">
1630
- {selectedModelLabel}
1631
- </span>
1632
- <IconChevronRight className="h-3 w-3 shrink-0 opacity-60 rtl:-scale-x-100" />
1633
- </button>
1634
- {effortOptions.length > 0 && (
1633
+ {!agentOnly && (
1634
+ <button
1635
+ type="button"
1636
+ role="tab"
1637
+ aria-selected={resolvedSection === "model"}
1638
+ onClick={() => setDetailSection("model")}
1639
+ onMouseEnter={() => setDetailSection("model")}
1640
+ className={`flex w-full min-w-0 items-center gap-1 rounded-md px-2 py-2 text-start transition-colors ${
1641
+ resolvedSection === "model"
1642
+ ? "bg-accent text-foreground"
1643
+ : "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
1644
+ }`}
1645
+ >
1646
+ <span className="shrink-0 text-[12px] font-medium">
1647
+ Model
1648
+ </span>
1649
+ <span className="ms-auto min-w-0 max-w-[6rem] truncate text-end text-[11px] text-muted-foreground/80">
1650
+ {selectedModelLabel}
1651
+ </span>
1652
+ <IconChevronRight className="h-3 w-3 shrink-0 opacity-60 rtl:-scale-x-100" />
1653
+ </button>
1654
+ )}
1655
+ {!agentOnly && effortOptions.length > 0 && (
1635
1656
  <button
1636
1657
  type="button"
1637
1658
  role="tab"
@@ -1703,6 +1724,14 @@ function ModelSelector({
1703
1724
  : "cursor-default opacity-50"
1704
1725
  }`}
1705
1726
  >
1727
+ {agent.icon ? (
1728
+ <span
1729
+ className="flex size-4 shrink-0 items-center justify-center text-muted-foreground"
1730
+ aria-hidden="true"
1731
+ >
1732
+ {agent.icon}
1733
+ </span>
1734
+ ) : null}
1706
1735
  <span
1707
1736
  className={`min-w-0 truncate text-[12px] ${
1708
1737
  isSelected
@@ -2134,6 +2163,7 @@ export function TiptapComposer({
2134
2163
  onEffortChange,
2135
2164
  availableAgents,
2136
2165
  selectedAgent,
2166
+ agentOnly = false,
2137
2167
  hostedHarness,
2138
2168
  onAgentChange,
2139
2169
  onModelSelectorOpenChange,
@@ -2145,6 +2175,7 @@ export function TiptapComposer({
2145
2175
  contextItems = [],
2146
2176
  onRemoveContextItem,
2147
2177
  plusMenuMode = "full",
2178
+ terminalModeControl,
2148
2179
  extensionTools = false,
2149
2180
  interceptBuildRequestsForBuilder = false,
2150
2181
  onAttachmentError,
@@ -3631,6 +3662,7 @@ export function TiptapComposer({
3631
3662
  <ComposerPlusMenu
3632
3663
  onSelectMode={handleSelectMode}
3633
3664
  mode={plusMenuMode}
3665
+ terminalModeControl={terminalModeControl}
3634
3666
  extensionTools={extensionTools}
3635
3667
  onAttachmentError={onAttachmentError}
3636
3668
  />
@@ -3645,6 +3677,7 @@ export function TiptapComposer({
3645
3677
  engines={availableModels}
3646
3678
  agents={availableAgents}
3647
3679
  selectedAgent={selectedAgent}
3680
+ agentOnly={agentOnly}
3648
3681
  hostedHarness={hostedHarness}
3649
3682
  showAutoModelOption={showAutoModelOption}
3650
3683
  modelListLoading={modelListLoading}
@@ -29,6 +29,7 @@ export {
29
29
  type PromptComposerFile,
30
30
  type PromptComposerSubmitOptions,
31
31
  } from "./PromptComposer.js";
32
+ export type { ComposerTerminalModeControl } from "./ComposerPlusMenu.js";
32
33
  export {
33
34
  RealtimeVoiceModeDock,
34
35
  RealtimeVoiceModeEntry,