@vvedantb/ui 0.17.1 → 0.18.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 (130) hide show
  1. package/README.md +251 -3
  2. package/package.json +13 -5
  3. package/shell/src/SidebarResize.tsx +1 -4
  4. package/src/ai-elements/__snapshots__/queue.test.tsx.snap +11 -0
  5. package/src/ai-elements/activity-question-utils.ts +26 -0
  6. package/src/ai-elements/activity-question.tsx +159 -0
  7. package/src/ai-elements/activity-shared.tsx +459 -0
  8. package/src/ai-elements/activity-step-detail.tsx +115 -0
  9. package/src/ai-elements/activity-step-label.ts +981 -0
  10. package/src/ai-elements/activity-tasks-utils.ts +199 -0
  11. package/src/ai-elements/activity-tasks.tsx +770 -0
  12. package/src/ai-elements/artifact.tsx +162 -52
  13. package/src/ai-elements/attachments.tsx +62 -32
  14. package/src/ai-elements/code-block.tsx +76 -22
  15. package/src/ai-elements/context.tsx +370 -0
  16. package/src/ai-elements/conversation.tsx +186 -62
  17. package/src/ai-elements/file-chip.tsx +53 -0
  18. package/src/ai-elements/message.tsx +161 -50
  19. package/src/ai-elements/model-picker-content.tsx +505 -0
  20. package/src/ai-elements/model-picker-types.ts +56 -0
  21. package/src/ai-elements/model-picker.tsx +301 -0
  22. package/src/ai-elements/model-selector.tsx +102 -21
  23. package/src/ai-elements/plan.tsx +130 -0
  24. package/src/ai-elements/prompt-input-settings.tsx +144 -0
  25. package/src/ai-elements/prompt-input-speech.tsx +189 -0
  26. package/src/ai-elements/prompt-input-traits.tsx +281 -0
  27. package/src/ai-elements/prompt-input.tsx +320 -134
  28. package/src/ai-elements/provider-icon.tsx +133 -0
  29. package/src/ai-elements/queue.tsx +293 -9
  30. package/src/ai-elements/shimmer.tsx +72 -1
  31. package/src/ai-elements/simple-model-ladder.tsx +108 -0
  32. package/src/ai-elements/speech-input.tsx +5 -3
  33. package/src/ai-elements/test-results.tsx +510 -0
  34. package/src/ai-elements/trace.tsx +243 -0
  35. package/src/ai-elements/use-streaming-disclosure.ts +50 -0
  36. package/src/ai-elements/web-preview.tsx +349 -0
  37. package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
  38. package/src/index.ts +66 -12
  39. package/src/kibo/avatar-stack.tsx +40 -0
  40. package/src/kibo/contribution-graph.tsx +539 -0
  41. package/src/kibo/cursor.tsx +72 -0
  42. package/src/kibo/gantt-features.tsx +439 -0
  43. package/src/kibo/gantt-header.tsx +181 -0
  44. package/src/kibo/gantt-provider.tsx +657 -0
  45. package/src/kibo/gantt-sidebar.tsx +157 -0
  46. package/src/kibo/gantt-timeline.tsx +154 -0
  47. package/src/kibo/index.ts +151 -0
  48. package/src/kibo/kanban.tsx +338 -0
  49. package/src/kibo/kanbanDropTarget.ts +25 -0
  50. package/src/kibo/list.tsx +162 -0
  51. package/src/kibo/theme-switcher.tsx +109 -0
  52. package/src/kibo/video-player.tsx +237 -0
  53. package/src/markdown/Markdown.tsx +112 -0
  54. package/src/markdown/MarkdownBlockquote.tsx +45 -0
  55. package/src/markdown/MarkdownCodeBlock.tsx +197 -0
  56. package/src/markdown/MarkdownInlineCode.tsx +40 -0
  57. package/src/markdown/MarkdownLink.tsx +92 -0
  58. package/src/markdown/codeFence.ts +57 -0
  59. package/src/markdown/codeHighlight.ts +138 -0
  60. package/src/markdown/incrementalBlocks.ts +182 -0
  61. package/src/markdown/index.ts +3 -0
  62. package/src/markdown/markdownClipboard.ts +178 -0
  63. package/src/markdown/mermaid.ts +155 -0
  64. package/src/markdown/rehypeGithubAlerts.ts +80 -0
  65. package/src/motion/presets.ts +24 -0
  66. package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
  67. package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
  68. package/src/ui/_menu-parts.tsx +29 -0
  69. package/src/ui/accordion.alt.tsx +46 -53
  70. package/src/ui/accordion.tsx +59 -10
  71. package/src/ui/avatar.tsx +56 -0
  72. package/src/ui/badge.tsx +45 -21
  73. package/src/ui/border-beam.tsx +97 -0
  74. package/src/ui/button-group.tsx +50 -21
  75. package/src/ui/button.tsx +67 -45
  76. package/src/ui/calendar.tsx +48 -5
  77. package/src/ui/card.tsx +77 -8
  78. package/src/ui/carousel.tsx +60 -0
  79. package/src/ui/centered-spinner.tsx +30 -0
  80. package/src/ui/checkbox.tsx +34 -16
  81. package/src/ui/clear-input.alt.tsx +33 -75
  82. package/src/ui/clear-input.tsx +140 -15
  83. package/src/ui/collapsible.tsx +40 -2
  84. package/src/ui/command.tsx +143 -70
  85. package/src/ui/context-menu.tsx +159 -93
  86. package/src/ui/crossfade-icon.tsx +123 -0
  87. package/src/ui/dialog.tsx +154 -70
  88. package/src/ui/dropdown-menu.tsx +177 -117
  89. package/src/ui/hover-card.tsx +59 -17
  90. package/src/ui/input-group.alt.tsx +83 -83
  91. package/src/ui/input-group.tsx +129 -58
  92. package/src/ui/input.tsx +13 -4
  93. package/src/ui/label.tsx +11 -8
  94. package/src/ui/list-row.tsx +232 -0
  95. package/src/ui/loading-state.tsx +215 -0
  96. package/src/ui/popover.tsx +56 -23
  97. package/src/ui/progress.tsx +28 -16
  98. package/src/ui/refresh-spin-icon.tsx +41 -0
  99. package/src/ui/search-input.tsx +66 -15
  100. package/src/ui/select.tsx +171 -100
  101. package/src/ui/separator.tsx +23 -14
  102. package/src/ui/slider.tsx +21 -7
  103. package/src/ui/sonner.tsx +68 -30
  104. package/src/ui/spinner.tsx +229 -11
  105. package/src/ui/status-dot.tsx +84 -0
  106. package/src/ui/surface.tsx +49 -0
  107. package/src/ui/switch.tsx +29 -15
  108. package/src/ui/table.tsx +38 -16
  109. package/src/ui/tabs.tsx +123 -35
  110. package/src/ui/tabsSliding.ts +33 -1
  111. package/src/ui/textarea.tsx +13 -4
  112. package/src/ui/tooltip.tsx +50 -14
  113. package/src/ui/ui-config.tsx +149 -0
  114. package/src/utils/cat-color.ts +123 -0
  115. package/src/utils/dayjs.ts +6 -0
  116. package/src/utils/gesture.ts +59 -0
  117. package/src/utils/runtimeVisibility.ts +167 -0
  118. package/src/utils/sharedClock.ts +96 -0
  119. package/src/utils/surface-radius.ts +16 -0
  120. package/src/utils/useDragSensors.ts +58 -0
  121. package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
  122. package/tokens/examples/tokens-moved/README.md +20 -0
  123. package/tokens/examples/tokens-moved/index.html +12 -0
  124. package/tokens/examples/tokens-moved/main.tsx +6 -0
  125. package/tokens/examples/tokens-moved/package.json +14 -0
  126. package/tokens/examples/tokens-moved/styles.css +12 -0
  127. package/tokens/examples/tokens-moved/tsconfig.json +14 -0
  128. package/tokens/examples/tokens-moved/vite.config.ts +11 -0
  129. package/tokens/src/css/border-beam.css +116 -0
  130. package/tokens/src/css/markdown.css +318 -0
@@ -0,0 +1,301 @@
1
+ "use client";
2
+
3
+ import { useState, type ReactNode } from "react";
4
+ import { IconBolt, IconChevronDown } from "@tabler/icons-react";
5
+ import { SimpleModelLadder } from "./simple-model-ladder";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
7
+ import { useCn, useUiConfig } from "../ui/ui-config";
8
+ import { ProviderIcon } from "./provider-icon";
9
+ import {
10
+ ModelPickerContent,
11
+ buildPickerInstances,
12
+ providerInstanceInitials,
13
+ teamInstanceKey,
14
+ } from "./model-picker-content";
15
+ import {
16
+ type ModelAccount,
17
+ type ModelOption,
18
+ findModelOption,
19
+ formatModelDisplayLabel,
20
+ getProviderLabel,
21
+ } from "./model-picker-types";
22
+
23
+ export type { ModelAccount, ModelOption };
24
+ export { findModelOption, formatModelDisplayLabel, getProviderLabel };
25
+ /** Embeddable picker body — use in menus/submenus; use `ModelPicker` for button triggers. */
26
+ export { ModelPickerContent };
27
+
28
+ /**
29
+ * `UiConfig.ModelPicker`: `classes.trigger` replaces the `ModelPicker` trigger
30
+ * button's classes; `badgeRing` sets the ring colour around the account
31
+ * initials badge, which should match the surface behind it (`trigger`: the
32
+ * trigger, default `var(--color-border)`; `panel`: the provider rail and list,
33
+ * default `var(--color-surface-secondary)`).
34
+ */
35
+ export interface ModelPickerConfig {
36
+ classes?: { trigger?: string };
37
+ badgeRing?: { trigger?: string; panel?: string };
38
+ }
39
+
40
+ const TRIGGER_CLASS =
41
+ // Carries `data-slot="select-trigger"` and is styled to read as one, so it
42
+ // has to press like one: `motion-press active:scale-[0.98]`, like the select
43
+ // triggers it sits beside in a composer footer. `motion-press` supersedes
44
+ // `transition-colors`: the two at equal specificity replace each other.
45
+ "motion-press flex h-7 items-center gap-1.5 rounded-md px-2 text-xs font-medium text-muted-foreground hover:bg-surface-tertiary hover:text-foreground active:scale-[0.98] disabled:opacity-50";
46
+
47
+ /**
48
+ * Shared chrome for the picker panel (popover + menu submenu). Keep these in
49
+ * sync so trigger vs embed surfaces cannot drift.
50
+ */
51
+ export const modelPickerSurfaceClass =
52
+ "w-100 max-w-[calc(100vw-1rem)] overflow-hidden rounded-lg border-0 bg-transparent p-0 shadow-none";
53
+
54
+ export interface ModelPickerProps<TModel extends string = string> {
55
+ value: TModel;
56
+ onValueChange: (model: TModel) => void;
57
+ options: ReadonlyArray<ModelOption<TModel>>;
58
+ disabled?: boolean;
59
+ className?: string;
60
+ /**
61
+ * The user's own provider accounts. When non-empty, each provider opens into
62
+ * Team + account groups, each listing that provider's models so picking a
63
+ * model also picks whose credential runs it.
64
+ */
65
+ accounts?: ReadonlyArray<ModelAccount>;
66
+ /** Currently selected account id, or null for the team credential. */
67
+ accountId?: string | null;
68
+ /** Fired alongside a model change with the chosen account (null = team). */
69
+ onAccountChange?: (accountId: string | null) => void;
70
+ /**
71
+ * When set, called once per pick instead of separate model/account
72
+ * callbacks — use this to persist both fields in a single mutation.
73
+ */
74
+ onSelectionChange?: (model: TModel, accountId: string | null) => void;
75
+ /**
76
+ * When a personal account is selected, Team for that provider is shown
77
+ * disabled. Set false when the viewer cannot switch the sticky account
78
+ * (e.g. non-owner on a task) so Team stays unselectable.
79
+ */
80
+ canSelectTeamWhilePersonal?: boolean;
81
+ /**
82
+ * Compact label after the model name on the trigger (e.g. the active
83
+ * reasoning level).
84
+ */
85
+ triggerSuffix?: string;
86
+ /** Pinned above the model list — typically trait controls. */
87
+ header?: ReactNode;
88
+ /** Bolt before the provider icon when Fast mode is on. */
89
+ showFastIcon?: boolean;
90
+ /**
91
+ * Simple-view capability ladder (cheapest → strongest). When at least two of
92
+ * these ids are in `options`, the popover opens on a discrete slider;
93
+ * Advanced swaps to the full picker.
94
+ */
95
+ simpleLadder?: ReadonlyArray<TModel>;
96
+ /** Parks the slider thumb when `value` is not itself a ladder tick. */
97
+ simpleLadderSnap?: TModel;
98
+ }
99
+
100
+ /**
101
+ * Button + popover model picker (create modal, composers, toolbars).
102
+ * For context/dropdown menus, embed `ModelPickerContent` with
103
+ * `modelPickerSurfaceClass` instead of nesting this popover.
104
+ */
105
+ export function ModelPicker<TModel extends string>({
106
+ value,
107
+ onValueChange,
108
+ options,
109
+ disabled,
110
+ className,
111
+ accounts,
112
+ accountId = null,
113
+ onAccountChange,
114
+ onSelectionChange,
115
+ canSelectTeamWhilePersonal = true,
116
+ triggerSuffix,
117
+ header,
118
+ showFastIcon,
119
+ simpleLadder,
120
+ simpleLadderSnap,
121
+ }: ModelPickerProps<TModel>) {
122
+ const merge = useCn();
123
+ const config = useUiConfig("ModelPicker");
124
+ const [open, setOpen] = useState(false);
125
+ const [advanced, setAdvanced] = useState(false);
126
+ const [previewId, setPreviewId] = useState<TModel | null>(null);
127
+ const displayValue = previewId ?? value;
128
+ const selectedModel = findModelOption(displayValue, options);
129
+
130
+ const ladderSteps: ModelOption<TModel>[] = [];
131
+ if (simpleLadder) {
132
+ for (const id of simpleLadder) {
133
+ const option = options.find((entry) => entry.id === id);
134
+ if (option) ladderSteps.push(option);
135
+ }
136
+ }
137
+ const showLadder = ladderSteps.length >= 2 && !advanced;
138
+ const preferredSnap = simpleLadderSnap ?? value;
139
+ let snappedAmong = ladderSteps[0]?.id ?? value;
140
+ for (const step of ladderSteps) {
141
+ if (step.id === value || step.id === preferredSnap) {
142
+ snappedAmong = step.id;
143
+ if (step.id === value) break;
144
+ }
145
+ }
146
+ const triggerLabel = selectedModel ? selectedModel.label : "Select model";
147
+ const instances = buildPickerInstances(options, accounts);
148
+
149
+ const selectedAccount = accountId
150
+ ? accounts?.find((account) => account.id === accountId)
151
+ : undefined;
152
+
153
+ const activeInstance =
154
+ (selectedAccount
155
+ ? instances.find((instance) => instance.key === selectedAccount.id)
156
+ : undefined) ??
157
+ (selectedModel
158
+ ? instances.find(
159
+ (instance) =>
160
+ instance.key === teamInstanceKey(selectedModel.provider),
161
+ )
162
+ : undefined) ??
163
+ instances[0];
164
+
165
+ const showAccountBadge = Boolean(
166
+ selectedAccount &&
167
+ instances.filter(
168
+ (instance) => instance.provider === selectedAccount.provider,
169
+ ).length > 1,
170
+ );
171
+
172
+ const commit = (modelId: TModel, nextAccountId: string | null) => {
173
+ if (onSelectionChange) {
174
+ onSelectionChange(modelId, nextAccountId);
175
+ } else {
176
+ onValueChange(modelId);
177
+ onAccountChange?.(nextAccountId);
178
+ }
179
+ };
180
+
181
+ const accountForModel = (modelId: TModel): string | null => {
182
+ const option = options.find((entry) => entry.id === modelId);
183
+ if (!option) return null;
184
+ if (selectedAccount && selectedAccount.provider === option.provider) {
185
+ return accountId;
186
+ }
187
+ return null;
188
+ };
189
+
190
+ return (
191
+ <Popover
192
+ open={open}
193
+ onOpenChange={(next) => {
194
+ setOpen(next);
195
+ if (!next) {
196
+ setAdvanced(false);
197
+ setPreviewId(null);
198
+ }
199
+ }}
200
+ >
201
+ <PopoverTrigger asChild>
202
+ <button
203
+ type="button"
204
+ role="combobox"
205
+ aria-expanded={open}
206
+ data-slot="select-trigger"
207
+ className={merge(
208
+ config?.classes?.trigger ?? TRIGGER_CLASS,
209
+ className,
210
+ )}
211
+ disabled={disabled}
212
+ >
213
+ {showFastIcon ? (
214
+ <span title="Fast" className="inline-flex shrink-0">
215
+ <IconBolt size={12} aria-hidden />
216
+ </span>
217
+ ) : null}
218
+ {activeInstance && selectedAccount && showAccountBadge ? (
219
+ <span className="relative isolate inline-flex size-4 shrink-0 items-center justify-center">
220
+ <ProviderIcon
221
+ provider={selectedModel?.provider ?? "claude"}
222
+ size={14}
223
+ />
224
+ <span
225
+ className="pointer-events-none absolute -right-0.5 -bottom-0.5 z-10 flex h-3 min-w-3 items-center justify-center rounded-full bg-surface-secondary px-0.5 text-[7px] font-semibold leading-none text-muted-foreground"
226
+ style={{
227
+ boxShadow: `0 0 0 1.5px ${config?.badgeRing?.trigger ?? "var(--color-border)"}`,
228
+ }}
229
+ aria-hidden
230
+ >
231
+ {providerInstanceInitials(selectedAccount.label)}
232
+ </span>
233
+ </span>
234
+ ) : (
235
+ <ProviderIcon
236
+ provider={selectedModel?.provider ?? "claude"}
237
+ size={14}
238
+ />
239
+ )}
240
+ {/* On a phone the composer's under-bar is only a few hundred pixels
241
+ wide, so the label gives up width to the control beside it rather
242
+ than pushing it out of the row. `min-w-0` is the load-bearing half
243
+ — a flex child defaults to `min-width: auto` and never shrinks. */}
244
+ <span className="whitespace-nowrap text-left max-sm:min-w-0 max-sm:truncate">
245
+ {triggerLabel}
246
+ {triggerSuffix ? (
247
+ <span className="text-muted-foreground/70">
248
+ {" · "}
249
+ {triggerSuffix}
250
+ </span>
251
+ ) : null}
252
+ </span>
253
+ <IconChevronDown
254
+ size={12}
255
+ className="shrink-0 opacity-60"
256
+ aria-hidden
257
+ />
258
+ </button>
259
+ </PopoverTrigger>
260
+ <PopoverContent
261
+ align="start"
262
+ className={
263
+ showLadder ? "w-56 p-3" : modelPickerSurfaceClass
264
+ }
265
+ onOpenAutoFocus={(event) => {
266
+ if (showLadder) return;
267
+ // Let ModelPickerContent own focus (search input); Radix would
268
+ // otherwise park it on the first focusable and fight our layout effect.
269
+ event.preventDefault();
270
+ }}
271
+ >
272
+ {showLadder ? (
273
+ <SimpleModelLadder
274
+ value={displayValue}
275
+ steps={ladderSteps}
276
+ snappedId={snappedAmong}
277
+ disabled={disabled}
278
+ onValueChange={(modelId) => {
279
+ setPreviewId(modelId);
280
+ commit(modelId, accountForModel(modelId));
281
+ }}
282
+ onAdvanced={() => setAdvanced(true)}
283
+ />
284
+ ) : (
285
+ <ModelPickerContent
286
+ value={displayValue}
287
+ accountId={accountId}
288
+ options={options}
289
+ accounts={accounts}
290
+ canSelectTeamWhilePersonal={canSelectTeamWhilePersonal}
291
+ onSelect={(modelId, nextAccountId) => {
292
+ setPreviewId(null);
293
+ commit(modelId, nextAccountId);
294
+ }}
295
+ header={header}
296
+ />
297
+ )}
298
+ </PopoverContent>
299
+ </Popover>
300
+ );
301
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ComponentProps, ReactNode } from "react";
2
+ import { IconCpu } from "@tabler/icons-react";
2
3
 
3
4
  import {
4
5
  Command,
@@ -17,24 +18,32 @@ import {
17
18
  DialogTitle,
18
19
  DialogTrigger,
19
20
  } from "../ui/dialog";
21
+ import { useCn } from "../ui/ui-config";
20
22
  import { cn } from "../utils/cn";
23
+ import { ProviderIcon } from "./provider-icon";
21
24
 
25
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
22
26
  export type ModelSelectorProps = ComponentProps<typeof Dialog>;
23
27
 
28
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
24
29
  export const ModelSelector = (props: ModelSelectorProps) => (
25
30
  <Dialog {...props} />
26
31
  );
27
32
 
33
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
28
34
  export type ModelSelectorTriggerProps = ComponentProps<typeof DialogTrigger>;
29
35
 
36
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
30
37
  export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => (
31
38
  <DialogTrigger {...props} />
32
39
  );
33
40
 
41
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
34
42
  export type ModelSelectorContentProps = ComponentProps<typeof DialogContent> & {
35
43
  title?: ReactNode;
36
44
  };
37
45
 
46
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
38
47
  export const ModelSelectorContent = ({
39
48
  className,
40
49
  children,
@@ -53,14 +62,18 @@ export const ModelSelectorContent = ({
53
62
  </DialogContent>
54
63
  );
55
64
 
65
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
56
66
  export type ModelSelectorDialogProps = ComponentProps<typeof CommandDialog>;
57
67
 
68
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
58
69
  export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => (
59
70
  <CommandDialog {...props} />
60
71
  );
61
72
 
73
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
62
74
  export type ModelSelectorInputProps = ComponentProps<typeof CommandInput>;
63
75
 
76
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
64
77
  export const ModelSelectorInput = ({
65
78
  className,
66
79
  ...props
@@ -68,48 +81,67 @@ export const ModelSelectorInput = ({
68
81
  <CommandInput className={cn("h-auto py-3.5", className)} {...props} />
69
82
  );
70
83
 
84
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
71
85
  export type ModelSelectorListProps = ComponentProps<typeof CommandList>;
72
86
 
87
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
73
88
  export const ModelSelectorList = (props: ModelSelectorListProps) => (
74
89
  <CommandList {...props} />
75
90
  );
76
91
 
92
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
77
93
  export type ModelSelectorEmptyProps = ComponentProps<typeof CommandEmpty>;
78
94
 
95
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
79
96
  export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => (
80
97
  <CommandEmpty {...props} />
81
98
  );
82
99
 
100
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
83
101
  export type ModelSelectorGroupProps = ComponentProps<typeof CommandGroup>;
84
102
 
103
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
85
104
  export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => (
86
105
  <CommandGroup {...props} />
87
106
  );
88
107
 
108
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
89
109
  export type ModelSelectorItemProps = ComponentProps<typeof CommandItem>;
90
110
 
111
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
91
112
  export const ModelSelectorItem = (props: ModelSelectorItemProps) => (
92
113
  <CommandItem {...props} />
93
114
  );
94
115
 
116
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
95
117
  export type ModelSelectorShortcutProps = ComponentProps<typeof CommandShortcut>;
96
118
 
119
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
97
120
  export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => (
98
121
  <CommandShortcut {...props} />
99
122
  );
100
123
 
124
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
101
125
  export type ModelSelectorSeparatorProps = ComponentProps<
102
126
  typeof CommandSeparator
103
127
  >;
104
128
 
129
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
105
130
  export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (
106
131
  <CommandSeparator {...props} />
107
132
  );
108
133
 
134
+ /**
135
+ * @deprecated Use `ModelPicker`. Removed in the next minor release.
136
+ *
137
+ * Now SVG props: the logo renders `ProviderIcon` (see `ModelSelectorLogo`).
138
+ */
109
139
  export type ModelSelectorLogoProps = Omit<
110
- ComponentProps<"img">,
111
- "src" | "alt"
140
+ ComponentProps<"svg">,
141
+ "children"
112
142
  > & {
143
+ /** Rendered width and height in px. Default 12, the old `<img>` size. */
144
+ size?: number;
113
145
  provider:
114
146
  | "moonshotai-cn"
115
147
  | "lucidquery"
@@ -171,38 +203,87 @@ export type ModelSelectorLogoProps = Omit<
171
203
  | (string & {});
172
204
  };
173
205
 
206
+ /**
207
+ * models.dev provider ids that have a `ProviderIcon` mark. `ProviderIcon`
208
+ * knows four marks: Claude, OpenAI (`codex`), OpenCode and Cursor.
209
+ */
210
+ const LOGO_PROVIDER: Record<string, string> = {
211
+ anthropic: "claude",
212
+ "google-vertex-anthropic": "claude",
213
+ claude: "claude",
214
+ openai: "codex",
215
+ codex: "codex",
216
+ opencode: "opencode",
217
+ cursor: "cursor",
218
+ };
219
+
220
+ /**
221
+ * @deprecated Use `ModelPicker`. Removed in the next minor release.
222
+ *
223
+ * A thin wrapper over `ProviderIcon`: no network fetch (it used to load
224
+ * `https://models.dev/logos/<provider>.svg`). `anthropic`,
225
+ * `google-vertex-anthropic` and `claude` show the Claude mark; `openai` and
226
+ * `codex` the OpenAI mark; `opencode` and `cursor` their marks. Any other id
227
+ * shows a neutral CPU glyph, not another brand's mark. The SVG keeps the old
228
+ * `"<provider> logo"` accessible name.
229
+ */
174
230
  export const ModelSelectorLogo = ({
175
231
  provider,
232
+ size = 12,
176
233
  className,
177
234
  ...props
178
- }: ModelSelectorLogoProps) => (
179
- <img
180
- {...props}
181
- alt={`${provider} logo`}
182
- className={cn("size-3 dark:invert", className)}
183
- height={12}
184
- src={`https://models.dev/logos/${provider}.svg`}
185
- width={12}
186
- />
187
- );
235
+ }: ModelSelectorLogoProps) => {
236
+ const merge = useCn();
237
+ const mapped = LOGO_PROVIDER[provider];
238
+ const label = `${provider} logo`;
239
+ if (!mapped) {
240
+ return (
241
+ <IconCpu
242
+ {...props}
243
+ role="img"
244
+ aria-label={label}
245
+ size={size}
246
+ className={merge("shrink-0 text-muted-foreground", className)}
247
+ />
248
+ );
249
+ }
250
+ return (
251
+ <ProviderIcon
252
+ {...props}
253
+ role="img"
254
+ aria-label={label}
255
+ aria-hidden={false}
256
+ provider={mapped}
257
+ size={size}
258
+ className={className}
259
+ />
260
+ );
261
+ };
188
262
 
263
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
189
264
  export type ModelSelectorLogoGroupProps = ComponentProps<"div">;
190
265
 
266
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
191
267
  export const ModelSelectorLogoGroup = ({
192
268
  className,
193
269
  ...props
194
- }: ModelSelectorLogoGroupProps) => (
195
- <div
196
- className={cn(
197
- "flex shrink-0 items-center -space-x-1 [&>img]:rounded-full [&>img]:bg-surface [&>img]:p-px dark:[&>img]:bg-foreground",
198
- className,
199
- )}
200
- {...props}
201
- />
202
- );
270
+ }: ModelSelectorLogoGroupProps) => {
271
+ const merge = useCn();
272
+ return (
273
+ <div
274
+ className={merge(
275
+ "flex shrink-0 items-center -space-x-1 [&>svg]:rounded-full [&>svg]:bg-surface [&>svg]:p-px",
276
+ className,
277
+ )}
278
+ {...props}
279
+ />
280
+ );
281
+ };
203
282
 
283
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
204
284
  export type ModelSelectorNameProps = ComponentProps<"span">;
205
285
 
286
+ /** @deprecated Use `ModelPicker`. Removed in the next minor release. */
206
287
  export const ModelSelectorName = ({
207
288
  className,
208
289
  ...props
@@ -0,0 +1,130 @@
1
+ "use client";
2
+
3
+ import type { ComponentProps } from "react";
4
+
5
+ import { Button } from "../ui/button";
6
+ import {
7
+ Card,
8
+ CardAction,
9
+ CardContent,
10
+ CardFooter,
11
+ CardHeader,
12
+ CardTitle,
13
+ } from "../ui/card";
14
+ import {
15
+ Collapsible,
16
+ CollapsibleContent,
17
+ CollapsibleTrigger,
18
+ } from "../ui/collapsible";
19
+ import { useCn } from "../ui/ui-config";
20
+ import { IconSelector } from "@tabler/icons-react";
21
+ import { createContext, useContext } from "react";
22
+
23
+ import { Shimmer } from "./shimmer";
24
+
25
+ interface PlanContextValue {
26
+ isStreaming: boolean;
27
+ }
28
+
29
+ const PlanContext = createContext<PlanContextValue | null>(null);
30
+
31
+ const usePlan = () => {
32
+ const context = useContext(PlanContext);
33
+ if (!context) {
34
+ throw new Error("AgentPlan components must be used within AgentPlan");
35
+ }
36
+ return context;
37
+ };
38
+
39
+ export type AgentPlanProps = ComponentProps<typeof Collapsible> & {
40
+ isStreaming?: boolean;
41
+ };
42
+
43
+ export const AgentPlan = ({
44
+ className,
45
+ isStreaming = false,
46
+ children,
47
+ ...props
48
+ }: AgentPlanProps) => {
49
+ const merge = useCn();
50
+ return (
51
+ <PlanContext.Provider value={{ isStreaming }}>
52
+ <Collapsible asChild data-slot="plan" {...props}>
53
+ <Card className={merge("smooth-shadow-none", className)}>{children}</Card>
54
+ </Collapsible>
55
+ </PlanContext.Provider>
56
+ );
57
+ };
58
+
59
+ export type AgentPlanHeaderProps = ComponentProps<typeof CardHeader>;
60
+
61
+ export const AgentPlanHeader = ({ className, ...props }: AgentPlanHeaderProps) => {
62
+ const merge = useCn();
63
+ return (
64
+ <CardHeader
65
+ className={merge(
66
+ "flex flex-row items-center justify-between gap-2",
67
+ className,
68
+ )}
69
+ data-slot="plan-header"
70
+ {...props}
71
+ />
72
+ );
73
+ };
74
+
75
+ export type AgentPlanTitleProps = Omit<
76
+ ComponentProps<typeof CardTitle>,
77
+ "children"
78
+ > & {
79
+ children: string;
80
+ };
81
+
82
+ export const AgentPlanTitle = ({ children, ...props }: AgentPlanTitleProps) => {
83
+ const { isStreaming } = usePlan();
84
+
85
+ return (
86
+ <CardTitle data-slot="plan-title" {...props}>
87
+ {isStreaming ? <Shimmer>{children}</Shimmer> : children}
88
+ </CardTitle>
89
+ );
90
+ };
91
+
92
+ export type AgentPlanActionProps = ComponentProps<typeof CardAction>;
93
+
94
+ export const AgentPlanAction = (props: AgentPlanActionProps) => (
95
+ <CardAction data-slot="plan-action" {...props} />
96
+ );
97
+
98
+ export type AgentPlanContentProps = ComponentProps<typeof CardContent>;
99
+
100
+ export const AgentPlanContent = (props: AgentPlanContentProps) => (
101
+ <CollapsibleContent asChild>
102
+ <CardContent data-slot="plan-content" {...props} />
103
+ </CollapsibleContent>
104
+ );
105
+
106
+ export type AgentPlanFooterProps = ComponentProps<"div">;
107
+
108
+ export const AgentPlanFooter = (props: AgentPlanFooterProps) => (
109
+ <CardFooter data-slot="plan-footer" {...props} />
110
+ );
111
+
112
+ export type AgentPlanTriggerProps = ComponentProps<typeof CollapsibleTrigger>;
113
+
114
+ export const AgentPlanTrigger = ({ className, ...props }: AgentPlanTriggerProps) => {
115
+ const merge = useCn();
116
+ return (
117
+ <CollapsibleTrigger asChild>
118
+ <Button
119
+ className={merge("size-8", className)}
120
+ data-slot="plan-trigger"
121
+ size="icon"
122
+ variant="ghost"
123
+ {...props}
124
+ >
125
+ <IconSelector className="size-4" />
126
+ <span className="sr-only">Toggle plan</span>
127
+ </Button>
128
+ </CollapsibleTrigger>
129
+ );
130
+ };