@vvedantb/ui 0.17.1 → 0.18.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 (129) 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/test-results.tsx +510 -0
  33. package/src/ai-elements/trace.tsx +243 -0
  34. package/src/ai-elements/use-streaming-disclosure.ts +50 -0
  35. package/src/ai-elements/web-preview.tsx +349 -0
  36. package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
  37. package/src/index.ts +66 -12
  38. package/src/kibo/avatar-stack.tsx +40 -0
  39. package/src/kibo/contribution-graph.tsx +539 -0
  40. package/src/kibo/cursor.tsx +72 -0
  41. package/src/kibo/gantt-features.tsx +439 -0
  42. package/src/kibo/gantt-header.tsx +181 -0
  43. package/src/kibo/gantt-provider.tsx +657 -0
  44. package/src/kibo/gantt-sidebar.tsx +157 -0
  45. package/src/kibo/gantt-timeline.tsx +154 -0
  46. package/src/kibo/index.ts +151 -0
  47. package/src/kibo/kanban.tsx +338 -0
  48. package/src/kibo/kanbanDropTarget.ts +25 -0
  49. package/src/kibo/list.tsx +162 -0
  50. package/src/kibo/theme-switcher.tsx +109 -0
  51. package/src/kibo/video-player.tsx +237 -0
  52. package/src/markdown/Markdown.tsx +112 -0
  53. package/src/markdown/MarkdownBlockquote.tsx +45 -0
  54. package/src/markdown/MarkdownCodeBlock.tsx +197 -0
  55. package/src/markdown/MarkdownInlineCode.tsx +40 -0
  56. package/src/markdown/MarkdownLink.tsx +92 -0
  57. package/src/markdown/codeFence.ts +57 -0
  58. package/src/markdown/codeHighlight.ts +138 -0
  59. package/src/markdown/incrementalBlocks.ts +182 -0
  60. package/src/markdown/index.ts +3 -0
  61. package/src/markdown/markdownClipboard.ts +178 -0
  62. package/src/markdown/mermaid.ts +155 -0
  63. package/src/markdown/rehypeGithubAlerts.ts +80 -0
  64. package/src/motion/presets.ts +24 -0
  65. package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
  66. package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
  67. package/src/ui/_menu-parts.tsx +29 -0
  68. package/src/ui/accordion.alt.tsx +46 -53
  69. package/src/ui/accordion.tsx +59 -10
  70. package/src/ui/avatar.tsx +56 -0
  71. package/src/ui/badge.tsx +45 -21
  72. package/src/ui/border-beam.tsx +97 -0
  73. package/src/ui/button-group.tsx +50 -21
  74. package/src/ui/button.tsx +67 -45
  75. package/src/ui/calendar.tsx +48 -5
  76. package/src/ui/card.tsx +77 -8
  77. package/src/ui/carousel.tsx +60 -0
  78. package/src/ui/centered-spinner.tsx +30 -0
  79. package/src/ui/checkbox.tsx +34 -16
  80. package/src/ui/clear-input.alt.tsx +33 -75
  81. package/src/ui/clear-input.tsx +140 -15
  82. package/src/ui/collapsible.tsx +40 -2
  83. package/src/ui/command.tsx +143 -70
  84. package/src/ui/context-menu.tsx +159 -93
  85. package/src/ui/crossfade-icon.tsx +123 -0
  86. package/src/ui/dialog.tsx +154 -70
  87. package/src/ui/dropdown-menu.tsx +177 -117
  88. package/src/ui/hover-card.tsx +59 -17
  89. package/src/ui/input-group.alt.tsx +83 -83
  90. package/src/ui/input-group.tsx +129 -58
  91. package/src/ui/input.tsx +13 -4
  92. package/src/ui/label.tsx +11 -8
  93. package/src/ui/list-row.tsx +232 -0
  94. package/src/ui/loading-state.tsx +215 -0
  95. package/src/ui/popover.tsx +56 -23
  96. package/src/ui/progress.tsx +28 -16
  97. package/src/ui/refresh-spin-icon.tsx +41 -0
  98. package/src/ui/search-input.tsx +66 -15
  99. package/src/ui/select.tsx +171 -100
  100. package/src/ui/separator.tsx +23 -14
  101. package/src/ui/slider.tsx +21 -7
  102. package/src/ui/sonner.tsx +68 -30
  103. package/src/ui/spinner.tsx +229 -11
  104. package/src/ui/status-dot.tsx +84 -0
  105. package/src/ui/surface.tsx +49 -0
  106. package/src/ui/switch.tsx +29 -15
  107. package/src/ui/table.tsx +38 -16
  108. package/src/ui/tabs.tsx +123 -35
  109. package/src/ui/tabsSliding.ts +33 -1
  110. package/src/ui/textarea.tsx +13 -4
  111. package/src/ui/tooltip.tsx +50 -14
  112. package/src/ui/ui-config.tsx +149 -0
  113. package/src/utils/cat-color.ts +123 -0
  114. package/src/utils/dayjs.ts +6 -0
  115. package/src/utils/gesture.ts +59 -0
  116. package/src/utils/runtimeVisibility.ts +167 -0
  117. package/src/utils/sharedClock.ts +96 -0
  118. package/src/utils/surface-radius.ts +16 -0
  119. package/src/utils/useDragSensors.ts +58 -0
  120. package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
  121. package/tokens/examples/tokens-moved/README.md +20 -0
  122. package/tokens/examples/tokens-moved/index.html +12 -0
  123. package/tokens/examples/tokens-moved/main.tsx +6 -0
  124. package/tokens/examples/tokens-moved/package.json +14 -0
  125. package/tokens/examples/tokens-moved/styles.css +12 -0
  126. package/tokens/examples/tokens-moved/tsconfig.json +14 -0
  127. package/tokens/examples/tokens-moved/vite.config.ts +11 -0
  128. package/tokens/src/css/border-beam.css +116 -0
  129. package/tokens/src/css/markdown.css +318 -0
@@ -0,0 +1,144 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuTrigger,
7
+ DropdownMenuContent,
8
+ DropdownMenuSub,
9
+ DropdownMenuSubTrigger,
10
+ DropdownMenuSubContent,
11
+ DropdownMenuRadioGroup,
12
+ DropdownMenuRadioItem,
13
+ } from "../ui/dropdown-menu";
14
+ import { useCn, useUiConfig } from "../ui/ui-config";
15
+ import { IconDots } from "@tabler/icons-react";
16
+
17
+ /** One model in `PromptInputSettings`, grouped by `provider`. */
18
+ export interface PromptInputSettingsOption<TModel extends string = string> {
19
+ id: TModel;
20
+ provider: string;
21
+ label: string;
22
+ }
23
+
24
+ /**
25
+ * `UiConfig.PromptInputSettings`. `providerLabel` names a provider group
26
+ * (default: the provider id); `providerIcon` renders the icon before a
27
+ * provider and before "Model" (default: none). `classes.trigger` replaces the
28
+ * trigger button's classes.
29
+ */
30
+ export interface PromptInputSettingsConfig {
31
+ providerLabel?: (provider: string) => string;
32
+ providerIcon?: (provider: string) => ReactNode;
33
+ classes?: {
34
+ trigger?: string;
35
+ };
36
+ }
37
+
38
+ function getProviderOptions<TModel extends string>(
39
+ options: ReadonlyArray<PromptInputSettingsOption<TModel>>,
40
+ providerLabel: (provider: string) => string,
41
+ ): ReadonlyArray<{ id: string; label: string }> {
42
+ const providers: Array<{ id: string; label: string }> = [];
43
+ for (const option of options) {
44
+ if (providers.some((provider) => provider.id === option.provider)) {
45
+ continue;
46
+ }
47
+ providers.push({
48
+ id: option.provider,
49
+ label: providerLabel(option.provider),
50
+ });
51
+ }
52
+ return providers;
53
+ }
54
+
55
+ export interface PromptInputSettingsProps<TModel extends string = string> {
56
+ model: TModel;
57
+ onModelChange: (model: TModel) => void;
58
+ options: ReadonlyArray<PromptInputSettingsOption<TModel>>;
59
+ disabled?: boolean;
60
+ icon?: ReactNode;
61
+ className?: string;
62
+ }
63
+
64
+ /**
65
+ * Overflow menu for the prompt footer with a "Model" submenu: one radio list,
66
+ * or one submenu per provider when the options span several providers.
67
+ */
68
+ export function PromptInputSettings<TModel extends string>({
69
+ model,
70
+ onModelChange,
71
+ options,
72
+ disabled,
73
+ icon,
74
+ className,
75
+ }: PromptInputSettingsProps<TModel>) {
76
+ const config = useUiConfig("PromptInputSettings");
77
+ const merge = useCn();
78
+ const providerLabel = config?.providerLabel ?? ((provider: string) => provider);
79
+ const providerIcon = config?.providerIcon;
80
+ const selectedModel =
81
+ options.find((entry) => entry.id === model) ?? options[0] ?? null;
82
+ const providerOptions = getProviderOptions(options, providerLabel);
83
+ const handleValueChange = (nextValue: string) => {
84
+ const option = options.find((entry) => entry.id === nextValue);
85
+ if (option) {
86
+ onModelChange(option.id);
87
+ }
88
+ };
89
+
90
+ return (
91
+ <DropdownMenu modal={false}>
92
+ <DropdownMenuTrigger asChild>
93
+ <button
94
+ className={merge(
95
+ config?.classes?.trigger ??
96
+ "rounded-lg p-1.5 text-muted-foreground transition-[color,background-color,scale] duration-150 ease-out hover:bg-surface-secondary hover:text-foreground active:scale-[0.92] disabled:opacity-50",
97
+ className,
98
+ )}
99
+ disabled={disabled}
100
+ >
101
+ {icon ?? <IconDots size={16} />}
102
+ </button>
103
+ </DropdownMenuTrigger>
104
+ <DropdownMenuContent align="start">
105
+ <DropdownMenuSub>
106
+ <DropdownMenuSubTrigger>
107
+ {providerIcon?.(selectedModel?.provider ?? "")}
108
+ Model
109
+ </DropdownMenuSubTrigger>
110
+ <DropdownMenuSubContent>
111
+ {providerOptions.map((provider) => {
112
+ const models = options.filter(
113
+ (option) => option.provider === provider.id,
114
+ );
115
+ const radios = (
116
+ <DropdownMenuRadioGroup
117
+ key={provider.id}
118
+ value={selectedModel?.id}
119
+ onValueChange={handleValueChange}
120
+ >
121
+ {models.map((option) => (
122
+ <DropdownMenuRadioItem key={option.id} value={option.id}>
123
+ {option.label}
124
+ </DropdownMenuRadioItem>
125
+ ))}
126
+ </DropdownMenuRadioGroup>
127
+ );
128
+ if (providerOptions.length === 1) return radios;
129
+ return (
130
+ <DropdownMenuSub key={provider.id}>
131
+ <DropdownMenuSubTrigger>
132
+ {providerIcon?.(provider.id)}
133
+ {provider.label}
134
+ </DropdownMenuSubTrigger>
135
+ <DropdownMenuSubContent>{radios}</DropdownMenuSubContent>
136
+ </DropdownMenuSub>
137
+ );
138
+ })}
139
+ </DropdownMenuSubContent>
140
+ </DropdownMenuSub>
141
+ </DropdownMenuContent>
142
+ </DropdownMenu>
143
+ );
144
+ }
@@ -0,0 +1,189 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { IconMicrophone, IconPlayerStop } from "@tabler/icons-react";
5
+ import { PromptInputButton } from "./prompt-input";
6
+
7
+ // Web Speech API dictation for the prompt input: `useSpeechRecognition` streams
8
+ // the transcript (prefix + finals + interim) to a callback, and
9
+ // `PromptInputSpeech` is a microphone button that writes it into the form's
10
+ // `textarea[name="message"]`.
11
+
12
+ interface SpeechRecognitionResult {
13
+ readonly isFinal: boolean;
14
+ readonly length: number;
15
+ [index: number]: { transcript: string; confidence: number };
16
+ }
17
+
18
+ interface SpeechRecognitionResultList {
19
+ readonly length: number;
20
+ [index: number]: SpeechRecognitionResult;
21
+ }
22
+
23
+ interface SpeechRecognitionEvent extends Event {
24
+ readonly resultIndex: number;
25
+ readonly results: SpeechRecognitionResultList;
26
+ }
27
+
28
+ interface SpeechRecognitionInstance extends EventTarget {
29
+ continuous: boolean;
30
+ interimResults: boolean;
31
+ lang: string;
32
+ onresult: ((event: SpeechRecognitionEvent) => void) | null;
33
+ onerror: ((event: Event) => void) | null;
34
+ onend: (() => void) | null;
35
+ start(): void;
36
+ stop(): void;
37
+ }
38
+
39
+ type SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;
40
+
41
+ // Read the constructors through a local type: a package must not augment the
42
+ // global Window. Apps often declare their own `Window.SpeechRecognition` with
43
+ // other event types, so `window` is cast to this shape rather than assigned
44
+ // (an assignment fails to typecheck in those apps; see
45
+ // consumer/window-speech-global.d.ts).
46
+ type SpeechRecognitionWindow = {
47
+ SpeechRecognition?: SpeechRecognitionConstructor;
48
+ webkitSpeechRecognition?: SpeechRecognitionConstructor;
49
+ };
50
+
51
+ /** The browser's `SpeechRecognition` constructor, or `null` without one. */
52
+ export function getSpeechRecognition(): SpeechRecognitionConstructor | null {
53
+ if (typeof window === "undefined") return null;
54
+ const speechWindow = window as unknown as SpeechRecognitionWindow;
55
+ return speechWindow.SpeechRecognition ?? speechWindow.webkitSpeechRecognition ?? null;
56
+ }
57
+
58
+ /**
59
+ * Continuous dictation. `toggle(prefix)` starts listening (or stops); each
60
+ * result calls `onUpdate` with `prefix` plus the transcript so far.
61
+ */
62
+ export function useSpeechRecognition(onUpdate: (fullText: string) => void) {
63
+ const [isListening, setIsListening] = useState(false);
64
+ const recognitionRef = useRef<SpeechRecognitionInstance | null>(null);
65
+ const onUpdateRef = useRef(onUpdate);
66
+ onUpdateRef.current = onUpdate;
67
+ const prefixRef = useRef("");
68
+
69
+ const stop = () => {
70
+ recognitionRef.current?.stop();
71
+ recognitionRef.current = null;
72
+ setIsListening(false);
73
+ };
74
+
75
+ const start = (prefix: string) => {
76
+ const SR = getSpeechRecognition();
77
+ if (!SR) return;
78
+
79
+ const separator = prefix && !prefix.endsWith(" ") ? " " : "";
80
+ prefixRef.current = prefix + separator;
81
+
82
+ const recognition = new SR();
83
+ recognition.continuous = true;
84
+ recognition.interimResults = true;
85
+ recognition.lang = "en-US";
86
+
87
+ recognition.onresult = (event: SpeechRecognitionEvent) => {
88
+ let finals = "";
89
+ let interim = "";
90
+ for (let i = 0; i < event.results.length; i++) {
91
+ const result = event.results[i];
92
+ if (!result) continue;
93
+ const text = result[0]?.transcript ?? "";
94
+ if (result.isFinal) {
95
+ finals += text;
96
+ } else {
97
+ interim += text;
98
+ }
99
+ }
100
+ onUpdateRef.current(prefixRef.current + finals + interim);
101
+ };
102
+
103
+ recognition.onerror = () => {
104
+ stop();
105
+ };
106
+
107
+ recognition.onend = () => {
108
+ setIsListening(false);
109
+ recognitionRef.current = null;
110
+ };
111
+
112
+ recognitionRef.current = recognition;
113
+ recognition.start();
114
+ setIsListening(true);
115
+ };
116
+
117
+ const toggle = (prefix: string) => {
118
+ if (isListening) {
119
+ stop();
120
+ } else {
121
+ start(prefix);
122
+ }
123
+ };
124
+
125
+ // Stop listening on unmount.
126
+ useEffect(
127
+ () => () => {
128
+ recognitionRef.current?.stop();
129
+ recognitionRef.current = null;
130
+ },
131
+ [],
132
+ );
133
+
134
+ return { isListening, toggle };
135
+ }
136
+
137
+ export interface PromptInputSpeechProps {
138
+ disabled?: boolean;
139
+ }
140
+
141
+ /** Microphone button for the prompt input. Renders nothing without the Web Speech API. */
142
+ export function PromptInputSpeech({ disabled }: PromptInputSpeechProps) {
143
+ if (!getSpeechRecognition()) return null;
144
+
145
+ return <SpeechButton disabled={disabled} />;
146
+ }
147
+
148
+ function setTextareaValue(textarea: HTMLTextAreaElement, value: string) {
149
+ const setter = Object.getOwnPropertyDescriptor(
150
+ HTMLTextAreaElement.prototype,
151
+ "value",
152
+ )?.set;
153
+ if (!setter) return;
154
+ setter.call(textarea, value);
155
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
156
+ }
157
+
158
+ function SpeechButton({ disabled }: PromptInputSpeechProps) {
159
+ const { isListening, toggle } = useSpeechRecognition((fullText) => {
160
+ const textarea = document.querySelector<HTMLTextAreaElement>(
161
+ 'textarea[name="message"]',
162
+ );
163
+ if (!textarea) return;
164
+ setTextareaValue(textarea, fullText);
165
+ textarea.focus();
166
+ });
167
+
168
+ const handleToggle = () => {
169
+ const textarea = document.querySelector<HTMLTextAreaElement>(
170
+ 'textarea[name="message"]',
171
+ );
172
+ toggle(textarea?.value ?? "");
173
+ };
174
+
175
+ return (
176
+ <PromptInputButton
177
+ tooltip={isListening ? "Stop recording" : "Voice input"}
178
+ onClick={handleToggle}
179
+ disabled={disabled}
180
+ className={isListening ? "text-destructive" : ""}
181
+ >
182
+ {isListening ? (
183
+ <IconPlayerStop className="size-4" />
184
+ ) : (
185
+ <IconMicrophone className="size-4" />
186
+ )}
187
+ </PromptInputButton>
188
+ );
189
+ }
@@ -0,0 +1,281 @@
1
+ "use client";
2
+
3
+ import { useOptionalPromptInputController } from "./prompt-input";
4
+ import { useCn, useUiConfig } from "../ui/ui-config";
5
+
6
+ const ULTRATHINK_PROMPT_PREFIX = "Ultrathink:\n";
7
+
8
+ export function isUltrathinkPrompt(text: string | null | undefined): boolean {
9
+ return typeof text === "string" && /\bultrathink\b/i.test(text);
10
+ }
11
+
12
+ export function applyUltrathinkPrefix(text: string): string {
13
+ const trimmed = text.trim();
14
+ if (!trimmed) {
15
+ return ULTRATHINK_PROMPT_PREFIX;
16
+ }
17
+ if (trimmed.startsWith("Ultrathink:")) {
18
+ return trimmed;
19
+ }
20
+ return `${ULTRATHINK_PROMPT_PREFIX}${trimmed}`;
21
+ }
22
+
23
+ /**
24
+ * `UiConfig.TraitsPanel`: classes that replace the row `label`, the `empty`
25
+ * text, the option button `option` base and its `optionSelected` /
26
+ * `optionIdle` states, and the ultrathink `note`.
27
+ */
28
+ export interface TraitsPanelConfig {
29
+ classes?: {
30
+ label?: string;
31
+ empty?: string;
32
+ option?: string;
33
+ optionSelected?: string;
34
+ optionIdle?: string;
35
+ note?: string;
36
+ };
37
+ }
38
+
39
+ export interface TraitsReasoningConfig {
40
+ levels: ReadonlyArray<string>;
41
+ default: string;
42
+ ultrathink?: boolean;
43
+ }
44
+
45
+ export interface TraitsMenuConfig {
46
+ reasoning?: TraitsReasoningConfig;
47
+ thinkingToggle?: boolean;
48
+ contextWindow1m?: boolean;
49
+ contextWindowDefaultLabel?: string;
50
+ fastMode?: boolean;
51
+ }
52
+
53
+ export interface TraitsMenuProps {
54
+ config: TraitsMenuConfig;
55
+ effortLevel: string | undefined;
56
+ thinkingEnabled: boolean;
57
+ use1mContext: boolean;
58
+ fastMode: boolean;
59
+ getLevelLabel: (level: string) => string;
60
+ onEffortLevelChange: (level: string) => void;
61
+ onThinkingEnabledChange: (enabled: boolean) => void;
62
+ onUse1mContextChange: (use1m: boolean) => void;
63
+ onFastModeChange: (enabled: boolean) => void;
64
+ disabled?: boolean;
65
+ className?: string;
66
+ }
67
+
68
+ interface TraitOption {
69
+ value: string;
70
+ label: string;
71
+ }
72
+
73
+ function TraitSegment({
74
+ label,
75
+ value,
76
+ options,
77
+ onChange,
78
+ disabled,
79
+ }: {
80
+ label: string;
81
+ value: string;
82
+ options: ReadonlyArray<TraitOption>;
83
+ onChange: (value: string) => void;
84
+ disabled?: boolean;
85
+ }) {
86
+ const classes = useUiConfig("TraitsPanel")?.classes;
87
+ const merge = useCn();
88
+ return (
89
+ <div className="flex items-start gap-2.5">
90
+ <span
91
+ className={
92
+ classes?.label ??
93
+ "w-[4.75rem] shrink-0 pt-1 text-[0.6875rem] leading-4 font-medium text-muted-foreground"
94
+ }
95
+ >
96
+ {label}
97
+ </span>
98
+ {options.length === 0 ? (
99
+ <span
100
+ className={
101
+ classes?.empty ?? "flex h-6 items-center text-[0.6875rem] leading-4 text-muted-foreground/70"
102
+ }
103
+ >
104
+ No options available
105
+ </span>
106
+ ) : (
107
+ <div
108
+ className="flex min-w-0 flex-1 flex-wrap gap-0.5"
109
+ role="radiogroup"
110
+ aria-label={label}
111
+ >
112
+ {options.map((option) => {
113
+ const selected = option.value === value;
114
+ return (
115
+ <button
116
+ key={option.value}
117
+ type="button"
118
+ role="radio"
119
+ aria-checked={selected}
120
+ disabled={disabled}
121
+ onMouseDown={(event) => event.preventDefault()}
122
+ onClick={() => onChange(option.value)}
123
+ className={merge(
124
+ classes?.option ??
125
+ "h-6 rounded-md px-2 text-[0.6875rem] leading-4 font-medium transition-[color,background-color,scale] duration-150 ease-out active:scale-[0.96]",
126
+ selected
127
+ ? (classes?.optionSelected ?? "bg-background text-foreground")
128
+ : (classes?.optionIdle ??
129
+ "text-muted-foreground hover:bg-surface-secondary/80 hover:text-foreground"),
130
+ disabled && "pointer-events-none opacity-50",
131
+ )}
132
+ >
133
+ {option.label}
134
+ </button>
135
+ );
136
+ })}
137
+ </div>
138
+ )}
139
+ </div>
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Compact trait controls for embedding in the model picker. Every section
145
+ * stays mounted so switching models cannot collapse the header; unsupported
146
+ * traits show a muted empty state.
147
+ */
148
+ export function TraitsPanel({
149
+ config,
150
+ effortLevel,
151
+ thinkingEnabled,
152
+ use1mContext,
153
+ fastMode,
154
+ getLevelLabel,
155
+ onEffortLevelChange,
156
+ onThinkingEnabledChange,
157
+ onUse1mContextChange,
158
+ onFastModeChange,
159
+ disabled,
160
+ className,
161
+ }: TraitsMenuProps) {
162
+ const controller = useOptionalPromptInputController();
163
+ const classes = useUiConfig("TraitsPanel")?.classes;
164
+ const merge = useCn();
165
+ const prompt = controller?.textInput.value ?? "";
166
+
167
+ const ultrathinkAvailable =
168
+ Boolean(config.reasoning?.ultrathink) && controller !== null;
169
+ const ultrathinkPromptControlled =
170
+ ultrathinkAvailable && isUltrathinkPrompt(prompt);
171
+ const ultrathinkInBodyText =
172
+ ultrathinkPromptControlled &&
173
+ isUltrathinkPrompt(prompt.replace(/^Ultrathink:\s*/i, ""));
174
+
175
+ const resolvedEffort = effortLevel ?? config.reasoning?.default ?? "";
176
+ const contextDefaultLabel = config.contextWindowDefaultLabel ?? "200K";
177
+
178
+ const handleEffortChange = (value: string) => {
179
+ if (!value || !config.reasoning) return;
180
+
181
+ if (value === "ultrathink") {
182
+ if (!controller) return;
183
+ const nextPrompt =
184
+ prompt.trim().length === 0
185
+ ? ULTRATHINK_PROMPT_PREFIX
186
+ : applyUltrathinkPrefix(prompt);
187
+ controller.textInput.setInput(nextPrompt);
188
+ return;
189
+ }
190
+
191
+ if (ultrathinkInBodyText) return;
192
+
193
+ if (ultrathinkPromptControlled && controller) {
194
+ controller.textInput.setInput(prompt.replace(/^Ultrathink:\s*/i, ""));
195
+ }
196
+
197
+ onEffortLevelChange(value);
198
+ };
199
+
200
+ const effortRadioValue = ultrathinkPromptControlled
201
+ ? "ultrathink"
202
+ : resolvedEffort;
203
+
204
+ const reasoningOptions: TraitOption[] = config.reasoning
205
+ ? [
206
+ ...config.reasoning.levels.map((level) => ({
207
+ value: level,
208
+ label: getLevelLabel(level),
209
+ })),
210
+ ...(ultrathinkAvailable
211
+ ? [{ value: "ultrathink", label: "Ultrathink" }]
212
+ : []),
213
+ ]
214
+ : [];
215
+
216
+ return (
217
+ <div className={merge("flex flex-col gap-1.5", className)}>
218
+ <div className="flex min-h-6 flex-col gap-1">
219
+ <TraitSegment
220
+ label="Reasoning"
221
+ value={effortRadioValue}
222
+ options={reasoningOptions}
223
+ onChange={handleEffortChange}
224
+ disabled={disabled || ultrathinkInBodyText}
225
+ />
226
+ {ultrathinkInBodyText ? (
227
+ <p
228
+ className={
229
+ classes?.note ?? "pl-[5.375rem] text-[0.6875rem] leading-snug text-muted-foreground/80"
230
+ }
231
+ >
232
+ Your prompt contains &quot;ultrathink&quot; in the text. Remove it
233
+ to change this option.
234
+ </p>
235
+ ) : null}
236
+ </div>
237
+ <TraitSegment
238
+ label="Speed"
239
+ value={fastMode ? "fast" : "standard"}
240
+ options={
241
+ config.fastMode
242
+ ? [
243
+ { value: "standard", label: "Standard" },
244
+ { value: "fast", label: "Fast" },
245
+ ]
246
+ : []
247
+ }
248
+ onChange={(value) => onFastModeChange(value === "fast")}
249
+ disabled={disabled}
250
+ />
251
+ <TraitSegment
252
+ label="Context"
253
+ value={use1mContext ? "1m" : "standard"}
254
+ options={
255
+ config.contextWindow1m
256
+ ? [
257
+ { value: "standard", label: contextDefaultLabel },
258
+ { value: "1m", label: "1M" },
259
+ ]
260
+ : []
261
+ }
262
+ onChange={(value) => onUse1mContextChange(value === "1m")}
263
+ disabled={disabled}
264
+ />
265
+ <TraitSegment
266
+ label="Thinking"
267
+ value={thinkingEnabled ? "on" : "off"}
268
+ options={
269
+ config.thinkingToggle
270
+ ? [
271
+ { value: "on", label: "On" },
272
+ { value: "off", label: "Off" },
273
+ ]
274
+ : []
275
+ }
276
+ onChange={(value) => onThinkingEnabledChange(value === "on")}
277
+ disabled={disabled}
278
+ />
279
+ </div>
280
+ );
281
+ }