@nextclaw/ui 0.12.14 → 0.12.16

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 (92) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/assets/{api-C51456xV.js → api-BIg--UMJ.js} +1 -1
  3. package/dist/assets/{app-manager-provider-D_cKqqRG.js → app-manager-provider-BfKiVYea.js} +1 -1
  4. package/dist/assets/{app-navigation.config-Dve1W20Y.js → app-navigation.config-xIjCAn-R.js} +1 -1
  5. package/dist/assets/{book-open-B4mOKdz8.js → book-open-CUd69I2f.js} +1 -1
  6. package/dist/assets/{channels-list-page-WJ7d4zMI.js → channels-list-page-5wQy-UW7.js} +1 -1
  7. package/dist/assets/chat-CXrERmQ1.js +60 -0
  8. package/dist/assets/{chat-page-DLFTPfmu.js → chat-page-CTLX0KsB.js} +1 -1
  9. package/dist/assets/{chunk-JZWAC4HX-ptDyT_1C.js → chunk-JZWAC4HX-THqEFwu9.js} +1 -1
  10. package/dist/assets/{config-split-page-B3PRA_AV.js → config-split-page-UJSTBsEU.js} +1 -1
  11. package/dist/assets/{createLucideIcon-C_GFKVuW.js → createLucideIcon-DVAlgDOi.js} +1 -1
  12. package/dist/assets/{desktop-update-config-CzGi43xw.js → desktop-update-config-B0vQ5JID.js} +1 -1
  13. package/dist/assets/{dialog-BHcaU6NE.js → dialog-t7OAmObC.js} +1 -1
  14. package/dist/assets/{dist-DtBFqZ6_.js → dist-DWPNydLC.js} +1 -1
  15. package/dist/assets/doc-browser-BIggpN8Z.js +1 -0
  16. package/dist/assets/{doc-browser-CwgI7ipB.js → doc-browser-DznuT-CU.js} +1 -1
  17. package/dist/assets/{doc-browser-context-Dib9sS83.js → doc-browser-context-zXaTjrpA.js} +1 -1
  18. package/dist/assets/{doc-browser-CoKIUCJj.js → doc-browser-t96ibd-b.js} +1 -1
  19. package/dist/assets/{es2015-BlNhrQUG.js → es2015-BRVsmfFO.js} +1 -1
  20. package/dist/assets/{external-link-DP2IJ7AM.js → external-link-D1Xqff6i.js} +1 -1
  21. package/dist/assets/{folder-BPwc278w.js → folder-B_fuaX3x.js} +1 -1
  22. package/dist/assets/{hash-CvcvtMBq.js → hash-BRvv_UUq.js} +1 -1
  23. package/dist/assets/i18n-CF_jgT_-.js +1 -0
  24. package/dist/assets/index-DuHqJ8wn.css +1 -0
  25. package/dist/assets/{index-CAYF44Dz.js → index-mw77my39.js} +2 -2
  26. package/dist/assets/{key-round-BQXmPSxD.js → key-round-DFVNXZcD.js} +1 -1
  27. package/dist/assets/loader-circle-DEz3bHGb.js +1 -0
  28. package/dist/assets/{logo-badge-uB4SwANR.js → logo-badge-CRVKkIl9.js} +1 -1
  29. package/dist/assets/{logos-BcELLmYh.js → logos-BVCi_7_I.js} +1 -1
  30. package/dist/assets/marketplace-page-DVmk8dZk.js +1 -0
  31. package/dist/assets/{marketplace-page-DiqqX25V.js → marketplace-page-apq5LpYx.js} +1 -1
  32. package/dist/assets/mcp-marketplace-page-CskrJuKU.js +1 -0
  33. package/dist/assets/{mcp-marketplace-page-C_akqPwv.js → mcp-marketplace-page-DiqTAdRJ.js} +1 -1
  34. package/dist/assets/message-square-CLhDWybk.js +1 -0
  35. package/dist/assets/{model-config-B0L43HTL.js → model-config-cth12uRn.js} +1 -1
  36. package/dist/assets/{notice-card-C9PFAR67.js → notice-card-CXY09tsa.js} +1 -1
  37. package/dist/assets/play-CnnPm8ca.js +1 -0
  38. package/dist/assets/plus-CdYMdiws.js +1 -0
  39. package/dist/assets/{popover-B8msg2FQ.js → popover-CbQxrchk.js} +1 -1
  40. package/dist/assets/{provider-scoped-model-input-DeAo2Y65.js → provider-scoped-model-input-CZEB2m98.js} +1 -1
  41. package/dist/assets/{providers-list-5_VShcn7.js → providers-list-Dsj2BYPm.js} +1 -1
  42. package/dist/assets/{refresh-ccw-CeG203yU.js → refresh-ccw-C-ytTHiq.js} +1 -1
  43. package/dist/assets/remote-DMMC2PSo.js +1 -0
  44. package/dist/assets/{rotate-cw-F7aThvYj.js → rotate-cw-ClSrRUa0.js} +1 -1
  45. package/dist/assets/{runtime-config-page-B-y_0HIS.js → runtime-config-page-JpAUjHTI.js} +1 -1
  46. package/dist/assets/{save-7ztImRj7.js → save-KxhpE3Zr.js} +1 -1
  47. package/dist/assets/{search-DZSNKEGp.js → search-Bz3Q64sr.js} +1 -1
  48. package/dist/assets/{search-config-DJTm9Fno.js → search-config-C_xRBv_i.js} +1 -1
  49. package/dist/assets/{secrets-config-DKFeFii1.js → secrets-config-BOL024Fj.js} +1 -1
  50. package/dist/assets/{select-DRDejPLk.js → select-tRTLG4FK.js} +1 -1
  51. package/dist/assets/{sessions-config-page-CZGqS32n.js → sessions-config-page-CLEEGNYL.js} +1 -1
  52. package/dist/assets/{setting-row-BcF6eTW0.js → setting-row-DMDgBCC7.js} +1 -1
  53. package/dist/assets/{settings-DjvNMJde.js → settings-Cto6z-Ij.js} +1 -1
  54. package/dist/assets/skeleton-BwfJfVK3.js +1 -0
  55. package/dist/assets/{sparkles-CyDTgTM4.js → sparkles-xZ74eW0P.js} +1 -1
  56. package/dist/assets/{status-dot-aQU9Mia4.js → status-dot-Bobpfutv.js} +1 -1
  57. package/dist/assets/{tabs-custom-C4P7g4vR.js → tabs-custom-C3Mf-NLb.js} +1 -1
  58. package/dist/assets/{tag-chip-CVIqyMv7.js → tag-chip-BZ14i5b1.js} +1 -1
  59. package/dist/assets/{theme-provider-dHqcWU-j.js → theme-provider-D6Cgm6i-.js} +1 -1
  60. package/dist/assets/{tooltip-C6VPreZ7.js → tooltip-7lsLGcL9.js} +1 -1
  61. package/dist/assets/{trash-2-C1cdqL6V.js → trash-2-WFSNa5oj.js} +1 -1
  62. package/dist/assets/{use-config-DFja1sda.js → use-config-CXu7dFzw.js} +1 -1
  63. package/dist/assets/{use-confirm-dialog-DvIbSUX3.js → use-confirm-dialog-Df1SozKw.js} +1 -1
  64. package/dist/assets/{use-infinite-scroll-loader-D8h0k-iL.js → use-infinite-scroll-loader-BogRuzgz.js} +1 -1
  65. package/dist/assets/{use-viewport-layout-D-pjxsyz.js → use-viewport-layout-CWlW5b-T.js} +1 -1
  66. package/dist/assets/x-BvS2y4e_.js +1 -0
  67. package/dist/index.html +39 -39
  68. package/package.json +4 -4
  69. package/src/features/chat/components/chat-sidebar-session-item.tsx +22 -24
  70. package/src/features/chat/components/conversation/chat-input-bar.container.tsx +5 -1
  71. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +29 -0
  72. package/src/features/chat/utils/chat-input-bar.utils.test.ts +26 -0
  73. package/src/features/chat/utils/chat-input-toolbar.utils.ts +19 -18
  74. package/src/features/chat/utils/ncp-chat-input-availability.utils.test.ts +35 -3
  75. package/src/features/chat/utils/ncp-chat-input-availability.utils.ts +4 -6
  76. package/src/index.css +97 -5
  77. package/src/shared/components/common/agent-avatar.test.tsx +33 -0
  78. package/src/shared/components/common/agent-avatar.tsx +6 -9
  79. package/src/shared/lib/i18n/chat.ts +1 -0
  80. package/dist/assets/chat-BxA-mw53.js +0 -58
  81. package/dist/assets/doc-browser-DYKpRqe-.js +0 -1
  82. package/dist/assets/i18n-BnNAQpVM.js +0 -1
  83. package/dist/assets/index-mRmSAB-e.css +0 -1
  84. package/dist/assets/loader-circle-C6gg2m2a.js +0 -1
  85. package/dist/assets/marketplace-page-0sEdt5sA.js +0 -1
  86. package/dist/assets/mcp-marketplace-page-B8vmu9xe.js +0 -1
  87. package/dist/assets/message-square-CLVODA23.js +0 -1
  88. package/dist/assets/play-DeNVUA5C.js +0 -1
  89. package/dist/assets/plus-BptLViq1.js +0 -1
  90. package/dist/assets/remote-pzp4oLcL.js +0 -1
  91. package/dist/assets/skeleton-5Mg6vZHN.js +0 -1
  92. package/dist/assets/x-BjMO7v8q.js +0 -1
package/dist/index.html CHANGED
@@ -78,45 +78,45 @@
78
78
  })();
79
79
  </script>
80
80
  <title>NextClaw</title>
81
- <script type="module" crossorigin src="/assets/index-CAYF44Dz.js"></script>
82
- <link rel="modulepreload" crossorigin href="/assets/i18n-BnNAQpVM.js">
83
- <link rel="modulepreload" crossorigin href="/assets/api-C51456xV.js">
84
- <link rel="modulepreload" crossorigin href="/assets/es2015-BlNhrQUG.js">
85
- <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-C_GFKVuW.js">
86
- <link rel="modulepreload" crossorigin href="/assets/select-DRDejPLk.js">
87
- <link rel="modulepreload" crossorigin href="/assets/dist-DtBFqZ6_.js">
88
- <link rel="modulepreload" crossorigin href="/assets/x-BjMO7v8q.js">
89
- <link rel="modulepreload" crossorigin href="/assets/dialog-BHcaU6NE.js">
90
- <link rel="modulepreload" crossorigin href="/assets/popover-B8msg2FQ.js">
91
- <link rel="modulepreload" crossorigin href="/assets/tooltip-C6VPreZ7.js">
92
- <link rel="modulepreload" crossorigin href="/assets/chunk-JZWAC4HX-ptDyT_1C.js">
93
- <link rel="modulepreload" crossorigin href="/assets/use-config-DFja1sda.js">
94
- <link rel="modulepreload" crossorigin href="/assets/theme-provider-dHqcWU-j.js">
95
- <link rel="modulepreload" crossorigin href="/assets/search-DZSNKEGp.js">
96
- <link rel="modulepreload" crossorigin href="/assets/book-open-B4mOKdz8.js">
97
- <link rel="modulepreload" crossorigin href="/assets/external-link-DP2IJ7AM.js">
98
- <link rel="modulepreload" crossorigin href="/assets/folder-BPwc278w.js">
99
- <link rel="modulepreload" crossorigin href="/assets/logos-BcELLmYh.js">
100
- <link rel="modulepreload" crossorigin href="/assets/loader-circle-C6gg2m2a.js">
101
- <link rel="modulepreload" crossorigin href="/assets/plus-BptLViq1.js">
102
- <link rel="modulepreload" crossorigin href="/assets/refresh-ccw-CeG203yU.js">
103
- <link rel="modulepreload" crossorigin href="/assets/settings-DjvNMJde.js">
104
- <link rel="modulepreload" crossorigin href="/assets/sparkles-CyDTgTM4.js">
105
- <link rel="modulepreload" crossorigin href="/assets/trash-2-C1cdqL6V.js">
106
- <link rel="modulepreload" crossorigin href="/assets/doc-browser-context-Dib9sS83.js">
107
- <link rel="modulepreload" crossorigin href="/assets/doc-browser-CoKIUCJj.js">
108
- <link rel="modulepreload" crossorigin href="/assets/doc-browser-CwgI7ipB.js">
109
- <link rel="modulepreload" crossorigin href="/assets/use-viewport-layout-D-pjxsyz.js">
110
- <link rel="modulepreload" crossorigin href="/assets/logo-badge-uB4SwANR.js">
111
- <link rel="modulepreload" crossorigin href="/assets/skeleton-5Mg6vZHN.js">
112
- <link rel="modulepreload" crossorigin href="/assets/chat-BxA-mw53.js">
113
- <link rel="modulepreload" crossorigin href="/assets/key-round-BQXmPSxD.js">
114
- <link rel="modulepreload" crossorigin href="/assets/message-square-CLVODA23.js">
115
- <link rel="modulepreload" crossorigin href="/assets/app-navigation.config-Dve1W20Y.js">
116
- <link rel="modulepreload" crossorigin href="/assets/notice-card-C9PFAR67.js">
117
- <link rel="modulepreload" crossorigin href="/assets/status-dot-aQU9Mia4.js">
118
- <link rel="modulepreload" crossorigin href="/assets/app-manager-provider-D_cKqqRG.js">
119
- <link rel="stylesheet" crossorigin href="/assets/index-mRmSAB-e.css">
81
+ <script type="module" crossorigin src="/assets/index-mw77my39.js"></script>
82
+ <link rel="modulepreload" crossorigin href="/assets/i18n-CF_jgT_-.js">
83
+ <link rel="modulepreload" crossorigin href="/assets/api-BIg--UMJ.js">
84
+ <link rel="modulepreload" crossorigin href="/assets/es2015-BRVsmfFO.js">
85
+ <link rel="modulepreload" crossorigin href="/assets/createLucideIcon-DVAlgDOi.js">
86
+ <link rel="modulepreload" crossorigin href="/assets/select-tRTLG4FK.js">
87
+ <link rel="modulepreload" crossorigin href="/assets/dist-DWPNydLC.js">
88
+ <link rel="modulepreload" crossorigin href="/assets/x-BvS2y4e_.js">
89
+ <link rel="modulepreload" crossorigin href="/assets/dialog-t7OAmObC.js">
90
+ <link rel="modulepreload" crossorigin href="/assets/popover-CbQxrchk.js">
91
+ <link rel="modulepreload" crossorigin href="/assets/tooltip-7lsLGcL9.js">
92
+ <link rel="modulepreload" crossorigin href="/assets/chunk-JZWAC4HX-THqEFwu9.js">
93
+ <link rel="modulepreload" crossorigin href="/assets/use-config-CXu7dFzw.js">
94
+ <link rel="modulepreload" crossorigin href="/assets/theme-provider-D6Cgm6i-.js">
95
+ <link rel="modulepreload" crossorigin href="/assets/search-Bz3Q64sr.js">
96
+ <link rel="modulepreload" crossorigin href="/assets/book-open-CUd69I2f.js">
97
+ <link rel="modulepreload" crossorigin href="/assets/external-link-D1Xqff6i.js">
98
+ <link rel="modulepreload" crossorigin href="/assets/folder-B_fuaX3x.js">
99
+ <link rel="modulepreload" crossorigin href="/assets/logos-BVCi_7_I.js">
100
+ <link rel="modulepreload" crossorigin href="/assets/loader-circle-DEz3bHGb.js">
101
+ <link rel="modulepreload" crossorigin href="/assets/plus-CdYMdiws.js">
102
+ <link rel="modulepreload" crossorigin href="/assets/refresh-ccw-C-ytTHiq.js">
103
+ <link rel="modulepreload" crossorigin href="/assets/settings-Cto6z-Ij.js">
104
+ <link rel="modulepreload" crossorigin href="/assets/sparkles-xZ74eW0P.js">
105
+ <link rel="modulepreload" crossorigin href="/assets/trash-2-WFSNa5oj.js">
106
+ <link rel="modulepreload" crossorigin href="/assets/doc-browser-context-zXaTjrpA.js">
107
+ <link rel="modulepreload" crossorigin href="/assets/doc-browser-t96ibd-b.js">
108
+ <link rel="modulepreload" crossorigin href="/assets/doc-browser-DznuT-CU.js">
109
+ <link rel="modulepreload" crossorigin href="/assets/use-viewport-layout-CWlW5b-T.js">
110
+ <link rel="modulepreload" crossorigin href="/assets/logo-badge-CRVKkIl9.js">
111
+ <link rel="modulepreload" crossorigin href="/assets/skeleton-BwfJfVK3.js">
112
+ <link rel="modulepreload" crossorigin href="/assets/chat-CXrERmQ1.js">
113
+ <link rel="modulepreload" crossorigin href="/assets/key-round-DFVNXZcD.js">
114
+ <link rel="modulepreload" crossorigin href="/assets/message-square-CLhDWybk.js">
115
+ <link rel="modulepreload" crossorigin href="/assets/app-navigation.config-xIjCAn-R.js">
116
+ <link rel="modulepreload" crossorigin href="/assets/notice-card-CXY09tsa.js">
117
+ <link rel="modulepreload" crossorigin href="/assets/status-dot-Bobpfutv.js">
118
+ <link rel="modulepreload" crossorigin href="/assets/app-manager-provider-BfKiVYea.js">
119
+ <link rel="stylesheet" crossorigin href="/assets/index-DuHqJ8wn.css">
120
120
  </head>
121
121
 
122
122
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.12.14",
3
+ "version": "0.12.16",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,11 +28,11 @@
28
28
  "tailwind-merge": "^2.5.4",
29
29
  "zod": "^3.23.8",
30
30
  "zustand": "^5.0.2",
31
- "@nextclaw/agent-chat": "0.1.10",
32
- "@nextclaw/agent-chat-ui": "0.3.9",
31
+ "@nextclaw/agent-chat-ui": "0.3.11",
33
32
  "@nextclaw/ncp-http-agent-client": "0.3.17",
34
33
  "@nextclaw/ncp-react": "0.4.25",
35
- "@nextclaw/ncp": "0.5.5"
34
+ "@nextclaw/ncp": "0.5.5",
35
+ "@nextclaw/agent-chat": "0.1.10"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@testing-library/react": "^16.3.0",
@@ -7,7 +7,7 @@ import { Input } from '@/shared/components/ui/input';
7
7
  import { type SessionContextView } from '@/features/chat/utils/session-context.utils';
8
8
  import type { SessionRunStatus } from '@/features/chat/types/session-run-status.types';
9
9
  import { cn } from '@/shared/lib/utils';
10
- import { formatDateTime, t } from '@/shared/lib/i18n';
10
+ import { formatDateShort, t } from '@/shared/lib/i18n';
11
11
  import { Check, GitBranch, Pencil, X } from 'lucide-react';
12
12
 
13
13
  type ChatSidebarSessionItemProps = {
@@ -115,20 +115,16 @@ function ChatSidebarSessionDisplayView({
115
115
  onOpenChildSessions,
116
116
  onStartEditing
117
117
  }: ChatSidebarSessionDisplayViewProps) {
118
- const iconTone = active ? 'text-gray-700' : 'text-gray-500';
119
- const normalizedAgentId = agentId?.trim() ?? '';
120
- const shouldShowAgentAvatar = Boolean(
121
- normalizedAgentId && normalizedAgentId.toLowerCase() !== 'main',
122
- );
118
+ const trailingControlsClassName = childSessionCount > 0 && onOpenChildSessions ? 'pr-16' : 'pr-8';
123
119
 
124
120
  return (
125
121
  <div className="group/session relative">
126
122
  <button type="button" onClick={onSelect} className="w-full text-left">
127
- <div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-1.5 pr-8">
123
+ <div className={cn('grid grid-cols-[minmax(0,1fr)_auto] items-start gap-2', trailingControlsClassName)}>
128
124
  <span className="flex min-w-0 items-center gap-1.5">
129
- {shouldShowAgentAvatar ? (
125
+ {agentId?.trim() && agentId.trim().toLowerCase() !== 'main' ? (
130
126
  <AgentAvatar
131
- agentId={normalizedAgentId}
127
+ agentId={agentId}
132
128
  displayName={agentLabel}
133
129
  avatarUrl={agentAvatarUrl}
134
130
  className="h-5 w-5 shrink-0"
@@ -149,26 +145,28 @@ function ChatSidebarSessionDisplayView({
149
145
  ) : null}
150
146
  {context.icon ? (
151
147
  <span className="inline-flex h-[1.125rem] w-[1.125rem] shrink-0 items-center justify-center">
152
- <SessionContextIconNode icon={context.icon} className={iconTone} />
148
+ <SessionContextIconNode icon={context.icon} className={active ? 'text-gray-700' : 'text-gray-500'} />
153
149
  </span>
154
150
  ) : null}
155
151
  </span>
156
- <span className="inline-flex shrink-0 items-center justify-end gap-1.5">
157
- {showUnreadDot ? (
158
- <span
159
- aria-label={t('chatSessionUnread')}
160
- className="h-2 w-2 rounded-full bg-primary"
161
- />
162
- ) : null}
163
- <span className="inline-flex h-3.5 w-3.5 items-center justify-center">
164
- {runStatus ? <SessionRunBadge status={runStatus} /> : null}
152
+ {runStatus ? (
153
+ <span className="inline-flex shrink-0 items-center justify-end gap-1.5 pt-0.5">
154
+ <SessionRunBadge status={runStatus} />
165
155
  </span>
166
- </span>
156
+ ) : null}
167
157
  </div>
168
- <div className="mt-0.5 text-[11px] text-gray-400 truncate">
169
- <span>
170
- {agentLabel?.trim() ? `${agentLabel} · ` : ''}{session.messageCount} · {formatDateTime(session.updatedAt)}
158
+ <div className="mt-1 flex items-center gap-2 text-[11px] text-gray-400">
159
+ <span className="min-w-0 truncate">
160
+ {agentLabel?.trim() ? `${agentLabel} · ` : ''}{session.messageCount}
171
161
  </span>
162
+ {showUnreadDot ? (
163
+ <span
164
+ aria-label={t('chatSessionUnread')}
165
+ className="ml-auto h-2 w-2 shrink-0 rounded-full bg-primary"
166
+ />
167
+ ) : (
168
+ <span className="ml-auto shrink-0">{formatDateShort(session.updatedAt)}</span>
169
+ )}
172
170
  </div>
173
171
  </button>
174
172
  {childSessionCount > 0 && onOpenChildSessions ? (
@@ -235,7 +233,7 @@ export function ChatSidebarSessionItem({
235
233
  return (
236
234
  <div
237
235
  className={cn(
238
- 'w-full rounded-xl px-3 py-2 text-left transition-all text-[13px]',
236
+ 'w-full rounded-xl px-3 py-2.5 text-left transition-all text-[13px]',
239
237
  active
240
238
  ? 'bg-gray-200 text-gray-900 font-semibold shadow-sm'
241
239
  : 'text-gray-700 hover:bg-gray-200/60 hover:text-gray-900'
@@ -6,6 +6,7 @@ import type { SessionSkillEntryView } from '@/shared/lib/api';
6
6
  import { buildChatSlashItems, buildModelStateHint, buildModelToolbarSelect, buildSkillPickerModel, buildThinkingToolbarSelect, type ChatModelRecord, type ChatSkillRecord, type ChatThinkingLevel } from '@/features/chat/utils/chat-input-bar.utils';
7
7
  import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
8
8
  import { useI18n } from '@/app/components/i18n-provider';
9
+ import { useViewportLayout } from '@/app/hooks/use-viewport-layout';
9
10
  import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
10
11
  import { chatRecentModelsManager, CHAT_RECENT_MODELS_MIN_OPTIONS } from '@/features/chat/managers/chat-recent-models.manager';
11
12
  import { chatRecentSkillsManager, CHAT_RECENT_SKILLS_MIN_OPTIONS } from '@/features/chat/managers/chat-recent-skills.manager';
@@ -203,6 +204,7 @@ function buildSkillPicker(params: { allSkillsLabel: string; presenter: ChatPrese
203
204
  export function ChatInputBarContainer() {
204
205
  const presenter = usePresenter();
205
206
  const { language } = useI18n();
207
+ const { isMobile } = useViewportLayout();
206
208
  const snapshot = useChatInputStore((state) => state.snapshot);
207
209
  const runtimeAvailability = useChatRuntimeAvailability();
208
210
  const [slashQuery, setSlashQuery] = useState<string | null>(null);
@@ -215,7 +217,9 @@ export function ChatInputBarContainer() {
215
217
  const isModelOptionsEmpty = isNcpChatModelOptionsEmpty(snapshot);
216
218
  const inputDisabled = isNcpChatComposerDisabled(snapshot);
217
219
  const attachmentSupported = typeof presenter.chatInputManager.addAttachments === 'function';
218
- const textareaPlaceholder = isModelOptionsEmpty ? t('chatModelNoOptions') : t('chatInputPlaceholder');
220
+ const textareaPlaceholder = isModelOptionsEmpty
221
+ ? t('chatModelNoOptions')
222
+ : t(isMobile ? 'chatInputPlaceholderCompact' : 'chatInputPlaceholder');
219
223
  const slashItems = useMemo(
220
224
  () => buildChatSlashItems(skillRecords, slashQuery ?? '', labels.slashTexts, recentSkillValues),
221
225
  [labels.slashTexts, recentSkillValues, skillRecords, slashQuery]
@@ -146,4 +146,33 @@ describe('NcpChatInputManager', () => {
146
146
  expect(streamActionsManager.sendMessage).not.toHaveBeenCalled();
147
147
  expect(sessionListManager.promoteRootDraftSessionRoute).not.toHaveBeenCalled();
148
148
  });
149
+
150
+ it('still attempts to send when provider metadata is stale or the session type is marked unavailable', async () => {
151
+ useChatInputStore.setState({
152
+ snapshot: {
153
+ ...useChatInputStore.getState().snapshot,
154
+ isProviderStateResolved: true,
155
+ modelOptions: [],
156
+ sessionTypeUnavailable: true,
157
+ },
158
+ });
159
+ const streamActionsManager = {
160
+ sendMessage: vi.fn().mockResolvedValue(undefined),
161
+ stopCurrentRun: vi.fn().mockResolvedValue(undefined),
162
+ } as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
163
+ const sessionListManager = {
164
+ ensureDraftSession: vi.fn(() => 'draft-session'),
165
+ promoteRootDraftSessionRoute: vi.fn(),
166
+ } as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
167
+ const manager = new NcpChatInputManager(
168
+ {} as ConstructorParameters<typeof NcpChatInputManager>[0],
169
+ streamActionsManager,
170
+ sessionListManager,
171
+ );
172
+
173
+ await manager.send();
174
+
175
+ expect(streamActionsManager.sendMessage).toHaveBeenCalledTimes(1);
176
+ expect(sessionListManager.promoteRootDraftSessionRoute).toHaveBeenCalledWith('current-route-session');
177
+ });
149
178
  });
@@ -219,6 +219,32 @@ describe('buildModelToolbarSelect', () => {
219
219
  });
220
220
  });
221
221
 
222
+ it('keeps the full provider/model label in shared state while exposing a compact mobile label', () => {
223
+ const select = buildModelToolbarSelect({
224
+ modelOptions: [
225
+ {
226
+ value: 'anthropic/claude-sonnet-4-very-long-name',
227
+ modelLabel: 'claude-sonnet-4-very-long-name',
228
+ providerLabel: 'Anthropic'
229
+ }
230
+ ],
231
+ recentModelValues: [],
232
+ selectedModel: 'anthropic/claude-sonnet-4-very-long-name',
233
+ isModelOptionsLoading: false,
234
+ hasModelOptions: true,
235
+ onValueChange: vi.fn(),
236
+ texts: {
237
+ modelSelectPlaceholder: 'Select model',
238
+ modelNoOptionsLabel: 'No models',
239
+ recentModelsLabel: 'Recent',
240
+ allModelsLabel: 'All models'
241
+ }
242
+ });
243
+
244
+ expect(select.selectedLabel).toBe('Anthropic/claude-sonnet-4-very-long-name');
245
+ expect(select.options[0]?.label).toBe('Anthropic/claude-sonnet-4-very-long-name');
246
+ });
247
+
222
248
  it('groups recent models ahead of the remaining catalog', () => {
223
249
  const select = buildModelToolbarSelect({
224
250
  modelOptions: [
@@ -52,7 +52,9 @@ export function buildModelStateHint(params: {
52
52
  };
53
53
  }
54
54
 
55
- export function buildModelToolbarSelect(params: {
55
+ export function buildModelToolbarSelect({
56
+ modelOptions, recentModelValues, selectedModel, isModelOptionsLoading, hasModelOptions, onValueChange, texts,
57
+ }: {
56
58
  modelOptions: ChatModelRecord[];
57
59
  recentModelValues?: string[];
58
60
  selectedModel: string;
@@ -67,22 +69,21 @@ export function buildModelToolbarSelect(params: {
67
69
  | "allModelsLabel"
68
70
  >;
69
71
  }): ChatToolbarSelect {
70
- const selectedModelOption = params.modelOptions.find(
71
- (option) => option.value === params.selectedModel,
72
+ const selectedModelOption = modelOptions.find(
73
+ (option) => option.value === selectedModel,
72
74
  );
73
- const fallbackModelOption = params.modelOptions[0];
74
- const resolvedModelOption = selectedModelOption ?? fallbackModelOption;
75
- const resolvedValue = params.hasModelOptions
75
+ const resolvedModelOption = selectedModelOption ?? modelOptions[0];
76
+ const resolvedValue = hasModelOptions
76
77
  ? resolvedModelOption?.value
77
78
  : undefined;
78
- const recentValueSet = new Set(params.recentModelValues ?? []);
79
+ const recentValueSet = new Set(recentModelValues ?? []);
79
80
  const modelOptionMap = new Map(
80
- params.modelOptions.map((option) => [option.value, option] as const),
81
+ modelOptions.map((option) => [option.value, option] as const),
81
82
  );
82
- const recentOptions = (params.recentModelValues ?? [])
83
+ const recentOptions = (recentModelValues ?? [])
83
84
  .map((value) => modelOptionMap.get(value))
84
85
  .filter((option): option is ChatModelRecord => Boolean(option));
85
- const remainingOptions = params.modelOptions.filter(
86
+ const remainingOptions = modelOptions.filter(
86
87
  (option) => !recentValueSet.has(option.value),
87
88
  );
88
89
  const optionGroups =
@@ -90,7 +91,7 @@ export function buildModelToolbarSelect(params: {
90
91
  ? [
91
92
  {
92
93
  key: "recent-models",
93
- label: params.texts.recentModelsLabel,
94
+ label: texts.recentModelsLabel,
94
95
  options: recentOptions.map((option) => ({
95
96
  value: option.value,
96
97
  label: formatModelOptionLabel(option),
@@ -98,7 +99,7 @@ export function buildModelToolbarSelect(params: {
98
99
  },
99
100
  {
100
101
  key: "all-models",
101
- label: params.texts.allModelsLabel,
102
+ label: texts.allModelsLabel,
102
103
  options: remainingOptions.map((option) => ({
103
104
  value: option.value,
104
105
  label: formatModelOptionLabel(option),
@@ -110,20 +111,20 @@ export function buildModelToolbarSelect(params: {
110
111
  return {
111
112
  key: "model",
112
113
  value: resolvedValue,
113
- placeholder: params.texts.modelSelectPlaceholder,
114
+ placeholder: texts.modelSelectPlaceholder,
114
115
  selectedLabel: resolvedModelOption
115
116
  ? formatModelOptionLabel(resolvedModelOption)
116
117
  : undefined,
117
118
  icon: "sparkles",
118
- options: params.modelOptions.map((option) => ({
119
+ options: modelOptions.map((option) => ({
119
120
  value: option.value,
120
121
  label: formatModelOptionLabel(option),
121
122
  })),
122
123
  groups: optionGroups,
123
- disabled: !params.hasModelOptions,
124
- loading: params.isModelOptionsLoading,
125
- emptyLabel: params.texts.modelNoOptionsLabel,
126
- onValueChange: params.onValueChange,
124
+ disabled: !hasModelOptions,
125
+ loading: isModelOptionsLoading,
126
+ emptyLabel: texts.modelNoOptionsLabel,
127
+ onValueChange,
127
128
  };
128
129
  }
129
130
 
@@ -41,7 +41,7 @@ function createSnapshot(
41
41
  }
42
42
 
43
43
  describe('ncp-chat-input-availability.utils', () => {
44
- it('keeps the composer editable during cold start while send remains blocked', () => {
44
+ it('keeps the composer editable during cold start while runtime blocking still prevents send', () => {
45
45
  const snapshot = createSnapshot({
46
46
  isProviderStateResolved: false,
47
47
  modelOptions: [],
@@ -60,6 +60,22 @@ describe('ncp-chat-input-availability.utils', () => {
60
60
  ).toBe(true);
61
61
  });
62
62
 
63
+ it('does not block send only because model options have not loaded yet', () => {
64
+ const snapshot = createSnapshot({
65
+ isProviderStateResolved: false,
66
+ modelOptions: [],
67
+ sessionTypeUnavailable: false,
68
+ });
69
+
70
+ expect(
71
+ isNcpChatSendDisabled({
72
+ snapshot,
73
+ hasSendableDraft: true,
74
+ isRuntimeBlocked: false,
75
+ })
76
+ ).toBe(false);
77
+ });
78
+
63
79
  it('marks model options as empty only after provider state resolves', () => {
64
80
  const loadingSnapshot = createSnapshot({
65
81
  isProviderStateResolved: false,
@@ -74,19 +90,35 @@ describe('ncp-chat-input-availability.utils', () => {
74
90
  expect(isNcpChatModelOptionsEmpty(emptySnapshot)).toBe(true);
75
91
  });
76
92
 
77
- it('disables both editing and sending when the session type is unavailable', () => {
93
+ it('keeps editing and sending available when the selected session type reports unavailable', () => {
78
94
  const snapshot = createSnapshot({
79
95
  isProviderStateResolved: true,
80
96
  sessionTypeUnavailable: true,
81
97
  });
82
98
 
83
- expect(isNcpChatComposerDisabled(snapshot)).toBe(true);
99
+ expect(isNcpChatComposerDisabled(snapshot)).toBe(false);
84
100
  expect(
85
101
  isNcpChatSendDisabled({
86
102
  snapshot,
87
103
  hasSendableDraft: true,
88
104
  isRuntimeBlocked: false,
89
105
  })
106
+ ).toBe(false);
107
+ });
108
+
109
+ it('blocks send when there is no sendable draft', () => {
110
+ const snapshot = createSnapshot({
111
+ isProviderStateResolved: true,
112
+ modelOptions: [],
113
+ sessionTypeUnavailable: true,
114
+ });
115
+
116
+ expect(
117
+ isNcpChatSendDisabled({
118
+ snapshot,
119
+ hasSendableDraft: false,
120
+ isRuntimeBlocked: false,
121
+ })
90
122
  ).toBe(true);
91
123
  });
92
124
  });
@@ -24,9 +24,9 @@ export function isNcpChatModelOptionsEmpty(
24
24
  }
25
25
 
26
26
  export function isNcpChatComposerDisabled(
27
- snapshot: NcpChatInputAvailabilitySnapshot
27
+ _snapshot: NcpChatInputAvailabilitySnapshot
28
28
  ): boolean {
29
- return snapshot.sessionTypeUnavailable;
29
+ return false;
30
30
  }
31
31
 
32
32
  export function isNcpChatSendDisabled(params: {
@@ -34,12 +34,10 @@ export function isNcpChatSendDisabled(params: {
34
34
  snapshot: NcpChatInputAvailabilitySnapshot;
35
35
  isRuntimeBlocked: boolean;
36
36
  }): boolean {
37
- const { hasSendableDraft, isRuntimeBlocked, snapshot } = params;
37
+ const { hasSendableDraft, isRuntimeBlocked } = params;
38
38
 
39
39
  return (
40
40
  isRuntimeBlocked ||
41
- !hasSendableDraft ||
42
- !hasNcpChatModelOptions(snapshot) ||
43
- snapshot.sessionTypeUnavailable
41
+ !hasSendableDraft
44
42
  );
45
43
  }
package/src/index.css CHANGED
@@ -220,6 +220,16 @@
220
220
  --md-code-toolbar-bg: rgba(15, 23, 42, 0.56);
221
221
  --md-code-toolbar-border: rgba(148, 163, 184, 0.3);
222
222
  --md-code-text: #e5e7eb;
223
+ --md-code-muted: #9ca3af;
224
+ --md-code-keyword: #c084fc;
225
+ --md-code-title: #93c5fd;
226
+ --md-code-string: #86efac;
227
+ --md-code-number: #fbbf24;
228
+ --md-code-comment: #94a3b8;
229
+ --md-code-attr: #67e8f9;
230
+ --md-code-meta: #f0abfc;
231
+ --md-code-deletion: #fca5a5;
232
+ --md-code-addition: #86efac;
223
233
  --md-table-head-bg: rgba(148, 163, 184, 0.14);
224
234
 
225
235
  color: var(--md-text);
@@ -241,6 +251,16 @@
241
251
  --md-code-toolbar-bg: rgba(2, 6, 23, 0.88);
242
252
  --md-code-toolbar-border: rgba(191, 219, 254, 0.26);
243
253
  --md-code-text: #e2e8f0;
254
+ --md-code-muted: #a5b4fc;
255
+ --md-code-keyword: #d8b4fe;
256
+ --md-code-title: #bfdbfe;
257
+ --md-code-string: #bbf7d0;
258
+ --md-code-number: #fde68a;
259
+ --md-code-comment: #cbd5e1;
260
+ --md-code-attr: #a5f3fc;
261
+ --md-code-meta: #f5d0fe;
262
+ --md-code-deletion: #fecaca;
263
+ --md-code-addition: #bbf7d0;
244
264
  --md-table-head-bg: rgba(191, 219, 254, 0.16);
245
265
  }
246
266
 
@@ -394,9 +414,12 @@
394
414
  .chat-codeblock {
395
415
  margin: 0.2rem 0 0.1rem;
396
416
  border: 1px solid var(--md-code-toolbar-border);
397
- border-radius: 0.78rem;
417
+ border-radius: 0.5rem;
398
418
  overflow: hidden;
399
- background: var(--md-code-surface);
419
+ background:
420
+ linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0)),
421
+ var(--md-code-surface);
422
+ box-shadow: 0 12px 34px rgba(15, 23, 42, 0.13);
400
423
  }
401
424
 
402
425
  .chat-codeblock-toolbar {
@@ -426,7 +449,7 @@
426
449
  font-weight: 600;
427
450
  color: var(--md-code-text);
428
451
  border: 1px solid var(--md-code-toolbar-border);
429
- border-radius: 0.48rem;
452
+ border-radius: 0.42rem;
430
453
  padding: 0.18rem 0.36rem;
431
454
  background: transparent;
432
455
  cursor: pointer;
@@ -439,17 +462,86 @@
439
462
 
440
463
  .chat-codeblock pre {
441
464
  margin: 0;
442
- padding: 0.75rem 0.82rem 0.8rem;
465
+ padding: 0.8rem 0.9rem 0.86rem;
443
466
  overflow-x: auto;
444
467
  background: transparent;
468
+ scrollbar-color: rgba(148, 163, 184, 0.48) transparent;
469
+ scrollbar-width: thin;
445
470
  }
446
471
 
447
472
  .chat-codeblock code {
448
473
  display: block;
449
474
  min-width: max-content;
450
475
  white-space: pre;
451
- line-height: 1.58;
476
+ line-height: 1.62;
452
477
  color: var(--md-code-text);
478
+ font-size: 0.79rem;
479
+ font-variant-ligatures: none;
480
+ tab-size: 2;
481
+ }
482
+
483
+ .chat-codeblock .hljs-keyword,
484
+ .chat-codeblock .hljs-built_in,
485
+ .chat-codeblock .hljs-type,
486
+ .chat-codeblock .hljs-literal,
487
+ .chat-codeblock .hljs-selector-tag {
488
+ color: var(--md-code-keyword);
489
+ }
490
+
491
+ .chat-codeblock .hljs-title,
492
+ .chat-codeblock .hljs-title.function_,
493
+ .chat-codeblock .hljs-title.class_,
494
+ .chat-codeblock .hljs-section,
495
+ .chat-codeblock .hljs-selector-id {
496
+ color: var(--md-code-title);
497
+ }
498
+
499
+ .chat-codeblock .hljs-string,
500
+ .chat-codeblock .hljs-regexp,
501
+ .chat-codeblock .hljs-symbol,
502
+ .chat-codeblock .hljs-bullet {
503
+ color: var(--md-code-string);
504
+ }
505
+
506
+ .chat-codeblock .hljs-number,
507
+ .chat-codeblock .hljs-variable,
508
+ .chat-codeblock .hljs-template-variable,
509
+ .chat-codeblock .hljs-params {
510
+ color: var(--md-code-number);
511
+ }
512
+
513
+ .chat-codeblock .hljs-comment,
514
+ .chat-codeblock .hljs-quote {
515
+ color: var(--md-code-comment);
516
+ font-style: italic;
517
+ }
518
+
519
+ .chat-codeblock .hljs-attr,
520
+ .chat-codeblock .hljs-attribute,
521
+ .chat-codeblock .hljs-property,
522
+ .chat-codeblock .hljs-name,
523
+ .chat-codeblock .hljs-selector-class {
524
+ color: var(--md-code-attr);
525
+ }
526
+
527
+ .chat-codeblock .hljs-meta,
528
+ .chat-codeblock .hljs-doctag,
529
+ .chat-codeblock .hljs-tag {
530
+ color: var(--md-code-meta);
531
+ }
532
+
533
+ .chat-codeblock .hljs-subst,
534
+ .chat-codeblock .hljs-punctuation,
535
+ .chat-codeblock .hljs-operator {
536
+ color: var(--md-code-muted);
537
+ }
538
+
539
+ .chat-codeblock .hljs-deletion {
540
+ color: var(--md-code-deletion);
541
+ }
542
+
543
+ .chat-codeblock .hljs-addition {
544
+ color: var(--md-code-addition);
453
545
  }
454
546
 
455
547
  .chat-table-wrap {
@@ -0,0 +1,33 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { AgentAvatar } from './agent-avatar';
4
+
5
+ describe('AgentAvatar', () => {
6
+ it('uses the configured image for the main agent before fallback', () => {
7
+ render(
8
+ <AgentAvatar
9
+ agentId="main"
10
+ displayName="Main"
11
+ avatarUrl="https://example.com/main-avatar.png"
12
+ />,
13
+ );
14
+
15
+ const avatar = screen.getByRole('img', { name: 'Main' });
16
+ expect(avatar.getAttribute('src')).toBe('https://example.com/main-avatar.png');
17
+ expect(avatar.querySelector('svg')).toBeNull();
18
+ });
19
+
20
+ it('uses the bot icon for the main agent fallback', () => {
21
+ render(<AgentAvatar agentId="main" displayName="Main" />);
22
+
23
+ const avatar = screen.getByLabelText('Main');
24
+ expect(avatar.querySelector('svg')).toBeTruthy();
25
+ expect(avatar.textContent).not.toContain('M');
26
+ });
27
+
28
+ it('keeps letter fallback avatars for specialist agents', () => {
29
+ render(<AgentAvatar agentId="engineer" displayName="Engineer" />);
30
+
31
+ expect(screen.getByLabelText('Engineer').textContent).toBe('E');
32
+ });
33
+ });