devnonla-ui 0.1.0 → 0.3.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 (71) hide show
  1. package/README.md +22 -10
  2. package/package.json +2 -2
  3. package/src/alert/Alert.tsx +1 -1
  4. package/src/app/App.tsx +38 -18
  5. package/src/app/context.ts +32 -0
  6. package/src/button/Button.tsx +2 -2
  7. package/src/calendar/Calendar.tsx +4 -4
  8. package/src/chat/AgentPanel.tsx +15 -6
  9. package/src/chat/common/types.ts +27 -2
  10. package/src/chat/common/useAgentStream.ts +78 -12
  11. package/src/chat/common/utils.ts +13 -0
  12. package/src/chat/index.ts +5 -3
  13. package/src/chat/message-ui/ChatAgentMessage.tsx +1 -1
  14. package/src/chat/message-ui/ChatInput.tsx +1 -1
  15. package/src/chat/message-ui/ChatMarkdown.tsx +6 -6
  16. package/src/chat/message-ui/ChatMarkdownTable.tsx +1 -1
  17. package/src/chat/message-ui/ChatThinking.tsx +4 -3
  18. package/src/chat/message-ui/ChatUserMessage.tsx +0 -8
  19. package/src/chat/message-ui/MermaidBlock.tsx +11 -1
  20. package/src/chat/tool-ui/BackgroundTaskToolUI.tsx +2 -1
  21. package/src/chat/tool-ui/CallAgentToolUI.tsx +3 -2
  22. package/src/chat/tool-ui/ChatToolCall.tsx +3 -2
  23. package/src/chat/tool-ui/GetCurrentTimeToolUI.tsx +2 -1
  24. package/src/chat/tool-ui/ReadSkillToolUI.tsx +3 -2
  25. package/src/chat/tool-ui/RunJsToolUI.tsx +3 -2
  26. package/src/chat/tool-ui/WebFetchToolUI.tsx +3 -2
  27. package/src/chat/tool-ui/registry.ts +24 -12
  28. package/src/checkbox/Checkbox.tsx +5 -4
  29. package/src/codeblock/CodeBlock.tsx +1 -0
  30. package/src/colorpicker/ColorPicker.tsx +694 -30
  31. package/src/colorpicker/color.ts +246 -0
  32. package/src/datepicker/DatePicker.tsx +15 -9
  33. package/src/desktop/DesktopHeader.tsx +1 -1
  34. package/src/desktop/DesktopIcon.tsx +2 -4
  35. package/src/desktop/DesktopWindow.tsx +19 -5
  36. package/src/desktop/MeadowDesktop.tsx +2 -2
  37. package/src/drawer/Drawer.tsx +8 -6
  38. package/src/dropdown/ContextMenu.tsx +3 -1
  39. package/src/dropdown/Dropdown.tsx +8 -6
  40. package/src/form/Form.tsx +2 -2
  41. package/src/form/FormItem.tsx +2 -2
  42. package/src/form/partials/FormItemHelps.tsx +2 -2
  43. package/src/form/variants/FieldColor.tsx +16 -51
  44. package/src/form/variants/FieldObject.tsx +2 -2
  45. package/src/form/variants/FieldRepeaterItem.tsx +4 -4
  46. package/src/form-layout/FormLayout.tsx +6 -6
  47. package/src/index.ts +25 -8
  48. package/src/input/Input.tsx +11 -10
  49. package/src/lib/sizes.ts +13 -3
  50. package/src/lib/surface.ts +2 -1
  51. package/src/menu/Menu.tsx +2 -2
  52. package/src/message/message.tsx +35 -2
  53. package/src/modal/Modal.tsx +49 -3
  54. package/src/pagination/Pagination.tsx +6 -5
  55. package/src/popconfirm/Popconfirm.tsx +1 -1
  56. package/src/popover/Popover.tsx +6 -8
  57. package/src/scroll/OverlayScroll.tsx +5 -3
  58. package/src/segmented/Segmented.tsx +6 -5
  59. package/src/select/Select.tsx +9 -6
  60. package/src/shimmer/Shimmer.tsx +17 -0
  61. package/src/skeleton/Skeleton.tsx +3 -3
  62. package/src/spin/Spin.tsx +8 -5
  63. package/src/splitter/Splitter.tsx +330 -0
  64. package/src/splitter/sizes.ts +67 -0
  65. package/src/styles.css +96 -27
  66. package/src/switch/Switch.tsx +6 -5
  67. package/src/table/Table.tsx +6 -7
  68. package/src/tag/Tag.tsx +2 -2
  69. package/src/theme.ts +23 -3
  70. package/src/timepicker/TimePicker.tsx +13 -11
  71. package/src/tooltip/Tooltip.tsx +6 -7
@@ -24,11 +24,11 @@ export const chatMarkdownClass = cn(
24
24
  "[&_strong]:font-semibold [&_em]:italic",
25
25
  "[&_blockquote]:m-0 [&_blockquote]:mb-(--chat-p-mb) [&_blockquote]:border-0 [&_blockquote]:p-0 [&_blockquote]:not-italic [&_blockquote]:text-inherit [&_blockquote:last-child]:mb-0",
26
26
  "[&_ul]:mt-2 [&_ul]:mb-(--chat-p-mb)",
27
- "[&_ul:not(.contains-task-list)]:list-disc [&_ul:not(.contains-task-list)]:pl-[26px]",
28
- "[&_ul.contains-task-list]:list-none [&_ul.contains-task-list]:pl-[26px]",
27
+ "[&_ul:not(.contains-task-list)]:list-disc [&_ul:not(.contains-task-list)]:pl-6.5",
28
+ "[&_ul.contains-task-list]:list-none [&_ul.contains-task-list]:pl-6.5",
29
29
  "[&_li.task-list-item]:relative",
30
- "[&_li.task-list-item>.nonla-md-task-check]:absolute [&_li.task-list-item>.nonla-md-task-check]:top-0 [&_li.task-list-item>.nonla-md-task-check]:left-[-22px]",
31
- "[&_ol]:mt-2 [&_ol]:mb-(--chat-p-mb) [&_ol]:list-decimal [&_ol]:pl-[26px]",
30
+ "[&_li.task-list-item>.nonla-md-task-check]:absolute [&_li.task-list-item>.nonla-md-task-check]:top-0 [&_li.task-list-item>.nonla-md-task-check]:-left-5.5",
31
+ "[&_ol]:mt-2 [&_ol]:mb-(--chat-p-mb) [&_ol]:list-decimal [&_ol]:pl-6.5",
32
32
  "[&_li]:my-1.5 [&_li]:leading-(--chat-body-leading)",
33
33
  "[&_a]:text-link [&_a]:no-underline [&_a]:hover:underline [&_a]:hover:underline-offset-[3px]",
34
34
  );
@@ -78,14 +78,14 @@ export function createChatMarkdownComponents(getState?: () => ChatMarkdownStream
78
78
  th({ children }) {
79
79
  return (
80
80
  <th>
81
- <div className="inline-block max-w-[300px] wrap-break-word">{children}</div>
81
+ <div className="inline-block max-w-75 wrap-break-word">{children}</div>
82
82
  </th>
83
83
  );
84
84
  },
85
85
  td({ children }) {
86
86
  return (
87
87
  <td>
88
- <div className="inline-block max-w-[300px] wrap-break-word">{children}</div>
88
+ <div className="inline-block max-w-75 wrap-break-word">{children}</div>
89
89
  </td>
90
90
  );
91
91
  },
@@ -35,7 +35,7 @@ export function ChatMarkdownTable({ children }: { children: ReactNode }) {
35
35
  <div className="overflow-x-auto">
36
36
  <table
37
37
  ref={tableRef}
38
- className="w-max min-w-full wrap-normal border-separate border-spacing-0 text-[14px] [&_th]:bg-foreground/[0.04] [&_th]:text-foreground [&_th]:font-semibold [&_th]:px-2.5 [&_th]:py-1.5 [&_th]:text-left [&_th]:align-top [&_th]:border-r [&_th]:border-b [&_th]:border-border [&_th:last-child]:border-r-0 [&_td]:px-2.5 [&_td]:py-1.5 [&_td]:align-top [&_td]:text-foreground [&_td]:bg-card [&_td]:border-r [&_td]:border-b [&_td]:border-border [&_td:last-child]:border-r-0 [&_tbody_tr:last-child_td]:border-b-0"
38
+ className="w-max min-w-full wrap-normal border-separate border-spacing-0 text-[14px] [&_th]:bg-foreground/4 [&_th]:text-foreground [&_th]:font-semibold [&_th]:px-2.5 [&_th]:py-1.5 [&_th]:text-left [&_th]:align-top [&_th]:border-r [&_th]:border-b [&_th]:border-border [&_th:last-child]:border-r-0 [&_td]:px-2.5 [&_td]:py-1.5 [&_td]:align-top [&_td]:text-foreground [&_td]:bg-card [&_td]:border-r [&_td]:border-b [&_td]:border-border [&_td:last-child]:border-r-0 [&_tbody_tr:last-child_td]:border-b-0"
39
39
  >
40
40
  {children}
41
41
  </table>
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
2
  import { cn } from "../../lib/cn";
3
+ import { Shimmer } from "../../shimmer/Shimmer";
3
4
 
4
5
  export type ChatThinkingProps = {
5
6
  thinking: string;
@@ -46,9 +47,9 @@ export function ChatThinking({ thinking, duration = 0, streaming = false, classN
46
47
  if (el) el.scrollTop = el.scrollHeight;
47
48
  }}
48
49
  >
49
- <summary className="cursor-pointer select-none text-sm font-medium text-tertiary-foreground flex items-center gap-1 py-0.5 list-none [&::-webkit-details-marker]:hidden">
50
- <span className={streaming ? "nonla-chat-shimmer" : undefined}>{label}</span>
51
- <svg className="w-3 h-3 shrink-0 opacity-0 transition-[opacity,transform] duration-150 group-hover/thinking:opacity-100 group-open/thinking:opacity-100 group-open/thinking:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden>
50
+ <summary className="cursor-pointer select-none text-sm font-medium flex items-center gap-1 py-0.5 list-none [&::-webkit-details-marker]:hidden">
51
+ <Shimmer active={streaming} className="text-tertiary-foreground">{label}</Shimmer>
52
+ <svg className="w-3 h-3 shrink-0 opacity-0 text-tertiary-foreground transition-[opacity,transform] duration-150 group-hover/thinking:opacity-100 group-open/thinking:opacity-100 group-open/thinking:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden>
52
53
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
53
54
  </svg>
54
55
  </summary>
@@ -25,21 +25,13 @@ export function ChatUserMessage({ content, className }: ChatUserMessageProps) {
25
25
 
26
26
  const collapsed = isOverflow && !isExpanded;
27
27
 
28
- const toggle = () => {
29
- const sel = window.getSelection();
30
- if (sel?.toString()) return;
31
- setIsExpanded((v) => !v);
32
- };
33
-
34
28
  return (
35
29
  <div
36
30
  className={cn(
37
31
  "nonla-chat-user relative isolate mt-6 mb-3 mx-4 overflow-hidden rounded-xl px-3 py-2.5",
38
32
  "border border-brand/40 bg-[color-mix(in_oklab,var(--brand)_14%,var(--glass))] backdrop-blur-xl backdrop-saturate-150",
39
- isOverflow && "cursor-pointer",
40
33
  className,
41
34
  )}
42
- onClick={isOverflow ? toggle : undefined}
43
35
  >
44
36
  <div
45
37
  ref={contentRef}
@@ -257,7 +257,17 @@ export function MermaidBlock({ children, className }: MermaidBlockProps) {
257
257
  ) : null}
258
258
  </div>
259
259
 
260
- <div onWheel={handleWheel} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseLeave} onDoubleClick={resetView} className={cn("flex-1 overflow-hidden w-full h-full select-none", isDragging ? "cursor-grabbing" : "cursor-grab")}>
260
+ <div
261
+ role="application"
262
+ aria-label="Pan and zoom diagram"
263
+ onWheel={handleWheel}
264
+ onMouseDown={handleMouseDown}
265
+ onMouseMove={handleMouseMove}
266
+ onMouseUp={handleMouseUp}
267
+ onMouseLeave={handleMouseLeave}
268
+ onDoubleClick={resetView}
269
+ className={cn("flex-1 overflow-hidden w-full h-full select-none", isDragging ? "cursor-grabbing" : "cursor-grab")}
270
+ >
261
271
  <div ref={fullscreenRef} className="flex items-center justify-center w-full h-full origin-center [&_svg]:max-w-none [&_svg]:max-h-none pointer-events-none" style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})` }} />
262
272
  </div>
263
273
  </dialog>
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState } from "react";
2
+ import { Shimmer } from "../../shimmer/Shimmer";
2
3
  import { formatBgElapsed, parseBgTaskRef } from "../common/bgTasks";
3
4
  import { formatToolName, timestampMs } from "../common/utils";
4
5
  import { ChatSpinner } from "../message-ui/ChatSpinner";
@@ -24,7 +25,7 @@ export function BackgroundTaskToolUI({ msg, assistantLabel = "Assistant", assist
24
25
  <div className="min-w-0 flex-1 pl-2">
25
26
  <div className="flex items-center gap-2 py-1">
26
27
  <ChatSpinner />
27
- <span className="min-w-0 flex-1 truncate text-[14px] font-medium text-foreground">{label}</span>
28
+ <Shimmer className="min-w-0 flex-1 truncate text-[14px] font-medium text-foreground">{label}</Shimmer>
28
29
  <span className="shrink-0 rounded px-1 py-px text-[9px] font-semibold uppercase tracking-wider text-brand-700">Background</span>
29
30
  <span className="shrink-0 text-[14px] tabular-nums text-muted-foreground">{formatBgElapsed(timestampMs(msg.timestamp), now)}</span>
30
31
  </div>
@@ -1,6 +1,7 @@
1
1
  import { type ReactNode, useEffect, useRef, useState } from "react";
2
2
  import { FluentIcon } from "../../icon/FluentIcon";
3
3
  import { cn } from "../../lib/cn";
4
+ import { Shimmer } from "../../shimmer/Shimmer";
4
5
  import { formatBgElapsed, parseBgTaskRef } from "../common/bgTasks";
5
6
  import { parseJsonObject, prettyJson, timestampMs } from "../common/utils";
6
7
  import { ChatMarkdown } from "../message-ui/ChatMarkdown";
@@ -30,7 +31,7 @@ function parseCallAgentOutput(raw: unknown): CallAgentParsed | null {
30
31
 
31
32
  function TypingDots() {
32
33
  return (
33
- <div className="flex items-center gap-1 px-0.5 py-0.5" aria-label="Typing">
34
+ <div className="flex items-center gap-1 px-0.5 py-0.5" role="status" aria-label="Typing">
34
35
  {[0, 1, 2].map((i) => (
35
36
  <span key={i} className="size-1.5 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: `${i * 150}ms` }} />
36
37
  ))}
@@ -121,7 +122,7 @@ export function CallAgentToolUI({ msg, assistantLabel = "Assistant", assistantCo
121
122
  <div className={cn("overflow-hidden rounded-xl border bg-card", failed ? "border-destructive/35" : "border-border")}>
122
123
  <div className="flex items-center gap-2 border-b border-border-subtle px-3 py-2">
123
124
  <FluentIcon name="chat-24" size={13} className="shrink-0 text-muted-foreground" />
124
- <span className="min-w-0 flex-1 truncate text-[14px] font-medium text-foreground">Calling {calleeName}</span>
125
+ <Shimmer active={composing || awaitingReply} className="min-w-0 flex-1 truncate text-[14px] font-medium text-foreground">Calling {calleeName}</Shimmer>
125
126
  {bgRunning ? <span className="text-[14px] tabular-nums text-muted-foreground">{formatBgElapsed(timestampMs(msg.timestamp), now)}</span> : null}
126
127
  {statusLabel ? <span className="max-w-40 truncate text-[13px] italic text-destructive">{statusLabel}</span> : null}
127
128
  <ToolUiTrailing running={composing || awaitingReply} failed={failed && !awaitingReply} />
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode, useState } from "react";
2
2
  import { cn } from "../../lib/cn";
3
+ import { Shimmer } from "../../shimmer/Shimmer";
3
4
  import { formatToolName, hasMeaningfulInput, prettyJson } from "../common/utils";
4
5
  import { ChatSpinner } from "../message-ui/ChatSpinner";
5
6
  import { ToolUiTrailing } from "./ToolUiTrailing";
@@ -37,7 +38,7 @@ export function ChatToolCall({ toolName = "Tool", label, toolInput, toolOutput,
37
38
  const header = (
38
39
  <>
39
40
  {identityIcon}
40
- <span className={cn("min-w-0 flex-1 truncate text-left text-[14px] font-medium", running ? "nonla-chat-shimmer" : "text-muted-foreground", expandable && !running && "transition-colors group-hover:text-foreground")}>{displayLabel}</span>
41
+ <Shimmer active={running} className={cn("min-w-0 flex-1 truncate text-left text-[14px] font-medium", !running && "text-muted-foreground", expandable && !running && "transition-colors group-hover:text-foreground")}>{displayLabel}</Shimmer>
41
42
  <ToolUiTrailing running={running} failed={hasError} chevron={expandable} chevronClassName={cn("group-hover:opacity-100", open && "opacity-100 rotate-90")} />
42
43
  </>
43
44
  );
@@ -60,7 +61,7 @@ export function ChatToolCall({ toolName = "Tool", label, toolInput, toolOutput,
60
61
  {running ? (
61
62
  <div className="flex items-center gap-2 border-t border-border bg-muted/40 px-3 py-2 text-muted-foreground">
62
63
  <ChatSpinner />
63
- <span className="italic">Running…</span>
64
+ <Shimmer className="italic">Running…</Shimmer>
64
65
  </div>
65
66
  ) : null}
66
67
  {!isPending ? (
@@ -1,5 +1,6 @@
1
1
  import { FluentIcon } from "../../icon/FluentIcon";
2
2
  import { parseJsonObject } from "../common/utils";
3
+ import { Shimmer } from "../../shimmer/Shimmer";
3
4
  import { ToolUiBadge } from "./ToolUiBadge";
4
5
  import { ToolUiTrailing } from "./ToolUiTrailing";
5
6
  import { isToolRunning, type ToolUIProps } from "./types";
@@ -40,7 +41,7 @@ export function GetCurrentTimeToolUI({ msg, assistantLabel = "Assistant", assist
40
41
  <div className="px-4 pb-1">
41
42
  <div className="flex items-center gap-2 py-1">
42
43
  <FluentIcon name="clock-24" size={13} className="shrink-0 text-muted-foreground" />
43
- <span className="truncate text-[14px] font-medium tabular-nums text-muted-foreground">{hasError ? "Failed to get time" : running ? "Getting time…" : label}</span>
44
+ <Shimmer active={running} className="truncate text-[14px] font-medium tabular-nums text-muted-foreground">{hasError ? "Failed to get time" : running ? "Getting time…" : label}</Shimmer>
44
45
  <ToolUiTrailing running={running} failed={hasError} />
45
46
  </div>
46
47
  </div>
@@ -1,5 +1,6 @@
1
1
  import { FluentIcon } from "../../icon/FluentIcon";
2
2
  import { cn } from "../../lib/cn";
3
+ import { Shimmer } from "../../shimmer/Shimmer";
3
4
  import { parseJsonObject } from "../common/utils";
4
5
  import { ToolUiBadge } from "./ToolUiBadge";
5
6
  import { ToolUiTrailing } from "./ToolUiTrailing";
@@ -55,10 +56,10 @@ export function ReadSkillToolUI({ msg, assistantLabel = "Assistant", assistantCo
55
56
  <details className="group/readskill px-4 pb-2" style={{ overflowAnchor: "none" }}>
56
57
  <summary className="flex cursor-pointer list-none items-center gap-2 py-0.5 text-[14px] leading-5.5 select-none [&::-webkit-details-marker]:hidden">
57
58
  <FluentIcon name={reference ? "document-text-24" : "book-24"} size={13} className="shrink-0 text-muted-foreground" />
58
- <span className={cn("min-w-0 truncate font-medium text-muted-foreground", running && "nonla-chat-shimmer")}>
59
+ <Shimmer active={running} className="min-w-0 truncate font-medium text-muted-foreground">
59
60
  {verb}
60
61
  {targetLabel ? <span className="font-normal text-tertiary-foreground"> {targetLabel}</span> : null}
61
- </span>
62
+ </Shimmer>
62
63
  <ToolUiTrailing running={running} failed={failed} chevron chevronClassName="group-hover/readskill:opacity-100 group-open/readskill:opacity-100 group-open/readskill:rotate-90" />
63
64
  </summary>
64
65
  {body ? (
@@ -1,6 +1,7 @@
1
1
  import { FluentIcon } from "../../icon/FluentIcon";
2
2
  import { cn } from "../../lib/cn";
3
3
  import { CodeBlock } from "../../codeblock/CodeBlock";
4
+ import { Shimmer } from "../../shimmer/Shimmer";
4
5
  import { parseBgTaskRef } from "../common/bgTasks";
5
6
  import { parseJsonObject, prettyJson } from "../common/utils";
6
7
  import { ChatSpinner } from "../message-ui/ChatSpinner";
@@ -46,7 +47,7 @@ function SandboxOutput({
46
47
  return (
47
48
  <div className="flex items-center gap-2 border-t border-border px-3 py-2 text-[13px] text-muted-foreground">
48
49
  <ChatSpinner />
49
- <span>Running…</span>
50
+ <Shimmer>Running…</Shimmer>
50
51
  </div>
51
52
  );
52
53
  }
@@ -87,7 +88,7 @@ export function RunJsToolUI({ msg, assistantLabel = "Assistant", assistantColor,
87
88
  <details className="group/runjs px-4 pb-2" style={{ overflowAnchor: "none" }}>
88
89
  <summary className="flex cursor-pointer list-none items-center gap-2 py-0.5 text-[14px] leading-5.5 select-none [&::-webkit-details-marker]:hidden">
89
90
  <FluentIcon name="code-24" size={13} className="shrink-0 text-muted-foreground" />
90
- <span className={cn("min-w-0 truncate font-medium text-muted-foreground", running && "nonla-chat-shimmer")}>{verb}</span>
91
+ <Shimmer active={running} className="min-w-0 truncate font-medium text-muted-foreground">{verb}</Shimmer>
91
92
  <ToolUiTrailing running={running} failed={failed} chevron chevronClassName="group-hover/runjs:opacity-100 group-open/runjs:opacity-100 group-open/runjs:rotate-90" />
92
93
  </summary>
93
94
 
@@ -1,5 +1,6 @@
1
1
  import { FluentIcon } from "../../icon/FluentIcon";
2
2
  import { cn } from "../../lib/cn";
3
+ import { Shimmer } from "../../shimmer/Shimmer";
3
4
  import { parseJsonObject } from "../common/utils";
4
5
  import { ToolUiBadge } from "./ToolUiBadge";
5
6
  import { ToolUiTrailing } from "./ToolUiTrailing";
@@ -39,9 +40,9 @@ export function WebFetchToolUI({ msg, assistantLabel = "Assistant", assistantCol
39
40
  <details className="group/webfetch px-4 pb-2" style={{ overflowAnchor: "none" }}>
40
41
  <summary className="flex cursor-pointer list-none items-center gap-2 py-0.5 text-[14px] leading-5.5 select-none [&::-webkit-details-marker]:hidden">
41
42
  <FluentIcon name="globe-24" size={13} className="shrink-0 text-muted-foreground" />
42
- <span className={cn("min-w-0 truncate font-medium text-muted-foreground", running && "nonla-chat-shimmer")}>
43
+ <Shimmer active={running} className="min-w-0 truncate font-medium text-muted-foreground">
43
44
  {verb} <span className="font-normal text-tertiary-foreground">{url}</span>
44
- </span>
45
+ </Shimmer>
45
46
  <ToolUiTrailing running={running} failed={failed} chevron chevronClassName="group-hover/webfetch:opacity-100 group-open/webfetch:opacity-100 group-open/webfetch:rotate-90" />
46
47
  </summary>
47
48
  {body ? (
@@ -1,5 +1,6 @@
1
1
  import type { ComponentType } from "react";
2
- import { isCallAgentToolName } from "../common/utils";
2
+ import type { AgentToolNameMatch } from "../common/types";
3
+ import { isCallAgentToolName, matchesToolName } from "../common/utils";
3
4
  import { CallAgentToolUI } from "./CallAgentToolUI";
4
5
  import { GetCurrentTimeToolUI } from "./GetCurrentTimeToolUI";
5
6
  import { ReadSkillToolUI } from "./ReadSkillToolUI";
@@ -7,23 +8,34 @@ import { RunJsToolUI } from "./RunJsToolUI";
7
8
  import type { ToolUIProps } from "./types";
8
9
  import { WebFetchToolUI } from "./WebFetchToolUI";
9
10
 
10
- type ToolUIEntry = {
11
- match: (toolName: string) => boolean;
11
+ /** Exact name, aliases, or a predicate (e.g. `call_agent__*`). */
12
+ export type AgentToolUIName = AgentToolNameMatch;
13
+
14
+ export type AgentToolUI = {
15
+ name: AgentToolUIName;
12
16
  component: ComponentType<ToolUIProps>;
13
17
  };
14
18
 
15
- const TOOL_UIS: ToolUIEntry[] = [
16
- { match: isCallAgentToolName, component: CallAgentToolUI },
17
- { match: (n) => n === "web_fetch" || n === "fetch_url" || n === "browser", component: WebFetchToolUI },
18
- { match: (n) => n === "get_current_time", component: GetCurrentTimeToolUI },
19
- { match: (n) => n === "read_skill", component: ReadSkillToolUI },
20
- { match: (n) => n === "run_js", component: RunJsToolUI },
19
+ export function matchesToolUIName(name: AgentToolUIName, toolName: string): boolean {
20
+ return matchesToolName(name, toolName);
21
+ }
22
+
23
+ export const builtinToolUis: AgentToolUI[] = [
24
+ { name: isCallAgentToolName, component: CallAgentToolUI },
25
+ { name: ["web_fetch", "fetch_url", "browser"], component: WebFetchToolUI },
26
+ { name: "get_current_time", component: GetCurrentTimeToolUI },
27
+ { name: "read_skill", component: ReadSkillToolUI },
28
+ { name: "run_js", component: RunJsToolUI },
21
29
  ];
22
30
 
23
- export function resolveToolUI(toolName: string | null | undefined): ComponentType<ToolUIProps> | null {
31
+ /** App `toolUis` win on the same name; builtins are the fallback. */
32
+ export function resolveToolUI(toolName: string | null | undefined, extras?: readonly AgentToolUI[] | null): ComponentType<ToolUIProps> | null {
24
33
  if (!toolName) return null;
25
- for (const entry of TOOL_UIS) {
26
- if (entry.match(toolName)) return entry.component;
34
+ const lists = extras?.length ? [extras, builtinToolUis] : [builtinToolUis];
35
+ for (const list of lists) {
36
+ for (const entry of list) {
37
+ if (matchesToolUIName(entry.name, toolName)) return entry.component;
38
+ }
27
39
  }
28
40
  return null;
29
41
  }
@@ -5,7 +5,7 @@ import { cn } from "../lib/cn";
5
5
  export type CheckboxProps = Omit<ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>, "onCheckedChange" | "checked" | "onChange"> & {
6
6
  checked?: boolean | "indeterminate";
7
7
  defaultChecked?: boolean;
8
- /** antd-style — receives next checked boolean (not a DOM event). */
8
+ /** Receives next checked boolean (not a DOM event). */
9
9
  onChange?: (checked: boolean) => void;
10
10
  children?: ReactNode;
11
11
  indeterminate?: boolean;
@@ -25,6 +25,7 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(function Ch
25
25
  ) {
26
26
  const resolved = indeterminate ? "indeterminate" : checked;
27
27
  return (
28
+ // biome-ignore lint/a11y/noLabelWithoutControl: wraps Radix checkbox button, not a native input
28
29
  <label className={cn("inline-flex items-center gap-2 text-sm text-foreground cursor-pointer", disabled && "opacity-45 cursor-not-allowed")}>
29
30
  <CheckboxPrimitive.Root
30
31
  ref={ref}
@@ -33,9 +34,9 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(function Ch
33
34
  disabled={disabled}
34
35
  onCheckedChange={(v) => onChange?.(v === true)}
35
36
  className={cn(
36
- "size-4 shrink-0 rounded-[4px] border border-input bg-[var(--control-bg)] transition-colors duration-150 ease-[cubic-bezier(0.16,1,0.3,1)]",
37
- "focus-visible:outline-none data-[state=checked]:bg-brand data-[state=checked]:border-brand data-[state=checked]:text-[var(--nonla-solid-fg)]",
38
- "data-[state=indeterminate]:bg-brand data-[state=indeterminate]:border-brand data-[state=indeterminate]:text-[var(--nonla-solid-fg)]",
37
+ "size-4 shrink-0 rounded-sm border border-input bg-(--control-bg) transition-colors duration-150 ease-[cubic-bezier(0.16,1,0.3,1)]",
38
+ "focus-visible:outline-none data-[state=checked]:bg-brand data-[state=checked]:border-brand data-[state=checked]:text-(--nonla-solid-fg)",
39
+ "data-[state=indeterminate]:bg-brand data-[state=indeterminate]:border-brand data-[state=indeterminate]:text-(--nonla-solid-fg)",
39
40
  className,
40
41
  )}
41
42
  {...rest}
@@ -65,6 +65,7 @@ export function CodeBlock({ code, language, title, lineNumbers = false, wordWrap
65
65
  <div className={cn("nonla-codeblock-body nonla-codeblock-well relative min-w-0 overflow-y-auto bg-card font-mono text-[13px] leading-5", children ? "max-h-96" : "mx-0.5 mb-0.5 max-h-96 rounded-lg")}>
66
66
  <div className={cn("w-full", !wordWrap && "overflow-x-auto")}>
67
67
  <pre className={cn("nonla-codeblock-pre m-0 whitespace-pre break-normal", lineNumbers && "nonla-codeblock-lines", wordWrap && "whitespace-pre-wrap wrap-break-word")}>
68
+ {/* biome-ignore lint/security/noDangerouslySetInnerHtml: highlight.js emits escaped HTML */}
68
69
  <code className={cn("block px-3 py-2.5", !wordWrap && "w-max min-w-full")} dangerouslySetInnerHTML={{ __html: html }} />
69
70
  </pre>
70
71
  </div>