@nordmanai/nif 0.1.0 → 0.2.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 (253) hide show
  1. package/README.md +18 -0
  2. package/bin/nif.mjs +196 -0
  3. package/dist/index.d.ts +30 -1
  4. package/dist/index.js +9661 -2499
  5. package/dist/styles.css +1 -1
  6. package/dist/tokens.css +4 -1
  7. package/dist/ui/components/nm-ai/nm-ai.d.ts +7 -7
  8. package/dist/ui/components/nm-brand-logo/nm-brand-logo.d.ts +10 -0
  9. package/dist/ui/components/nm-breadcrumb/nm-breadcrumb.d.ts +11 -0
  10. package/dist/ui/components/nm-button/nm-button.d.ts +2 -0
  11. package/dist/ui/components/nm-button-group/nm-button-group.d.ts +17 -0
  12. package/dist/ui/components/nm-calendar/nm-calendar.d.ts +3 -0
  13. package/dist/ui/components/nm-card/nm-card.d.ts +34 -0
  14. package/dist/ui/components/nm-chip/nm-chip.d.ts +38 -0
  15. package/dist/ui/components/nm-code-block/nm-code-block.d.ts +55 -0
  16. package/dist/ui/components/nm-color-picker/nm-color-picker.d.ts +15 -0
  17. package/dist/ui/components/nm-combobox/nm-combobox.d.ts +26 -0
  18. package/dist/ui/components/nm-command/nm-command.d.ts +37 -0
  19. package/dist/ui/components/nm-composer/nm-composer.d.ts +4 -0
  20. package/dist/ui/components/nm-context-menu/nm-context-menu.d.ts +9 -0
  21. package/dist/ui/components/nm-date-picker/nm-date-picker.d.ts +35 -0
  22. package/dist/ui/components/nm-dialog/nm-dialog.d.ts +13 -0
  23. package/dist/ui/components/nm-field/nm-field.d.ts +2 -0
  24. package/dist/ui/components/nm-form-field/nm-form-field.d.ts +18 -0
  25. package/dist/ui/components/nm-gallery/nm-gallery.d.ts +8 -0
  26. package/dist/ui/components/nm-input/nm-input.d.ts +5 -0
  27. package/dist/ui/components/nm-input-group/nm-input-group.d.ts +9 -0
  28. package/dist/ui/components/nm-label/nm-label.d.ts +2 -0
  29. package/dist/ui/components/nm-menubar/nm-menubar.d.ts +9 -0
  30. package/dist/ui/components/nm-native-select/nm-native-select.d.ts +5 -0
  31. package/dist/ui/components/nm-panel/nm-panel.d.ts +14 -0
  32. package/dist/ui/components/nm-prose/nm-prose.d.ts +6 -0
  33. package/dist/ui/components/nm-resize-handle/nm-resize-handle.d.ts +18 -0
  34. package/dist/ui/components/nm-select/nm-select.d.ts +22 -0
  35. package/dist/ui/components/nm-side-panel/nm-side-panel.d.ts +33 -0
  36. package/dist/ui/components/nm-slider/nm-slider.d.ts +5 -1
  37. package/dist/ui/components/nm-status-bar/nm-status-bar.d.ts +8 -0
  38. package/dist/ui/components/nm-table/nm-table.d.ts +38 -0
  39. package/dist/ui/components/nm-textarea/nm-textarea.d.ts +2 -0
  40. package/dist/ui/components/nm-toolbar/nm-toolbar.d.ts +21 -0
  41. package/dist/ui/config/nm-ai.d.ts +0 -3
  42. package/dist/ui/config/nm-appearance.d.ts +36 -1
  43. package/dist/ui/config/nm-button.d.ts +2 -1
  44. package/dist/ui/config/nm-composer.d.ts +1 -2
  45. package/dist/ui/config/nm-field.d.ts +2 -0
  46. package/dist/ui/config/nm-icons.d.ts +13 -1
  47. package/dist/ui/config/nm-radius.d.ts +4 -4
  48. package/dist/ui/config/nm-side-panel.d.ts +33 -0
  49. package/dist/ui/lib/appearance/appearanceState.d.ts +1 -0
  50. package/dist/ui/lib/appearance/deriveTokens.d.ts +2 -0
  51. package/package.json +12 -4
  52. package/registry/index.json +1634 -0
  53. package/registry/tokens.css +72 -0
  54. package/registry/ui/components/nm-ai/nm-ai.css +358 -0
  55. package/registry/ui/components/nm-ai/nm-ai.tsx +632 -0
  56. package/registry/ui/components/nm-app-panel/nm-app-panel.css +169 -0
  57. package/registry/ui/components/nm-ask/nm-ask.css +277 -0
  58. package/registry/ui/components/nm-avatar/nm-avatar.css +85 -0
  59. package/registry/ui/components/nm-avatar/nm-avatar.tsx +30 -0
  60. package/registry/ui/components/nm-bar-overflow/nm-bar-overflow.css +43 -0
  61. package/registry/ui/components/nm-bar-overflow/nm-bar-overflow.tsx +61 -0
  62. package/registry/ui/components/nm-bar-overflow/use-bar-overflow.ts +92 -0
  63. package/registry/ui/components/nm-brand-logo/nm-brand-logo.css +20 -0
  64. package/registry/ui/components/nm-brand-logo/nm-brand-logo.tsx +21 -0
  65. package/registry/ui/components/nm-breadcrumb/nm-breadcrumb.css +57 -0
  66. package/registry/ui/components/nm-breadcrumb/nm-breadcrumb.tsx +42 -0
  67. package/registry/ui/components/nm-button/nm-button.css +110 -0
  68. package/registry/ui/components/nm-button/nm-button.tsx +58 -0
  69. package/registry/ui/components/nm-button-group/nm-button-group.css +87 -0
  70. package/registry/ui/components/nm-button-group/nm-button-group.tsx +32 -0
  71. package/registry/ui/components/nm-calendar/nm-calendar.css +178 -0
  72. package/registry/ui/components/nm-calendar/nm-calendar.tsx +43 -0
  73. package/registry/ui/components/nm-card/nm-card.css +130 -0
  74. package/registry/ui/components/nm-card/nm-card.tsx +83 -0
  75. package/registry/ui/components/nm-chatbox/nm-chatbox-icon.tsx +39 -0
  76. package/registry/ui/components/nm-chatbox/nm-chatbox.css +682 -0
  77. package/registry/ui/components/nm-chatbox/nm-chatbox.tsx +101 -0
  78. package/registry/ui/components/nm-checkbox/nm-checkbox.css +61 -0
  79. package/registry/ui/components/nm-checkbox/nm-checkbox.tsx +14 -0
  80. package/registry/ui/components/nm-chip/nm-chip.css +112 -0
  81. package/registry/ui/components/nm-chip/nm-chip.tsx +86 -0
  82. package/registry/ui/components/nm-code-block/nm-code-block.css +240 -0
  83. package/registry/ui/components/nm-code-block/nm-code-block.tsx +196 -0
  84. package/registry/ui/components/nm-collection/nm-collection-bar.tsx +65 -0
  85. package/registry/ui/components/nm-collection/nm-collection-grid.tsx +69 -0
  86. package/registry/ui/components/nm-collection/nm-collection-query.tsx +99 -0
  87. package/registry/ui/components/nm-collection/nm-collection-row-menu.tsx +55 -0
  88. package/registry/ui/components/nm-collection/nm-collection-table.tsx +99 -0
  89. package/registry/ui/components/nm-collection/nm-collection-tile-actions.tsx +50 -0
  90. package/registry/ui/components/nm-collection/nm-collection-title.tsx +25 -0
  91. package/registry/ui/components/nm-collection/nm-collection.css +353 -0
  92. package/registry/ui/components/nm-collection/use-collection-scroll-reveal.ts +38 -0
  93. package/registry/ui/components/nm-color-picker/nm-color-picker.css +67 -0
  94. package/registry/ui/components/nm-color-picker/nm-color-picker.tsx +79 -0
  95. package/registry/ui/components/nm-combobox/nm-combobox.css +72 -0
  96. package/registry/ui/components/nm-combobox/nm-combobox.tsx +80 -0
  97. package/registry/ui/components/nm-command/nm-command.css +190 -0
  98. package/registry/ui/components/nm-command/nm-command.tsx +125 -0
  99. package/registry/ui/components/nm-composer/nm-composer.css +178 -0
  100. package/registry/ui/components/nm-composer/nm-composer.tsx +151 -0
  101. package/registry/ui/components/nm-context-menu/nm-context-menu.tsx +25 -0
  102. package/registry/ui/components/nm-date-picker/nm-date-picker.css +140 -0
  103. package/registry/ui/components/nm-date-picker/nm-date-picker.tsx +153 -0
  104. package/registry/ui/components/nm-dialog/nm-dialog.css +84 -0
  105. package/registry/ui/components/nm-dialog/nm-dialog.tsx +40 -0
  106. package/registry/ui/components/nm-dropdown/nm-dropdown-item.ts +68 -0
  107. package/registry/ui/components/nm-dropdown/nm-dropdown-options.tsx +242 -0
  108. package/registry/ui/components/nm-dropdown/nm-dropdown-panel.tsx +171 -0
  109. package/registry/ui/components/nm-dropdown/nm-dropdown-placement.ts +157 -0
  110. package/registry/ui/components/nm-dropdown/nm-dropdown.css +312 -0
  111. package/registry/ui/components/nm-dropdown/nm-dropdown.tsx +412 -0
  112. package/registry/ui/components/nm-dropdown-menu/nm-dropdown-menu.css +145 -0
  113. package/registry/ui/components/nm-dropdown-menu/nm-dropdown-menu.tsx +118 -0
  114. package/registry/ui/components/nm-field/nm-field.css +356 -0
  115. package/registry/ui/components/nm-field/nm-field.tsx +355 -0
  116. package/registry/ui/components/nm-form-field/nm-form-field.css +103 -0
  117. package/registry/ui/components/nm-form-field/nm-form-field.tsx +71 -0
  118. package/registry/ui/components/nm-gallery/nm-gallery.css +66 -0
  119. package/registry/ui/components/nm-gallery/nm-gallery.tsx +31 -0
  120. package/registry/ui/components/nm-hover-card/nm-hover-card.css +26 -0
  121. package/registry/ui/components/nm-hover-card/nm-hover-card.tsx +28 -0
  122. package/registry/ui/components/nm-icon/nm-icon.css +6 -0
  123. package/registry/ui/components/nm-icon/nm-icon.tsx +69 -0
  124. package/registry/ui/components/nm-icon/nm-lucide.tsx +180 -0
  125. package/registry/ui/components/nm-icon-border/nm-icon-border.tsx +56 -0
  126. package/registry/ui/components/nm-input/nm-input.css +75 -0
  127. package/registry/ui/components/nm-input/nm-input.tsx +7 -0
  128. package/registry/ui/components/nm-input-group/nm-input-group.css +66 -0
  129. package/registry/ui/components/nm-input-group/nm-input-group.tsx +20 -0
  130. package/registry/ui/components/nm-label/nm-label.css +11 -0
  131. package/registry/ui/components/nm-label/nm-label.tsx +6 -0
  132. package/registry/ui/components/nm-list-item/nm-list-item.css +177 -0
  133. package/registry/ui/components/nm-list-item/nm-list-item.tsx +112 -0
  134. package/registry/ui/components/nm-menubar/nm-menubar.css +32 -0
  135. package/registry/ui/components/nm-menubar/nm-menubar.tsx +19 -0
  136. package/registry/ui/components/nm-message/nm-message-actions.tsx +24 -0
  137. package/registry/ui/components/nm-message/nm-message-meta.tsx +49 -0
  138. package/registry/ui/components/nm-message/nm-message.css +686 -0
  139. package/registry/ui/components/nm-message/nm-text-shimmer.tsx +6 -0
  140. package/registry/ui/components/nm-message/nm-trace.tsx +219 -0
  141. package/registry/ui/components/nm-modal/nm-modal.css +763 -0
  142. package/registry/ui/components/nm-modal/nm-modal.tsx +497 -0
  143. package/registry/ui/components/nm-native-select/nm-native-select.css +47 -0
  144. package/registry/ui/components/nm-native-select/nm-native-select.tsx +23 -0
  145. package/registry/ui/components/nm-panel/nm-panel.css +73 -0
  146. package/registry/ui/components/nm-panel/nm-panel.tsx +35 -0
  147. package/registry/ui/components/nm-panel-bar/nm-panel-bar.css +20 -0
  148. package/registry/ui/components/nm-panel-bar/nm-panel-bar.tsx +48 -0
  149. package/registry/ui/components/nm-popover/nm-popover.css +43 -0
  150. package/registry/ui/components/nm-popover/nm-popover.tsx +40 -0
  151. package/registry/ui/components/nm-progress/nm-progress.css +21 -0
  152. package/registry/ui/components/nm-progress/nm-progress.tsx +14 -0
  153. package/registry/ui/components/nm-prose/nm-prose.css +81 -0
  154. package/registry/ui/components/nm-prose/nm-prose.tsx +10 -0
  155. package/registry/ui/components/nm-questionnaire/nm-questionnaire.css +182 -0
  156. package/registry/ui/components/nm-questionnaire/nm-questionnaire.tsx +221 -0
  157. package/registry/ui/components/nm-radio-group/nm-radio-group.css +48 -0
  158. package/registry/ui/components/nm-radio-group/nm-radio-group.tsx +15 -0
  159. package/registry/ui/components/nm-rail-icon/nm-rail-icon.css +6 -0
  160. package/registry/ui/components/nm-rail-icon/nm-rail-icon.tsx +28 -0
  161. package/registry/ui/components/nm-resize-handle/nm-resize-handle.css +45 -0
  162. package/registry/ui/components/nm-resize-handle/nm-resize-handle.tsx +102 -0
  163. package/registry/ui/components/nm-segmented/nm-segmented.css +104 -0
  164. package/registry/ui/components/nm-segmented/nm-segmented.tsx +77 -0
  165. package/registry/ui/components/nm-select/nm-select.css +78 -0
  166. package/registry/ui/components/nm-select/nm-select.tsx +64 -0
  167. package/registry/ui/components/nm-separator/nm-separator.css +14 -0
  168. package/registry/ui/components/nm-separator/nm-separator.tsx +6 -0
  169. package/registry/ui/components/nm-side-panel/nm-side-panel.css +93 -0
  170. package/registry/ui/components/nm-side-panel/nm-side-panel.tsx +86 -0
  171. package/registry/ui/components/nm-slider/nm-slider.css +53 -0
  172. package/registry/ui/components/nm-slider/nm-slider.tsx +40 -0
  173. package/registry/ui/components/nm-small-icon-button/nm-small-icon-button.tsx +66 -0
  174. package/registry/ui/components/nm-status-bar/nm-status-bar.css +50 -0
  175. package/registry/ui/components/nm-status-bar/nm-status-bar.tsx +28 -0
  176. package/registry/ui/components/nm-switch/nm-switch.css +45 -0
  177. package/registry/ui/components/nm-switch/nm-switch.tsx +10 -0
  178. package/registry/ui/components/nm-table/nm-table.css +155 -0
  179. package/registry/ui/components/nm-table/nm-table.tsx +95 -0
  180. package/registry/ui/components/nm-tabs/nm-tabs.css +28 -0
  181. package/registry/ui/components/nm-tabs/nm-tabs.tsx +32 -0
  182. package/registry/ui/components/nm-textarea/nm-textarea.css +41 -0
  183. package/registry/ui/components/nm-textarea/nm-textarea.tsx +6 -0
  184. package/registry/ui/components/nm-toggle/nm-toggle.css +55 -0
  185. package/registry/ui/components/nm-toggle/nm-toggle.tsx +16 -0
  186. package/registry/ui/components/nm-toolbar/nm-toolbar.css +42 -0
  187. package/registry/ui/components/nm-toolbar/nm-toolbar.tsx +54 -0
  188. package/registry/ui/components/nm-tooltip/nm-tooltip.css +37 -0
  189. package/registry/ui/components/nm-tooltip/nm-tooltip.tsx +35 -0
  190. package/registry/ui/components/nm-top-bar/nm-top-bar.css +29 -0
  191. package/registry/ui/components/nm-top-bar/nm-top-bar.tsx +34 -0
  192. package/registry/ui/components/nordman-icon-button/nordman-icon-button.css +119 -0
  193. package/registry/ui/components/nordman-icon-button/nordman-icon-button.tsx +185 -0
  194. package/registry/ui/components/nordman-login/nordman-login.css +195 -0
  195. package/registry/ui/components/nordman-login/nordman-login.tsx +136 -0
  196. package/registry/ui/components/nordman-login-field/nordman-login-field.tsx +55 -0
  197. package/registry/ui/components/nordman-login-forgot/nordman-login-forgot.tsx +56 -0
  198. package/registry/ui/components/nordman-login-google/nordman-login-google.tsx +28 -0
  199. package/registry/ui/components/nordman-login-hero/nordman-login-hero.tsx +38 -0
  200. package/registry/ui/components/nordman-login-register/nordman-login-register.tsx +70 -0
  201. package/registry/ui/components/nordman-login-reset/nordman-login-reset.tsx +71 -0
  202. package/registry/ui/components/nordman-login-reveal/nordman-login-reveal.tsx +41 -0
  203. package/registry/ui/components/nordman-login-sign-in/nordman-login-sign-in.tsx +69 -0
  204. package/registry/ui/components/nordman-toggle/nordman-toggle.css +28 -0
  205. package/registry/ui/components/nordman-toggle/nordman-toggle.tsx +35 -0
  206. package/registry/ui/config/nm-ai.ts +59 -0
  207. package/registry/ui/config/nm-appearance.ts +275 -0
  208. package/registry/ui/config/nm-bar-overflow.ts +10 -0
  209. package/registry/ui/config/nm-button.ts +38 -0
  210. package/registry/ui/config/nm-chatbox-icons.ts +67 -0
  211. package/registry/ui/config/nm-chatbox.ts +42 -0
  212. package/registry/ui/config/nm-collection.ts +101 -0
  213. package/registry/ui/config/nm-composer.ts +39 -0
  214. package/registry/ui/config/nm-dropdown.ts +62 -0
  215. package/registry/ui/config/nm-field.ts +43 -0
  216. package/registry/ui/config/nm-icon-button.ts +35 -0
  217. package/registry/ui/config/nm-icons.ts +417 -0
  218. package/registry/ui/config/nm-list-item.ts +14 -0
  219. package/registry/ui/config/nm-message.ts +106 -0
  220. package/registry/ui/config/nm-modal.ts +76 -0
  221. package/registry/ui/config/nm-panel-bar.ts +11 -0
  222. package/registry/ui/config/nm-questionnaire.ts +53 -0
  223. package/registry/ui/config/nm-radius.ts +56 -0
  224. package/registry/ui/config/nm-rail-icons.ts +27 -0
  225. package/registry/ui/config/nm-segmented.ts +19 -0
  226. package/registry/ui/config/nm-side-panel.ts +16 -0
  227. package/registry/ui/config/nm-site-palette.ts +30 -0
  228. package/registry/ui/config/nm-small-icon-button.ts +17 -0
  229. package/registry/ui/config/nm-tabs.ts +9 -0
  230. package/registry/ui/config/nm-top-bar.ts +4 -0
  231. package/registry/ui/config/nordman-chrome.ts +14 -0
  232. package/registry/ui/config/nordman-login.ts +28 -0
  233. package/registry/ui/content/nm-modal.ts +5 -0
  234. package/registry/ui/content/nordman-login.ts +85 -0
  235. package/registry/ui/kit-logger.ts +1 -0
  236. package/registry/ui/lib/appearance/appearanceState.ts +240 -0
  237. package/registry/ui/lib/appearance/applyAppearance.ts +36 -0
  238. package/registry/ui/lib/appearance/cloudOrbAppearance.ts +36 -0
  239. package/registry/ui/lib/appearance/color.ts +96 -0
  240. package/registry/ui/lib/appearance/deriveTokens.ts +218 -0
  241. package/registry/ui/lib/appearance/index.ts +6 -0
  242. package/registry/ui/lib/appearance/markTokens.ts +49 -0
  243. package/registry/ui/lib/appearance/readLayoutMetrics.ts +28 -0
  244. package/registry/ui/lib/bar-overflow.ts +41 -0
  245. package/registry/ui/lib/logger.ts +53 -0
  246. package/registry/ui/lib/menu-motion.ts +187 -0
  247. package/registry/ui/lib/nm-class.ts +4 -0
  248. package/registry/ui/lib/nordman-ui-locale.ts +43 -0
  249. package/registry/ui/styles/interface-theme.css +196 -0
  250. package/registry/ui/styles/light-appearance.css +72 -0
  251. package/registry/ui/styles/tokens.css +424 -0
  252. package/registry/ui/styles/utilities.css +12 -0
  253. package/dist/ui/components/nm-accordion/nm-accordion.d.ts +0 -6
@@ -0,0 +1,632 @@
1
+ import { NmChip } from '../nm-chip/nm-chip.tsx';
2
+ import { NmCodeBlock, NmCodeBlockContent, NmCodeBlockHeader, NmCodeBlockTitle } from '../nm-code-block/nm-code-block.tsx';
3
+ import { useState, type ReactNode } from 'react';
4
+ import {
5
+ NM_AI_CLASSES as C,
6
+ nmAiTraceStatus,
7
+ type NmAiBadgeTone,
8
+ type NmAiStatus,
9
+ type NmToolState,
10
+ } from '../../config/nm-ai.ts';
11
+ import type { NmTraceGlyphKind } from '../../config/nm-message.ts';
12
+ import type { NmDropdownItem } from '../nm-dropdown/nm-dropdown-item.ts';
13
+ import { NmButton } from '../nm-button/nm-button.tsx';
14
+ import { NmDropdown } from '../nm-dropdown/nm-dropdown.tsx';
15
+ import { NmHoverCard, NmHoverCardContent, NmHoverCardTrigger } from '../nm-hover-card/nm-hover-card.tsx';
16
+ import { NmIcon } from '../nm-icon/nm-icon.tsx';
17
+ import { NmProgress } from '../nm-progress/nm-progress.tsx';
18
+ import { nmButtonClassName } from '../../config/nm-button.ts';
19
+ import { NmMessageActions } from '../nm-message/nm-message-actions.tsx';
20
+ import { NmMessageMeta } from '../nm-message/nm-message-meta.tsx';
21
+ import { NmTextShimmer } from '../nm-message/nm-text-shimmer.tsx';
22
+ import {
23
+ NmTrace,
24
+ NmTraceBadge,
25
+ NmTraceBadges,
26
+ NmTraceCollapsible,
27
+ NmTraceCollapsibleContent,
28
+ NmTraceCollapsibleTrigger,
29
+ NmTraceDescription,
30
+ NmTraceGlyph,
31
+ NmTraceIcon,
32
+ NmTraceLine,
33
+ NmTraceTitle,
34
+ } from '../nm-message/nm-trace.tsx';
35
+
36
+ const STATUS_GLYPH: Record<NmAiStatus, NmTraceGlyphKind> = {
37
+ pending: 'circle',
38
+ running: 'loader',
39
+ done: 'check',
40
+ error: 'alert',
41
+ };
42
+
43
+ function StatusIcon(props: { status: NmAiStatus }): ReactNode {
44
+ return (
45
+ <NmTraceIcon>
46
+ <NmTraceGlyph kind={STATUS_GLYPH[props.status]} spin={props.status === 'running'} />
47
+ </NmTraceIcon>
48
+ );
49
+ }
50
+
51
+ /* ----- Primitives ----- */
52
+
53
+ export function NmBubble(props: { role: 'user' | 'assistant'; children: ReactNode }): ReactNode {
54
+ return <div className={props.role === 'user' ? `${C.bubble} ${C.bubbleUser}` : C.bubble}>{props.children}</div>;
55
+ }
56
+
57
+ /** Output, errors and logs inside a trace: the NIF Code Block, without line numbers. */
58
+ function NmOutput(props: { children: string; label?: string; nowrap?: boolean; maxHeight?: string }): ReactNode {
59
+ return (
60
+ <NmCodeBlock size="sm">
61
+ {props.label ? <NmCodeBlockHeader><NmCodeBlockTitle>{props.label}</NmCodeBlockTitle></NmCodeBlockHeader> : null}
62
+ <NmCodeBlockContent wrap={!props.nowrap} style={props.maxHeight ? { maxHeight: props.maxHeight } : undefined}>{props.children}</NmCodeBlockContent>
63
+ </NmCodeBlock>
64
+ );
65
+ }
66
+
67
+ export function NmBadge(props: { tone?: NmAiBadgeTone; children: ReactNode }): ReactNode {
68
+ return <NmChip size="sm" tone={props.tone ?? 'neutral'}>{props.children}</NmChip>;
69
+ }
70
+
71
+ export function NmItem(props: { media?: ReactNode; title: ReactNode; description?: ReactNode; actions?: ReactNode }): ReactNode {
72
+ return (
73
+ <div className={C.item}>
74
+ {props.media ? <span className={C.itemMedia}>{props.media}</span> : null}
75
+ <span className={C.itemBody}>
76
+ <span className={C.itemTitle}>{props.title}</span>
77
+ {props.description ? <span className={C.itemDescription}>{props.description}</span> : null}
78
+ </span>
79
+ {props.actions ? <span className={C.itemActions}>{props.actions}</span> : null}
80
+ </div>
81
+ );
82
+ }
83
+
84
+ export function NmSheet(props: { title: string; description?: string; onClose: () => void; children: ReactNode }): ReactNode {
85
+ return (
86
+ <section className={C.sheet} aria-label={props.title}>
87
+ <div className={C.sheetHead}>
88
+ <span>{props.title}</span>
89
+ <NmButton label="Close" tone="muted" size="compact" icon="x" onClick={props.onClose} />
90
+ </div>
91
+ {props.description ? <span className={C.itemDescription}>{props.description}</span> : null}
92
+ <div className={C.sheetBody}>{props.children}</div>
93
+ </section>
94
+ );
95
+ }
96
+
97
+ /* ----- Message ----- */
98
+
99
+ export type NmAiMessageAction = { id: string; label: string; icon: 'copy' | 'reload' | 'pen' | 'check' | 'volume' | 'fork' };
100
+
101
+ export function NmMessage(props: {
102
+ role: 'user' | 'assistant';
103
+ children: ReactNode;
104
+ actions?: readonly NmAiMessageAction[];
105
+ onAction?: (id: string) => void;
106
+ footer?: ReactNode;
107
+ }): ReactNode {
108
+ return (
109
+ <div className={props.role === 'user' ? `${C.turn} ${C.turnUser}` : C.turn}>
110
+ <div className={C.turnBody}>
111
+ {props.role === 'user' ? <NmBubble role="user">{props.children}</NmBubble> : props.children}
112
+ </div>
113
+ {props.actions ? (
114
+ <NmMessageActions>
115
+ {props.actions.map((action) => (
116
+ <NmButton key={action.id} label={action.label} tone="muted" size="compact" icon={action.icon} onClick={() => props.onAction?.(action.id)} />
117
+ ))}
118
+ </NmMessageActions>
119
+ ) : null}
120
+ {props.footer ? <div className={C.turnFooter}>{props.footer}</div> : null}
121
+ </div>
122
+ );
123
+ }
124
+
125
+ /* ----- Reasoning ----- */
126
+
127
+ export function NmReasoning(props: { text: string; streaming?: boolean; seconds?: number; defaultOpen?: boolean }): ReactNode {
128
+ const label = props.streaming ? 'Thinking' : `Thought for ${props.seconds ?? 0} seconds`;
129
+ return (
130
+ <NmTrace ariaLabel="Reasoning">
131
+ <NmTraceCollapsible defaultOpen={props.defaultOpen}>
132
+ <NmTraceCollapsibleTrigger>
133
+ <NmTraceLine status={props.streaming ? 'running' : undefined}>
134
+ <NmTraceIcon><NmTraceGlyph kind={props.streaming ? 'loader' : 'check'} spin={props.streaming} /></NmTraceIcon>
135
+ <NmTraceTitle shimmer={props.streaming}>{label}</NmTraceTitle>
136
+ </NmTraceLine>
137
+ </NmTraceCollapsibleTrigger>
138
+ <NmTraceCollapsibleContent><NmTraceDescription>{props.text}</NmTraceDescription></NmTraceCollapsibleContent>
139
+ </NmTraceCollapsible>
140
+ </NmTrace>
141
+ );
142
+ }
143
+
144
+ /* ----- Chain of thought ----- */
145
+
146
+ export type NmChainStep = { id: string; title: string; status: NmAiStatus; detail?: string; results?: readonly string[] };
147
+
148
+ export function NmChainOfThought(props: { summary: string; steps: readonly NmChainStep[]; defaultOpen?: boolean }): ReactNode {
149
+ return (
150
+ <NmTrace ariaLabel="Chain of thought">
151
+ <NmTraceCollapsible defaultOpen={props.defaultOpen}>
152
+ <NmTraceCollapsibleTrigger>
153
+ <NmTraceLine><NmTraceIcon><NmTraceGlyph kind="plan" /></NmTraceIcon><NmTraceTitle>{props.summary}</NmTraceTitle></NmTraceLine>
154
+ </NmTraceCollapsibleTrigger>
155
+ <NmTraceCollapsibleContent>
156
+ <ol className={C.steps}>
157
+ {props.steps.map((step) => (
158
+ <li key={step.id} className={C.stepsItem}>
159
+ <NmTraceLine status={nmAiTraceStatus(step.status)}>
160
+ <StatusIcon status={step.status} />
161
+ <NmTraceTitle shimmer={step.status === 'running'}>{step.title}</NmTraceTitle>
162
+ </NmTraceLine>
163
+ {step.detail ? <NmTraceDescription>{step.detail}</NmTraceDescription> : null}
164
+ {step.results ? <NmTraceBadges>{step.results.map((r) => <NmTraceBadge key={r}>{r}</NmTraceBadge>)}</NmTraceBadges> : null}
165
+ </li>
166
+ ))}
167
+ </ol>
168
+ </NmTraceCollapsibleContent>
169
+ </NmTraceCollapsible>
170
+ </NmTrace>
171
+ );
172
+ }
173
+
174
+ /* ----- Checkpoint ----- */
175
+
176
+ export function NmCheckpoint(props: { label: string; restoreLabel?: string; onRestore?: () => void }): ReactNode {
177
+ return (
178
+ <div className="nm-ai-checkpoint" role="separator" aria-label={props.label}>
179
+ <span className="nm-ai-checkpoint__label">
180
+ <NmIcon id="folder" />
181
+ {props.label}
182
+ {props.onRestore ? <NmButton label={props.restoreLabel ?? 'Restore'} tone="muted" size="chip" icon="reload" onClick={props.onRestore} /> : null}
183
+ </span>
184
+ </div>
185
+ );
186
+ }
187
+
188
+ /* ----- Confirmation ----- */
189
+
190
+ export type NmConfirmationState = 'requested' | 'approved' | 'denied';
191
+
192
+ export function NmConfirmation(props: {
193
+ title: string;
194
+ description?: string;
195
+ state: NmConfirmationState;
196
+ onApprove?: () => void;
197
+ onDeny?: () => void;
198
+ }): ReactNode {
199
+ if (props.state !== 'requested') {
200
+ return (
201
+ <NmTrace ariaLabel="Confirmation">
202
+ <NmTraceLine status={props.state === 'approved' ? 'running' : undefined}>
203
+ <NmTraceIcon><NmTraceGlyph kind={props.state === 'approved' ? 'check' : 'alert'} /></NmTraceIcon>
204
+ <NmTraceTitle>{props.state === 'approved' ? `Approved ${props.title}` : `Denied ${props.title}`}</NmTraceTitle>
205
+ </NmTraceLine>
206
+ </NmTrace>
207
+ );
208
+ }
209
+ return (
210
+ <NmItem
211
+ media={<NmIcon id="terminal" />}
212
+ title={props.title}
213
+ description={props.description}
214
+ actions={<>
215
+ <NmButton label="Deny" tone="outline" size="compact" onClick={props.onDeny} />
216
+ <NmButton label="Approve" tone="filled" size="compact" onClick={props.onApprove} />
217
+ </>}
218
+ />
219
+ );
220
+ }
221
+
222
+ /* ----- Context ----- */
223
+
224
+ function formatTokens(value: number): string {
225
+ return value < 1000 ? String(value) : `${(value / 1000).toFixed(1).replace(/\.0$/, '')}k`;
226
+ }
227
+
228
+ export function NmContext(props: { used: number; max: number; breakdown?: readonly { label: string; value: number }[] }): ReactNode {
229
+ const percent = Math.round((props.used / props.max) * 100);
230
+ return (
231
+ <NmHoverCard>
232
+ <NmHoverCardTrigger delay={10} closeDelay={100} render={<button type="button" className={nmButtonClassName('outline', 'compact')} />}>
233
+ {percent}% context
234
+ </NmHoverCardTrigger>
235
+ <NmHoverCardContent>
236
+ <div className="nm-ai-context__head">
237
+ <span>Context</span>
238
+ <span>{formatTokens(props.used)} / {formatTokens(props.max)}</span>
239
+ </div>
240
+ <NmProgress value={props.used} max={props.max} aria-label="Context used" />
241
+ {props.breakdown ? (
242
+ <div className="nm-ai-context__rows">
243
+ {props.breakdown.map((row) => <div key={row.label}><span>{row.label}</span><span>{formatTokens(row.value)}</span></div>)}
244
+ </div>
245
+ ) : null}
246
+ </NmHoverCardContent>
247
+ </NmHoverCard>
248
+ );
249
+ }
250
+
251
+ /* ----- File tree ----- */
252
+
253
+ export type NmFileNode = { name: string; children?: readonly NmFileNode[] };
254
+
255
+ type TreeSelection = { selected?: string; onSelect?: (path: string) => void };
256
+
257
+ function TreeNodes(props: { nodes: readonly NmFileNode[]; defaultOpen: boolean; parent: string } & TreeSelection): ReactNode {
258
+ return (
259
+ <ul className={C.tree} role="group">
260
+ {props.nodes.map((node) => <TreeNode key={node.name} node={node} path={props.parent ? `${props.parent}/${node.name}` : node.name} defaultOpen={props.defaultOpen} selected={props.selected} onSelect={props.onSelect} />)}
261
+ </ul>
262
+ );
263
+ }
264
+
265
+ function TreeNode(props: { node: NmFileNode; path: string; defaultOpen: boolean } & TreeSelection): ReactNode {
266
+ const [open, setOpen] = useState(props.defaultOpen);
267
+ const folder = props.node.children != null;
268
+ return (
269
+ <li role="treeitem" aria-expanded={folder ? open : undefined} aria-selected={!folder && props.selected === props.path ? true : undefined}>
270
+ <button
271
+ type="button"
272
+ className={C.treeNode}
273
+ data-kind={folder ? 'folder' : 'file'}
274
+ data-selected={!folder && props.selected === props.path ? '' : undefined}
275
+ onClick={() => (folder ? setOpen(!open) : props.onSelect?.(props.path))}
276
+ >
277
+ {folder ? <span className="nm-ai-tree__chevron" data-open={open}><NmIcon id="chevron" /></span> : null}
278
+ <NmIcon id={folder ? 'folder' : 'file'} />
279
+ <span>{props.node.name}</span>
280
+ </button>
281
+ {folder && open ? <TreeNodes nodes={props.node.children!} defaultOpen={props.defaultOpen} parent={props.path} selected={props.selected} onSelect={props.onSelect} /> : null}
282
+ </li>
283
+ );
284
+ }
285
+
286
+ /** Folders open and close. Give it `onSelect` and a file can be chosen. `selected` is its path, like `src/App.tsx`. */
287
+ export function NmFileTree(props: { nodes: readonly NmFileNode[]; defaultOpen?: boolean } & TreeSelection): ReactNode {
288
+ return <div role="tree"><TreeNodes nodes={props.nodes} defaultOpen={props.defaultOpen ?? true} parent="" selected={props.selected} onSelect={props.onSelect} /></div>;
289
+ }
290
+
291
+ /* ----- Inline citation ----- */
292
+
293
+ export function NmInlineCitation(props: { label: string; title: string; url: string; excerpt?: string }): ReactNode {
294
+ return (
295
+ <NmHoverCard>
296
+ <NmHoverCardTrigger className={C.citation} href={props.url} aria-label={`Source: ${props.title}`}>{props.label}</NmHoverCardTrigger>
297
+ <NmHoverCardContent>
298
+ <strong>{props.title}</strong>
299
+ <span className={C.itemDescription}>{props.url}</span>
300
+ {props.excerpt ? <span>{props.excerpt}</span> : null}
301
+ </NmHoverCardContent>
302
+ </NmHoverCard>
303
+ );
304
+ }
305
+
306
+ /* ----- Model selector ----- */
307
+
308
+ export function NmModelSelector(props: {
309
+ models: readonly { id: string; label: string }[];
310
+ model: string;
311
+ onModel: (id: string) => void;
312
+ efforts: readonly { id: string; label: string }[];
313
+ effort: string;
314
+ onEffort: (id: string) => void;
315
+ }): ReactNode {
316
+ const items = (rows: readonly { id: string; label: string }[], selected: string): NmDropdownItem[] =>
317
+ rows.map((row) => ({ id: row.id, label: row.label, checked: row.id === selected }));
318
+ const labelOf = (rows: readonly { id: string; label: string }[], id: string) => rows.find((r) => r.id === id)?.label ?? id;
319
+ return (
320
+ <div className={C.selector}>
321
+ <NmDropdown label="Model" displayLabel={labelOf(props.models, props.model)} items={items(props.models, props.model)} selected={props.model} onSelect={props.onModel} trigger="text" />
322
+ <NmDropdown label="Effort" displayLabel={labelOf(props.efforts, props.effort)} items={items(props.efforts, props.effort)} selected={props.effort} onSelect={props.onEffort} trigger="text" />
323
+ </div>
324
+ );
325
+ }
326
+
327
+ /* ----- Plan, queue, task ----- */
328
+
329
+ export type NmPlanStep = { id: string; title: string; status: NmAiStatus; detail?: string };
330
+
331
+ export function NmPlan(props: { title: string; steps: readonly NmPlanStep[]; defaultOpen?: boolean }): ReactNode {
332
+ const done = props.steps.filter((s) => s.status === 'done').length;
333
+ return (
334
+ <NmTrace ariaLabel="Plan">
335
+ <NmTraceCollapsible defaultOpen={props.defaultOpen}>
336
+ <NmTraceCollapsibleTrigger>
337
+ <NmTraceLine>
338
+ <NmTraceIcon><NmTraceGlyph kind="plan" /></NmTraceIcon>
339
+ <NmTraceTitle>{props.title}</NmTraceTitle>
340
+ <NmTraceBadges><NmTraceBadge>{done}/{props.steps.length}</NmTraceBadge></NmTraceBadges>
341
+ </NmTraceLine>
342
+ </NmTraceCollapsibleTrigger>
343
+ <NmTraceCollapsibleContent>
344
+ <ol className={C.steps}>
345
+ {props.steps.map((s) => (
346
+ <li key={s.id} className={C.stepsItem}>
347
+ <NmTraceLine status={nmAiTraceStatus(s.status)}><StatusIcon status={s.status} /><NmTraceTitle>{s.title}</NmTraceTitle></NmTraceLine>
348
+ {s.detail ? <NmTraceDescription>{s.detail}</NmTraceDescription> : null}
349
+ </li>
350
+ ))}
351
+ </ol>
352
+ </NmTraceCollapsibleContent>
353
+ </NmTraceCollapsible>
354
+ </NmTrace>
355
+ );
356
+ }
357
+
358
+ export type NmQueueItem = { id: string; title: string; description?: string };
359
+
360
+ export function NmQueue(props: { remaining: readonly NmQueueItem[]; completed: readonly NmQueueItem[]; defaultOpen?: boolean }): ReactNode {
361
+ const group = (title: string, kind: NmTraceGlyphKind, items: readonly NmQueueItem[], done: boolean) => (
362
+ <NmTraceCollapsible defaultOpen={props.defaultOpen}>
363
+ <NmTraceCollapsibleTrigger>
364
+ <NmTraceLine><NmTraceIcon><NmTraceGlyph kind={kind} /></NmTraceIcon><NmTraceTitle>{title}</NmTraceTitle><NmTraceBadges><NmTraceBadge>{items.length}</NmTraceBadge></NmTraceBadges></NmTraceLine>
365
+ </NmTraceCollapsibleTrigger>
366
+ <NmTraceCollapsibleContent>
367
+ <ul className={C.steps}>
368
+ {items.map((i) => (
369
+ <li key={i.id} className={C.stepsItem}>
370
+ <NmTraceLine status={done ? undefined : 'pending'}><NmTraceIcon><NmTraceGlyph kind={done ? 'check' : 'circle'} /></NmTraceIcon><NmTraceTitle>{i.title}</NmTraceTitle></NmTraceLine>
371
+ {i.description ? <NmTraceDescription>{i.description}</NmTraceDescription> : null}
372
+ </li>
373
+ ))}
374
+ </ul>
375
+ </NmTraceCollapsibleContent>
376
+ </NmTraceCollapsible>
377
+ );
378
+ return <NmTrace ariaLabel="Queue">{group('To do', 'todo', props.remaining, false)}{group('Completed', 'completed', props.completed, true)}</NmTrace>;
379
+ }
380
+
381
+ export type NmTaskStep = { id: string; title: string; status: NmAiStatus; icon?: 'search' | 'file' };
382
+
383
+ export function NmTask(props: { title: string; steps: readonly NmTaskStep[] }): ReactNode {
384
+ return (
385
+ <NmTrace ariaLabel={props.title}>
386
+ <NmTraceLine><NmTraceIcon><NmTraceGlyph kind="todo" /></NmTraceIcon><NmTraceTitle>{props.title}</NmTraceTitle></NmTraceLine>
387
+ {props.steps.map((s) => (
388
+ <NmTraceLine key={s.id} status={nmAiTraceStatus(s.status)}>
389
+ <NmTraceIcon><NmTraceGlyph kind={s.status === 'done' ? (s.icon === 'search' ? 'search' : 'read') : STATUS_GLYPH[s.status]} spin={s.status === 'running'} /></NmTraceIcon>
390
+ <NmTraceTitle shimmer={s.status === 'running'}>{s.title}</NmTraceTitle>
391
+ </NmTraceLine>
392
+ ))}
393
+ </NmTrace>
394
+ );
395
+ }
396
+
397
+ /* ----- Sources ----- */
398
+
399
+ export function NmSources(props: { sources: readonly { title: string; url: string }[]; defaultOpen?: boolean }): ReactNode {
400
+ return (
401
+ <NmTrace ariaLabel="Sources">
402
+ <NmTraceCollapsible defaultOpen={props.defaultOpen}>
403
+ <NmTraceCollapsibleTrigger>
404
+ <NmTraceLine><NmTraceIcon><NmTraceGlyph kind="source" /></NmTraceIcon><NmTraceTitle>Used {props.sources.length} sources</NmTraceTitle></NmTraceLine>
405
+ </NmTraceCollapsibleTrigger>
406
+ <NmTraceCollapsibleContent asList>
407
+ {props.sources.map((s) => (
408
+ <NmTraceLine key={s.url} as="li">
409
+ <NmTraceIcon><NmTraceGlyph kind="external" /></NmTraceIcon>
410
+ <a className="nm-trace__link" href={s.url}>{s.title}</a>
411
+ </NmTraceLine>
412
+ ))}
413
+ </NmTraceCollapsibleContent>
414
+ </NmTraceCollapsible>
415
+ </NmTrace>
416
+ );
417
+ }
418
+
419
+ /* ----- Terminal, stack trace, test results ----- */
420
+
421
+ export function NmTerminal(props: { command: string; output?: string; exitCode?: number; running?: boolean; defaultOpen?: boolean }): ReactNode {
422
+ const failed = props.exitCode != null && props.exitCode !== 0;
423
+ const title = props.running ? `Running ${props.command}` : failed ? `${props.command} exited with code ${props.exitCode}` : `Ran ${props.command}`;
424
+ return (
425
+ <NmTrace ariaLabel="Terminal">
426
+ <NmTraceCollapsible defaultOpen={props.defaultOpen}>
427
+ <NmTraceCollapsibleTrigger>
428
+ <NmTraceLine status={props.running ? 'running' : failed ? 'error' : undefined}>
429
+ <NmTraceIcon><NmTraceGlyph kind="command" /></NmTraceIcon>
430
+ <NmTraceTitle shimmer={props.running}>{title}</NmTraceTitle>
431
+ </NmTraceLine>
432
+ </NmTraceCollapsibleTrigger>
433
+ <NmTraceCollapsibleContent><NmOutput nowrap maxHeight="16rem">{props.output ?? ''}</NmOutput></NmTraceCollapsibleContent>
434
+ </NmTraceCollapsible>
435
+ </NmTrace>
436
+ );
437
+ }
438
+
439
+ export type NmStackFrame = { fn: string; file: string; line: number; internal?: boolean };
440
+
441
+ export function NmStackTrace(props: { error: string; frames: readonly NmStackFrame[]; defaultOpen?: boolean }): ReactNode {
442
+ const app = props.frames.filter((f) => !f.internal);
443
+ const internal = props.frames.filter((f) => f.internal);
444
+ const row = (f: NmStackFrame) => (
445
+ <div key={`${f.file}:${f.line}:${f.fn}`} className={C.frame} data-internal={f.internal ? 'true' : undefined}>
446
+ <span>at {f.fn}</span><span>{f.file}:{f.line}</span>
447
+ </div>
448
+ );
449
+ return (
450
+ <NmTrace ariaLabel="Stack trace">
451
+ <NmTraceCollapsible defaultOpen={props.defaultOpen}>
452
+ <NmTraceCollapsibleTrigger>
453
+ <NmTraceLine status="error"><NmTraceIcon><NmTraceGlyph kind="alert" /></NmTraceIcon><NmTraceTitle>{props.error}</NmTraceTitle></NmTraceLine>
454
+ </NmTraceCollapsibleTrigger>
455
+ <NmTraceCollapsibleContent>
456
+ <div className={C.stack}>
457
+ {app.map(row)}
458
+ {internal.length > 0 ? (
459
+ <NmTraceCollapsible>
460
+ <NmTraceCollapsibleTrigger><NmTraceLine><NmTraceTitle>{internal.length} internal frames</NmTraceTitle></NmTraceLine></NmTraceCollapsibleTrigger>
461
+ <NmTraceCollapsibleContent>{internal.map(row)}</NmTraceCollapsibleContent>
462
+ </NmTraceCollapsible>
463
+ ) : null}
464
+ </div>
465
+ </NmTraceCollapsibleContent>
466
+ </NmTraceCollapsible>
467
+ </NmTrace>
468
+ );
469
+ }
470
+
471
+ export type NmTestStatus = 'passed' | 'failed' | 'skipped';
472
+ export type NmTestSuite = { name: string; tests: readonly { name: string; status: NmTestStatus; error?: string }[] };
473
+
474
+ export function NmTestResults(props: { suites: readonly NmTestSuite[]; defaultOpen?: boolean }): ReactNode {
475
+ const all = props.suites.flatMap((s) => s.tests);
476
+ const failed = all.filter((t) => t.status === 'failed').length;
477
+ const skipped = all.filter((t) => t.status === 'skipped').length;
478
+ const passed = all.length - failed - skipped;
479
+ return (
480
+ <NmTrace ariaLabel="Test results">
481
+ <NmTraceCollapsible defaultOpen={props.defaultOpen}>
482
+ <NmTraceCollapsibleTrigger>
483
+ <NmTraceLine status={failed > 0 ? 'error' : undefined}>
484
+ <NmTraceIcon><NmTraceGlyph kind={failed > 0 ? 'alert' : 'check'} /></NmTraceIcon>
485
+ <NmTraceTitle>{failed > 0 ? `${failed} of ${all.length} tests failed` : `${all.length} tests passed`}</NmTraceTitle>
486
+ <NmTraceBadges><NmTraceBadge>{passed} passed</NmTraceBadge>{failed ? <NmTraceBadge>{failed} failed</NmTraceBadge> : null}{skipped ? <NmTraceBadge>{skipped} skipped</NmTraceBadge> : null}</NmTraceBadges>
487
+ </NmTraceLine>
488
+ </NmTraceCollapsibleTrigger>
489
+ <NmTraceCollapsibleContent>
490
+ {props.suites.map((suite) => (
491
+ <div key={suite.name} className={C.stack}>
492
+ <NmTraceDescription>{suite.name}</NmTraceDescription>
493
+ <ul className={C.tests}>
494
+ {suite.tests.map((t) => (
495
+ <li key={t.name} className={C.testRow} data-status={t.status}>
496
+ <NmTraceGlyph kind={t.status === 'passed' ? 'check' : t.status === 'failed' ? 'alert' : 'circle'} />
497
+ <span>{t.name}</span>
498
+ </li>
499
+ ))}
500
+ </ul>
501
+ {suite.tests.filter((t) => t.error).map((t) => <NmOutput key={t.name} label={t.name}>{t.error!}</NmOutput>)}
502
+ </div>
503
+ ))}
504
+ </NmTraceCollapsibleContent>
505
+ </NmTraceCollapsible>
506
+ </NmTrace>
507
+ );
508
+ }
509
+
510
+ /* ----- Tool ----- */
511
+
512
+ const TOOL_TITLE: Record<NmToolState, (name: string) => string> = {
513
+ 'input-streaming': (n) => `Preparing ${n}`,
514
+ 'input-available': (n) => `Running ${n}`,
515
+ 'approval-requested': (n) => `Waiting to run ${n}`,
516
+ 'approval-responded': (n) => `Running ${n}`,
517
+ 'output-available': (n) => `Ran ${n}`,
518
+ 'output-error': (n) => `Failed to run ${n}`,
519
+ 'output-denied': (n) => `Denied ${n}`,
520
+ };
521
+
522
+ export function NmTool(props: {
523
+ name: string;
524
+ state: NmToolState;
525
+ input?: string;
526
+ output?: string;
527
+ error?: string;
528
+ onApprove?: () => void;
529
+ onDeny?: () => void;
530
+ defaultOpen?: boolean;
531
+ }): ReactNode {
532
+ const [sheet, setSheet] = useState(false);
533
+ if (props.state === 'approval-requested') {
534
+ return <NmConfirmation title={props.name} description={props.input} state="requested" onApprove={props.onApprove} onDeny={props.onDeny} />;
535
+ }
536
+ const running = props.state === 'input-streaming' || props.state === 'input-available' || props.state === 'approval-responded';
537
+ const status = props.state === 'output-error' ? 'error' : running ? 'running' : undefined;
538
+ return (
539
+ <>
540
+ <NmTrace ariaLabel={`Tool ${props.name}`}>
541
+ <NmTraceCollapsible defaultOpen={props.defaultOpen}>
542
+ <NmTraceCollapsibleTrigger>
543
+ <NmTraceLine status={status}>
544
+ <NmTraceIcon><NmTraceGlyph kind="command" /></NmTraceIcon>
545
+ <NmTraceTitle shimmer={running}>{TOOL_TITLE[props.state](props.name)}</NmTraceTitle>
546
+ </NmTraceLine>
547
+ </NmTraceCollapsibleTrigger>
548
+ <NmTraceCollapsibleContent>
549
+ <div className={C.stack}>
550
+ {props.input ? <NmOutput label="Input">{props.input}</NmOutput> : null}
551
+ {props.output ? <NmOutput label="Output">{props.output}</NmOutput> : null}
552
+ {props.error ? <NmOutput label="Error">{props.error}</NmOutput> : null}
553
+ {props.output ? <NmButton label="Open output" tone="muted" size="compact" icon="expand" onClick={() => setSheet(true)} /> : null}
554
+ </div>
555
+ </NmTraceCollapsibleContent>
556
+ </NmTraceCollapsible>
557
+ </NmTrace>
558
+ {sheet ? <NmSheet title={`${props.name} output`} description="The full output of the tool call." onClose={() => setSheet(false)}><NmOutput>{props.output ?? ''}</NmOutput></NmSheet> : null}
559
+ </>
560
+ );
561
+ }
562
+
563
+ /* ----- Subagent ----- */
564
+
565
+ export function NmSubagent(props: {
566
+ name: string;
567
+ task: string;
568
+ status: NmAiStatus;
569
+ tools?: readonly { id: string; title: string; status: NmAiStatus }[];
570
+ result?: string;
571
+ defaultOpen?: boolean;
572
+ }): ReactNode {
573
+ return (
574
+ <NmTrace ariaLabel={`Subagent ${props.name}`}>
575
+ <NmTraceCollapsible defaultOpen={props.defaultOpen}>
576
+ <NmTraceCollapsibleTrigger>
577
+ <NmTraceLine status={nmAiTraceStatus(props.status)}>
578
+ <StatusIcon status={props.status} />
579
+ <NmTraceTitle shimmer={props.status === 'running'}>{props.name}</NmTraceTitle>
580
+ <NmTraceBadges><NmTraceBadge>{props.status}</NmTraceBadge></NmTraceBadges>
581
+ </NmTraceLine>
582
+ </NmTraceCollapsibleTrigger>
583
+ <NmTraceCollapsibleContent>
584
+ <div className={C.stack}>
585
+ <NmTraceDescription>{props.task}</NmTraceDescription>
586
+ {props.tools?.map((t) => (
587
+ <NmTraceLine key={t.id} status={nmAiTraceStatus(t.status)}><StatusIcon status={t.status} /><NmTraceTitle>{t.title}</NmTraceTitle></NmTraceLine>
588
+ ))}
589
+ {props.result ? <NmOutput label="Result">{props.result}</NmOutput> : null}
590
+ </div>
591
+ </NmTraceCollapsibleContent>
592
+ </NmTraceCollapsible>
593
+ </NmTrace>
594
+ );
595
+ }
596
+
597
+ /* ----- Suggestion, prompt input, chat ----- */
598
+
599
+ export function NmSuggestions(props: { items: readonly string[]; onPick: (text: string) => void; disabled?: boolean }): ReactNode {
600
+ return (
601
+ <div className={C.suggestions} role="list">
602
+ {props.items.map((text) => (
603
+ <button key={text} type="button" role="listitem" className={C.suggestion} disabled={props.disabled} onClick={() => props.onPick(text)}>{text}</button>
604
+ ))}
605
+ </div>
606
+ );
607
+ }
608
+
609
+ export type NmChatTurn = { id: string; role: 'user' | 'assistant'; text: string; reasoning?: string; tool?: { name: string; state: NmToolState; output?: string } };
610
+
611
+ export function NmChat(props: { turns: readonly NmChatTurn[]; empty?: { title: string; description: string }; streaming?: boolean }): ReactNode {
612
+ if (props.turns.length === 0 && props.empty) {
613
+ return (
614
+ <div className={C.thread}>
615
+ <div className={C.threadEmpty}><NmIcon id="chat-dashed" /><strong>{props.empty.title}</strong><span>{props.empty.description}</span></div>
616
+ </div>
617
+ );
618
+ }
619
+ return (
620
+ <div className={C.thread} role="log" aria-live="polite">
621
+ {props.turns.map((turn) => (
622
+ <NmMessage key={turn.id} role={turn.role}>
623
+ {turn.reasoning ? <NmReasoning text={turn.reasoning} seconds={3} /> : null}
624
+ {turn.tool ? <NmTool name={turn.tool.name} state={turn.tool.state} output={turn.tool.output} /> : null}
625
+ {turn.text}
626
+ </NmMessage>
627
+ ))}
628
+ {props.streaming ? <NmMessageMeta label="Thinking" live /> : null}
629
+ {props.streaming ? <NmTextShimmer>Generating response</NmTextShimmer> : null}
630
+ </div>
631
+ );
632
+ }