@nextclaw/ui 0.12.9 → 0.12.11
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.
- package/CHANGELOG.md +102 -0
- package/dist/assets/ChannelsList-SQ7Oxotv.js +8 -0
- package/dist/assets/DocBrowser-BCO2k6XD.js +1 -0
- package/dist/assets/{DocBrowser-6ReNjvzF.js → DocBrowser-rDOjI3ga.js} +1 -1
- package/dist/assets/{DocBrowserContext-B6SpA7Qs.js → DocBrowserContext-BUq3Wo8O.js} +1 -1
- package/dist/assets/{LogoBadge-ByNLYg65.js → LogoBadge-DP8Ye7wJ.js} +1 -1
- package/dist/assets/ModelConfig-C77Ae9ru.js +1 -0
- package/dist/assets/ProviderScopedModelInput-CEnK61uo.js +1 -0
- package/dist/assets/ProvidersList-BCupBayq.js +1 -0
- package/dist/assets/RuntimeConfig-Ad-CAcmy.js +1 -0
- package/dist/assets/SearchConfig-BfCz4wJ4.js +1 -0
- package/dist/assets/SecretsConfig-DjmBIhyy.js +3 -0
- package/dist/assets/{SessionsConfig-ChHQ7M5c.js → SessionsConfig-CvjxU40H.js} +2 -2
- package/dist/assets/{book-open-BdcxxoQu.js → book-open-BE8M56IM.js} +1 -1
- package/dist/assets/chat-page-JKC6ln-y.js +58 -0
- package/dist/assets/chat-session-display-YcRMrAMa.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-DK5HPmIK.js → chunk-JZWAC4HX-erTUn3b8.js} +1 -1
- package/dist/assets/client-CszWMVKi.js +7 -0
- package/dist/assets/config-split-page-BAGSzUR3.js +1 -0
- package/dist/assets/{createLucideIcon-BSeTgkZW.js → createLucideIcon-CCiTGX8L.js} +1 -1
- package/dist/assets/desktop-DfkLlkG2.js +1 -0
- package/dist/assets/desktop-update-config-BXeGlqHD.js +1 -0
- package/dist/assets/dialog-BghZFPch.js +5 -0
- package/dist/assets/{dist-6TrrnPCR.js → dist-Dd9cr-kz.js} +1 -1
- package/dist/assets/dist-ZwoAXs46.js +9 -0
- package/dist/assets/{download-BhDxnyvU.js → download-D7LOizcW.js} +1 -1
- package/dist/assets/es2015-CEAreese.js +41 -0
- package/dist/assets/{external-link-BgErLCNT.js → external-link-qsnCMhw1.js} +1 -1
- package/dist/assets/{hash-Bl7dr_UG.js → hash-0zjWsNl-.js} +1 -1
- package/dist/assets/{i18n-eDHeDY0n.js → i18n-DvzXOGQX.js} +1 -1
- package/dist/assets/index-DvVTC9FF.css +1 -0
- package/dist/assets/index-lr6rQUSd.js +2 -0
- package/dist/assets/key-round-BLe9D8ND.js +1 -0
- package/dist/assets/loader-circle-wj7kARHv.js +1 -0
- package/dist/assets/{logos-x89HbrZ4.js → logos-_v5b2SdG.js} +1 -1
- package/dist/assets/marketplace-page-CAAk1Khc.js +1 -0
- package/dist/assets/marketplace-page-CfCiq90S.js +49 -0
- package/dist/assets/mcp-marketplace-page-D0Pp9Hs-.js +40 -0
- package/dist/assets/play-o6NmwGTi.js +1 -0
- package/dist/assets/plus-I9pBS4Fl.js +1 -0
- package/dist/assets/{refresh-cw-C47QSEwg.js → refresh-cw-MNqgR3LZ.js} +1 -1
- package/dist/assets/remote-C9fXm4V5.js +1 -0
- package/dist/assets/{save-3S6-H3Xw.js → save-D4bObrmH.js} +1 -1
- package/dist/assets/search-DxmL3IWE.js +1 -0
- package/dist/assets/security-config-BUm6FFfl.js +1 -0
- package/dist/assets/select-BILPf7zs.js +1 -0
- package/dist/assets/setting-row-BATDgg4r.js +1 -0
- package/dist/assets/skeleton-COKMAnJy.js +1 -0
- package/dist/assets/{switch-BsLtHOH-.js → switch-CBOzecWS.js} +1 -1
- package/dist/assets/{tabs-custom-D3HYMt6k.js → tabs-custom-Bx3cNhD-.js} +1 -1
- package/dist/assets/tag-chip-zUaDE2-H.js +1 -0
- package/dist/assets/{trash-2-G48scll7.js → trash-2-CQUgYyRn.js} +1 -1
- package/dist/assets/use-infinite-scroll-loader-B5V2Klve.js +1 -0
- package/dist/assets/useConfirmDialog-patAnl1g.js +1 -0
- package/dist/assets/{useMutation-CBWjE2uj.js → useMutation-__AYv-Pz.js} +1 -1
- package/dist/assets/x-BHUGQIUv.js +1 -0
- package/dist/index.html +22 -22
- package/dist/runtime-icons/claude.ico +0 -0
- package/dist/runtime-icons/codex-openai.svg +6 -0
- package/dist/runtime-icons/hermes-agent.png +0 -0
- package/module-structure.config.json +7 -0
- package/package.json +6 -6
- package/public/runtime-icons/claude.ico +0 -0
- package/public/runtime-icons/codex-openai.svg +6 -0
- package/public/runtime-icons/hermes-agent.png +0 -0
- package/src/api/chat-session-type.types.ts +7 -0
- package/src/api/config.ts +10 -0
- package/src/api/raw-client.test.ts +1 -1
- package/src/api/{raw-client.ts → raw-client.utils.ts} +2 -0
- package/src/api/runtime-control.types.ts +8 -0
- package/src/api/types.ts +48 -0
- package/src/app/components/app-manager-provider.tsx +20 -0
- package/src/app/managers/app.manager.ts +12 -0
- package/src/app.tsx +223 -59
- package/src/components/agents/agent-dialogs.tsx +499 -0
- package/src/components/agents/agents-page.test.tsx +238 -0
- package/src/components/agents/agents-page.tsx +435 -0
- package/src/components/chat/chat-conversation-panel.test.tsx +30 -0
- package/src/components/chat/chat-conversation-panel.tsx +83 -13
- package/src/components/chat/chat-input/ncp-chat-input-availability.utils.test.ts +92 -0
- package/src/components/chat/chat-input/ncp-chat-input-availability.utils.ts +45 -0
- package/src/components/chat/chat-page-shell.tsx +19 -13
- package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
- package/src/components/chat/chat-session-type-option-item.tsx +68 -0
- package/src/components/chat/chat-session-workspace-file-preview.test.tsx +87 -0
- package/src/components/chat/chat-session-workspace-file-preview.tsx +14 -43
- package/src/components/chat/chat-session-workspace-panel-nav.tsx +8 -2
- package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
- package/src/components/chat/containers/chat-input-bar.container.tsx +24 -12
- package/src/components/chat/{ChatSidebar.test.tsx → containers/chat-sidebar.test.tsx} +5 -4
- package/src/components/chat/{ChatSidebar.tsx → containers/chat-sidebar.tsx} +24 -72
- package/src/components/chat/hooks/use-chat-sidebar-session-label-editor.ts +49 -0
- package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
- package/src/components/chat/ncp/ncp-app-client-fetch.ts +3 -0
- package/src/components/chat/ncp/ncp-chat-input.manager.ts +13 -5
- package/src/components/chat/ncp/ncp-chat-page.tsx +23 -2
- package/src/components/chat/ncp/ncp-session-adapter.test.ts +1 -0
- package/src/components/chat/ncp/ncp-session-adapter.ts +3 -0
- package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +10 -4
- package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +48 -4
- package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +43 -5
- package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +51 -1
- package/src/components/chat/stores/chat-input.store.ts +2 -1
- package/src/components/chat/stores/chat-thread.store.ts +3 -1
- package/src/components/chat/useChatSessionTypeState.ts +10 -1
- package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
- package/src/components/common/BrandHeader.tsx +3 -1
- package/src/components/common/session-context-icon.tsx +15 -2
- package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
- package/src/components/config/ChannelForm.test.tsx +89 -3
- package/src/components/config/ChannelForm.tsx +157 -188
- package/src/components/config/ChannelsList.test.tsx +163 -119
- package/src/components/config/ChannelsList.tsx +90 -101
- package/src/components/config/ProviderForm.tsx +108 -146
- package/src/components/config/ProvidersList.tsx +100 -123
- package/src/components/config/SearchConfig.tsx +423 -393
- package/src/components/config/channel-form-fields-section.tsx +70 -37
- package/src/components/config/config-split-page.tsx +109 -0
- package/src/components/config/desktop-update-config.test.tsx +10 -4
- package/src/components/config/desktop-update-config.tsx +5 -3
- package/src/components/config/provider-enabled-field.tsx +17 -10
- package/src/components/config/runtime-control-card.test.tsx +136 -158
- package/src/components/config/runtime-control-card.tsx +43 -68
- package/src/components/config/runtime-presence-card.test.tsx +10 -14
- package/src/components/config/runtime-presence-card.tsx +97 -81
- package/src/components/layout/AppLayout.tsx +25 -37
- package/src/components/layout/Sidebar.tsx +4 -4
- package/src/components/layout/app-layout.test.tsx +46 -14
- package/src/components/layout/runtime-status-entry.test.tsx +101 -0
- package/src/components/layout/runtime-status-entry.tsx +95 -0
- package/src/components/layout/sidebar.layout.test.tsx +11 -5
- package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
- package/src/components/marketplace/marketplace-list-card.tsx +288 -0
- package/src/components/marketplace/marketplace-page-data.ts +129 -0
- package/src/components/marketplace/marketplace-page.test.tsx +339 -0
- package/src/components/marketplace/marketplace-page.tsx +596 -0
- package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
- package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
- package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
- package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
- package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
- package/src/components/ui/notice-card.tsx +129 -0
- package/src/components/ui/setting-row.tsx +51 -0
- package/src/components/ui/tag-chip.tsx +39 -0
- package/src/components/ui/textarea.tsx +19 -0
- package/src/features/account/components/account-panel.tsx +255 -0
- package/src/features/account/index.ts +6 -0
- package/src/{account → features/account}/managers/account.manager.ts +6 -5
- package/src/features/remote/components/remote-access-page.test.tsx +104 -0
- package/src/features/remote/components/remote-access-page.tsx +250 -0
- package/src/{hooks/useRemoteAccess.ts → features/remote/hooks/use-remote-access.ts} +1 -1
- package/src/features/remote/index.ts +27 -0
- package/src/{remote → features/remote}/managers/remote-access.manager.ts +3 -4
- package/src/{remote → features/remote/services}/remote-access-feedback.service.test.ts +1 -1
- package/src/features/system-status/hooks/use-system-status.ts +104 -0
- package/src/features/system-status/index.ts +12 -0
- package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +126 -0
- package/src/features/system-status/managers/system-status.manager.test.ts +142 -0
- package/src/features/system-status/managers/system-status.manager.ts +511 -0
- package/src/features/system-status/stores/system-status.store.ts +32 -0
- package/src/features/system-status/types/system-status.types.ts +73 -0
- package/src/features/system-status/utils/system-status.utils.test.ts +132 -0
- package/src/features/system-status/utils/system-status.utils.ts +202 -0
- package/src/hooks/use-realtime-query-bridge.ts +34 -18
- package/src/hooks/useConfig.ts +2 -1
- package/src/index.css +24 -0
- package/src/lib/app-resource-uri.test.ts +20 -0
- package/src/lib/app-resource-uri.ts +29 -0
- package/src/lib/i18n.chat.ts +8 -0
- package/src/lib/i18n.remote.ts +1 -1
- package/src/lib/i18n.runtime-control.ts +31 -0
- package/src/lib/i18n.ts +5 -8
- package/src/lib/session-context.utils.test.ts +71 -0
- package/src/lib/session-context.utils.ts +28 -3
- package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
- package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
- package/src/platforms/desktop/index.ts +20 -0
- package/src/{desktop → platforms/desktop}/managers/desktop-presence.manager.ts +2 -2
- package/src/{desktop → platforms/desktop}/managers/desktop-update.manager.ts +2 -2
- package/src/{desktop → platforms/desktop}/stores/desktop-presence.store.ts +1 -1
- package/src/{desktop → platforms/desktop}/stores/desktop-update.store.ts +1 -1
- package/src/stores/ui.store.ts +0 -9
- package/src/transport/{app-client.ts → app-client.service.ts} +9 -9
- package/src/transport/app-client.test.ts +9 -5
- package/src/transport/index.ts +1 -1
- package/src/transport/{local.transport.ts → local-transport.service.ts} +14 -12
- package/dist/assets/ChannelsList-Ita2Zm1_.js +0 -8
- package/dist/assets/DocBrowser-BNwbPHf4.js +0 -1
- package/dist/assets/MarketplacePage-CjX2MWww.js +0 -1
- package/dist/assets/MarketplacePage-D0sDlYX4.js +0 -49
- package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +0 -40
- package/dist/assets/ModelConfig-BzZenCH-.js +0 -1
- package/dist/assets/ProviderScopedModelInput-Da7khnBA.js +0 -1
- package/dist/assets/ProvidersList-BbVzRxjY.js +0 -1
- package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +0 -1
- package/dist/assets/RuntimeConfig-F_XKGgLm.js +0 -1
- package/dist/assets/SearchConfig-BGkzXQP-.js +0 -1
- package/dist/assets/SecretsConfig-D281Rotl.js +0 -3
- package/dist/assets/app-query-client-VnFElj4E.js +0 -1
- package/dist/assets/chat-page-Doe0yTtB.js +0 -58
- package/dist/assets/chat-session-display-cw78aiI_.js +0 -1
- package/dist/assets/client-_i4MU2bB.js +0 -7
- package/dist/assets/config-DtIQwrHF.js +0 -1
- package/dist/assets/config-layout-CHs0mAaR.js +0 -1
- package/dist/assets/desktop-update-config-Dpcf4BKG.js +0 -1
- package/dist/assets/dist-ccBFUi-o.js +0 -9
- package/dist/assets/index-CF9xve0E.js +0 -6
- package/dist/assets/index-FgA52VBt.css +0 -1
- package/dist/assets/infiniteQueryBehavior-ZDS92Qpp.js +0 -1
- package/dist/assets/loader-circle-ACM1s51e.js +0 -1
- package/dist/assets/page-layout-vZnghcFy.js +0 -1
- package/dist/assets/play-CFUwCA2E.js +0 -1
- package/dist/assets/plus-rYsv72JG.js +0 -1
- package/dist/assets/popover-Bg1VoTZ6.js +0 -1
- package/dist/assets/refresh-ccw-DT98i__E.js +0 -1
- package/dist/assets/rotate-cw-JtFzpNn6.js +0 -1
- package/dist/assets/search-3kFR_zh9.js +0 -1
- package/dist/assets/security-config-BWaiARNk.js +0 -1
- package/dist/assets/select-DJ2MUjBB.js +0 -41
- package/dist/assets/skeleton-ByQepn0M.js +0 -1
- package/dist/assets/status-dot-vbanNPFU.js +0 -1
- package/dist/assets/use-infinite-scroll-loader-DkNhD-42.js +0 -1
- package/dist/assets/useConfirmDialog-BkvTN-vd.js +0 -1
- package/dist/assets/x-ByDbItbq.js +0 -1
- package/src/account/components/account-panel.tsx +0 -135
- package/src/components/agents/AgentDialogs.tsx +0 -400
- package/src/components/agents/AgentsPage.test.tsx +0 -217
- package/src/components/agents/AgentsPage.tsx +0 -352
- package/src/components/config/config-layout.ts +0 -10
- package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
- package/src/components/marketplace/MarketplacePage.tsx +0 -827
- package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
- package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
- package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
- package/src/components/remote/RemoteAccessPage.tsx +0 -144
- package/src/hooks/use-runtime-control.ts +0 -24
- package/src/presenter/app-presenter-context.tsx +0 -20
- package/src/presenter/app.presenter.ts +0 -12
- package/src/runtime-control/runtime-control.manager.ts +0 -118
- /package/dist/assets/{config-hints-BhTmc9P1.js → config-hints-DSQQbeOA.js} +0 -0
- /package/src/{account → features/account}/stores/account.store.ts +0 -0
- /package/src/{remote → features/remote/services}/remote-access-feedback.service.ts +0 -0
- /package/src/{remote/remote-access.query.ts → features/remote/services/remote-access-query.service.ts} +0 -0
- /package/src/{remote → features/remote}/stores/remote-access.store.ts +0 -0
- /package/src/{desktop → platforms/desktop/types}/desktop-update.types.ts +0 -0
|
@@ -8,12 +8,14 @@ import {
|
|
|
8
8
|
FileOperationCodeSurface,
|
|
9
9
|
} from "@nextclaw/agent-chat-ui";
|
|
10
10
|
import type { ChatWorkspaceFileTab } from "@/components/chat/stores/chat-thread.store";
|
|
11
|
+
import { ChatSessionWorkspaceFileBreadcrumbs } from "@/components/chat/workspace/chat-session-workspace-file-breadcrumbs";
|
|
11
12
|
import { useServerPathRead } from "@/hooks/server-path/use-server-path-read";
|
|
12
13
|
import {
|
|
13
14
|
buildLineDiff,
|
|
14
15
|
buildPreviewLines,
|
|
15
16
|
} from "@/components/chat/adapters/file-operation/line-builder";
|
|
16
17
|
import { t } from "@/lib/i18n";
|
|
18
|
+
import { buildWorkspaceFileBreadcrumb } from "@/lib/session-project/workspace-file-breadcrumb";
|
|
17
19
|
import { cn } from "@/lib/utils";
|
|
18
20
|
|
|
19
21
|
function inferPreviewKind(params: {
|
|
@@ -118,44 +120,6 @@ function WorkspaceFilePreviewStatus({
|
|
|
118
120
|
);
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
function WorkspaceFileHeader({
|
|
122
|
-
file,
|
|
123
|
-
resolvedPath,
|
|
124
|
-
truncated,
|
|
125
|
-
}: {
|
|
126
|
-
file: ChatWorkspaceFileTab;
|
|
127
|
-
resolvedPath: string;
|
|
128
|
-
truncated: boolean;
|
|
129
|
-
}) {
|
|
130
|
-
return (
|
|
131
|
-
<div className="border-b border-gray-200/80 px-4 py-3">
|
|
132
|
-
<div
|
|
133
|
-
title={resolvedPath}
|
|
134
|
-
className="truncate font-mono text-[12px] font-medium text-gray-700"
|
|
135
|
-
>
|
|
136
|
-
{resolvedPath}
|
|
137
|
-
</div>
|
|
138
|
-
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
139
|
-
<span className="rounded border border-gray-200 bg-gray-50 px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.08em] text-gray-500">
|
|
140
|
-
{file.viewMode === "diff"
|
|
141
|
-
? t("chatWorkspaceDiff")
|
|
142
|
-
: t("chatWorkspacePreview")}
|
|
143
|
-
</span>
|
|
144
|
-
{typeof file.line === "number" ? (
|
|
145
|
-
<span className="rounded border border-gray-200 bg-gray-50 px-2 py-0.5 text-[10px] font-medium text-gray-500">
|
|
146
|
-
{`L${file.line}${typeof file.column === "number" ? `:${file.column}` : ""}`}
|
|
147
|
-
</span>
|
|
148
|
-
) : null}
|
|
149
|
-
{truncated ? (
|
|
150
|
-
<span className="rounded border border-amber-200 bg-amber-50 px-2 py-0.5 text-[10px] font-medium text-amber-700">
|
|
151
|
-
{t("chatWorkspacePreviewTruncated")}
|
|
152
|
-
</span>
|
|
153
|
-
) : null}
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
123
|
function WorkspaceDiffBody({
|
|
160
124
|
diffBlock,
|
|
161
125
|
}: {
|
|
@@ -280,14 +244,21 @@ export function ChatSessionWorkspaceFilePreview({
|
|
|
280
244
|
}, [file.line, file.path, isPreviewMode, previewQuery.data?.resolvedPath, previewText]);
|
|
281
245
|
const resolvedPath = previewQuery.data?.resolvedPath ?? file.path;
|
|
282
246
|
const isTruncated = Boolean(previewQuery.data?.truncated);
|
|
247
|
+
const breadcrumb = useMemo(
|
|
248
|
+
() =>
|
|
249
|
+
buildWorkspaceFileBreadcrumb({
|
|
250
|
+
path: resolvedPath,
|
|
251
|
+
sessionProjectRoot,
|
|
252
|
+
line: file.line,
|
|
253
|
+
column: file.column,
|
|
254
|
+
truncated: isTruncated,
|
|
255
|
+
}),
|
|
256
|
+
[file.column, file.line, isTruncated, resolvedPath, sessionProjectRoot],
|
|
257
|
+
);
|
|
283
258
|
|
|
284
259
|
return (
|
|
285
260
|
<div className="flex h-full min-h-0 flex-col bg-white">
|
|
286
|
-
<
|
|
287
|
-
file={file}
|
|
288
|
-
resolvedPath={resolvedPath}
|
|
289
|
-
truncated={isTruncated}
|
|
290
|
-
/>
|
|
261
|
+
<ChatSessionWorkspaceFileBreadcrumbs breadcrumb={breadcrumb} />
|
|
291
262
|
|
|
292
263
|
<div className="flex-1 min-h-0 overflow-hidden">
|
|
293
264
|
{file.viewMode === "diff" ? (
|
|
@@ -186,8 +186,14 @@ export function WorkspaceTabsBar({
|
|
|
186
186
|
tabs: readonly WorkspaceTabViewModel[];
|
|
187
187
|
}) {
|
|
188
188
|
return (
|
|
189
|
-
<div
|
|
190
|
-
|
|
189
|
+
<div
|
|
190
|
+
data-testid="workspace-tabs-bar"
|
|
191
|
+
className="workspace-horizontal-scrollbar overflow-x-auto overflow-y-hidden border-b border-gray-200/70 bg-gray-50/85"
|
|
192
|
+
>
|
|
193
|
+
<div
|
|
194
|
+
data-testid="workspace-tabs-scroll"
|
|
195
|
+
className="flex min-w-max items-stretch"
|
|
196
|
+
>
|
|
191
197
|
{tabs.map((tab) => (
|
|
192
198
|
<WorkspaceTabItem key={tab.key} tab={tab} />
|
|
193
199
|
))}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useMemo, useState, type ReactNode } from 'react';
|
|
2
2
|
import { Plus } from 'lucide-react';
|
|
3
3
|
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { ChatSessionTypeOptionItem } from '@/components/chat/chat-session-type-option-item';
|
|
4
5
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
5
6
|
import type { ChatInputSnapshot } from '@/components/chat/stores/chat-input.store';
|
|
6
7
|
import type { NcpSessionListItemView } from '@/components/chat/ncp/use-ncp-session-list-view';
|
|
7
8
|
import { t } from '@/lib/i18n';
|
|
8
|
-
import { cn } from '@/lib/utils';
|
|
9
9
|
|
|
10
10
|
export type ChatSidebarProjectGroup = {
|
|
11
11
|
projectRoot: string;
|
|
@@ -34,16 +34,6 @@ function resolveProjectGroupDefaultSessionType(
|
|
|
34
34
|
return sessionTypeOptions[0]?.value ?? defaultSessionType;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function resolveSessionTypeStatusText(option: {
|
|
38
|
-
ready?: boolean;
|
|
39
|
-
reasonMessage?: string | null;
|
|
40
|
-
}): string {
|
|
41
|
-
if (option.ready === false) {
|
|
42
|
-
return option.reasonMessage?.trim() || t('statusSetup');
|
|
43
|
-
}
|
|
44
|
-
return t('statusReady');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
37
|
export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
|
|
48
38
|
const { groups, defaultSessionType, sessionTypeOptions, renderSessionItem, onCreateSession } = props;
|
|
49
39
|
const [openProjectRoot, setOpenProjectRoot] = useState<string | null>(null);
|
|
@@ -91,38 +81,23 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
|
|
|
91
81
|
<Plus className="h-3.5 w-3.5" />
|
|
92
82
|
</Button>
|
|
93
83
|
</PopoverTrigger>
|
|
94
|
-
<PopoverContent
|
|
95
|
-
|
|
84
|
+
<PopoverContent
|
|
85
|
+
align="end"
|
|
86
|
+
className="w-56 rounded-2xl border border-gray-200/80 bg-white p-1.5 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
|
|
87
|
+
>
|
|
88
|
+
<div className="px-3 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-gray-400">
|
|
96
89
|
{t('chatSessionTypeLabel')}
|
|
97
90
|
</div>
|
|
98
|
-
<div className="
|
|
91
|
+
<div className="space-y-1">
|
|
99
92
|
{sessionTypeOptions.map((option) => (
|
|
100
|
-
<
|
|
93
|
+
<ChatSessionTypeOptionItem
|
|
101
94
|
key={`${group.projectRoot}:${option.value}`}
|
|
102
|
-
|
|
103
|
-
|
|
95
|
+
option={option}
|
|
96
|
+
onSelect={() => {
|
|
104
97
|
onCreateSession(option.value, group.projectRoot);
|
|
105
98
|
setOpenProjectRoot(null);
|
|
106
99
|
}}
|
|
107
|
-
|
|
108
|
-
>
|
|
109
|
-
<div className="flex items-center justify-between gap-3">
|
|
110
|
-
<div className="text-[13px] font-medium text-gray-900">{option.label}</div>
|
|
111
|
-
<span
|
|
112
|
-
className={cn(
|
|
113
|
-
'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold',
|
|
114
|
-
option.ready === false
|
|
115
|
-
? 'bg-amber-100 text-amber-800'
|
|
116
|
-
: 'bg-emerald-100 text-emerald-700'
|
|
117
|
-
)}
|
|
118
|
-
>
|
|
119
|
-
{option.ready === false ? t('statusSetup') : t('statusReady')}
|
|
120
|
-
</span>
|
|
121
|
-
</div>
|
|
122
|
-
<div className="mt-0.5 text-[11px] text-gray-500">
|
|
123
|
-
{resolveSessionTypeStatusText(option)}
|
|
124
|
-
</div>
|
|
125
|
-
</button>
|
|
100
|
+
/>
|
|
126
101
|
))}
|
|
127
102
|
</div>
|
|
128
103
|
</PopoverContent>
|
|
@@ -16,6 +16,13 @@ import {
|
|
|
16
16
|
type ChatThinkingLevel
|
|
17
17
|
} from '@/components/chat/adapters/chat-input-bar.adapter';
|
|
18
18
|
import { deriveSelectedSkillsFromComposer } from '@/components/chat/chat-composer-state';
|
|
19
|
+
import {
|
|
20
|
+
hasNcpChatModelOptions,
|
|
21
|
+
isNcpChatComposerDisabled,
|
|
22
|
+
isNcpChatModelOptionsEmpty,
|
|
23
|
+
isNcpChatModelOptionsLoading,
|
|
24
|
+
isNcpChatSendDisabled,
|
|
25
|
+
} from '@/components/chat/chat-input/ncp-chat-input-availability.utils';
|
|
19
26
|
import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
|
|
20
27
|
import {
|
|
21
28
|
CHAT_RECENT_MODELS_MIN_OPTIONS,
|
|
@@ -27,6 +34,7 @@ import {
|
|
|
27
34
|
} from '@/components/chat/chat-recent-skills.manager';
|
|
28
35
|
import { useI18n } from '@/components/providers/I18nProvider';
|
|
29
36
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
37
|
+
import { useChatRuntimeAvailability } from '@/features/system-status';
|
|
30
38
|
import type { SessionSkillEntryView } from '@/api/types';
|
|
31
39
|
import { t } from '@/lib/i18n';
|
|
32
40
|
import { toast } from 'sonner';
|
|
@@ -83,11 +91,13 @@ export function ChatInputBarContainer() {
|
|
|
83
91
|
const presenter = usePresenter();
|
|
84
92
|
const { language } = useI18n();
|
|
85
93
|
const snapshot = useChatInputStore((state) => state.snapshot);
|
|
94
|
+
const runtimeAvailability = useChatRuntimeAvailability();
|
|
86
95
|
const [slashQuery, setSlashQuery] = useState<string | null>(null);
|
|
87
96
|
const inputBarRef = useRef<ChatInputBarHandle | null>(null);
|
|
88
97
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
89
98
|
|
|
90
99
|
const skillScopeLabels = useMemo<Record<'builtin' | 'project' | 'workspace', string>>(() => {
|
|
100
|
+
void language;
|
|
91
101
|
return {
|
|
92
102
|
builtin: t('chatSkillScopeBuiltin'),
|
|
93
103
|
project: t('chatSkillScopeProject'),
|
|
@@ -96,6 +106,7 @@ export function ChatInputBarContainer() {
|
|
|
96
106
|
}, [language]);
|
|
97
107
|
const slashTexts = useMemo(
|
|
98
108
|
() => {
|
|
109
|
+
void language;
|
|
99
110
|
return {
|
|
100
111
|
slashSkillSubtitle: t('chatSlashTypeSkill'),
|
|
101
112
|
slashSkillSpecLabel: t('chatSlashSkillSpec'),
|
|
@@ -124,17 +135,14 @@ export function ChatInputBarContainer() {
|
|
|
124
135
|
minAvailableCount: CHAT_RECENT_SKILLS_MIN_OPTIONS
|
|
125
136
|
});
|
|
126
137
|
|
|
127
|
-
const hasModelOptions =
|
|
128
|
-
const isModelOptionsLoading =
|
|
129
|
-
const isModelOptionsEmpty = snapshot
|
|
130
|
-
const inputDisabled =
|
|
131
|
-
((isModelOptionsLoading || isModelOptionsEmpty) && !snapshot.isSending) || snapshot.sessionTypeUnavailable;
|
|
138
|
+
const hasModelOptions = hasNcpChatModelOptions(snapshot);
|
|
139
|
+
const isModelOptionsLoading = isNcpChatModelOptionsLoading(snapshot);
|
|
140
|
+
const isModelOptionsEmpty = isNcpChatModelOptionsEmpty(snapshot);
|
|
141
|
+
const inputDisabled = isNcpChatComposerDisabled(snapshot);
|
|
132
142
|
const attachmentSupported = typeof presenter.chatInputManager.addAttachments === 'function';
|
|
133
|
-
const textareaPlaceholder =
|
|
134
|
-
? ''
|
|
135
|
-
:
|
|
136
|
-
? t('chatInputPlaceholder')
|
|
137
|
-
: t('chatModelNoOptions');
|
|
143
|
+
const textareaPlaceholder = isModelOptionsEmpty
|
|
144
|
+
? t('chatModelNoOptions')
|
|
145
|
+
: t('chatInputPlaceholder');
|
|
138
146
|
const recentModelsLabel = t('chatPickerRecentModels');
|
|
139
147
|
const allModelsLabel = t('chatPickerAllModels');
|
|
140
148
|
const recentSkillsLabel = t('chatPickerRecent');
|
|
@@ -298,10 +306,14 @@ export function ChatInputBarContainer() {
|
|
|
298
306
|
],
|
|
299
307
|
skillPicker,
|
|
300
308
|
actions: {
|
|
301
|
-
sendError: snapshot.sendError,
|
|
309
|
+
sendError: runtimeAvailability.isBlocked ? null : snapshot.sendError,
|
|
302
310
|
isSending: snapshot.isSending,
|
|
303
311
|
canStopGeneration: snapshot.canStopGeneration,
|
|
304
|
-
sendDisabled:
|
|
312
|
+
sendDisabled: isNcpChatSendDisabled({
|
|
313
|
+
snapshot,
|
|
314
|
+
hasSendableDraft,
|
|
315
|
+
isRuntimeBlocked: runtimeAvailability.isBlocked,
|
|
316
|
+
}),
|
|
305
317
|
stopDisabled: !snapshot.canStopGeneration,
|
|
306
318
|
stopHint: resolvedStopHint,
|
|
307
319
|
sendButtonLabel: t('chatSend'),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
2
|
import { MemoryRouter } from 'react-router-dom';
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { ChatSidebar } from '@/components/chat/
|
|
4
|
+
import { ChatSidebar } from '@/components/chat/containers/chat-sidebar';
|
|
5
5
|
import type { NcpSessionListItemView } from '@/components/chat/ncp/use-ncp-session-list-view';
|
|
6
6
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
7
7
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
@@ -107,9 +107,10 @@ vi.mock('@/components/providers/ThemeProvider', () => ({
|
|
|
107
107
|
})
|
|
108
108
|
}));
|
|
109
109
|
|
|
110
|
-
vi.mock('@/
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
vi.mock('@/features/system-status', () => ({
|
|
111
|
+
useSystemStatus: () => ({
|
|
112
|
+
connectionStatus: 'connected'
|
|
113
|
+
})
|
|
113
114
|
}));
|
|
114
115
|
|
|
115
116
|
function resetSidebarTestState() {
|
|
@@ -7,13 +7,14 @@ import { Input } from '@/components/ui/input';
|
|
|
7
7
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
8
8
|
import { SelectItem } from '@/components/ui/select';
|
|
9
9
|
import { ChatSidebarSessionItem } from '@/components/chat/chat-sidebar-session-item';
|
|
10
|
+
import { ChatSessionTypeOptionItem } from '@/components/chat/chat-session-type-option-item';
|
|
10
11
|
import { ChatSidebarListModeSwitch } from '@/components/chat/chat-sidebar-list-mode-switch';
|
|
11
12
|
import {
|
|
12
13
|
ChatSidebarProjectGroups,
|
|
13
14
|
type ChatSidebarProjectGroup
|
|
14
15
|
} from '@/components/chat/chat-sidebar-project-groups';
|
|
15
16
|
import { resolveSessionContextView } from '@/lib/session-context.utils';
|
|
16
|
-
import {
|
|
17
|
+
import { useChatSidebarSessionLabelEditor } from '@/components/chat/hooks/use-chat-sidebar-session-label-editor';
|
|
17
18
|
import { useNcpSessionListView, type NcpSessionListItemView } from '@/components/chat/ncp/use-ncp-session-list-view';
|
|
18
19
|
import { usePresenter } from '@/components/chat/presenter/chat-presenter-context';
|
|
19
20
|
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
@@ -30,7 +31,7 @@ import { useI18n } from '@/components/providers/I18nProvider';
|
|
|
30
31
|
import { useTheme } from '@/components/providers/ThemeProvider';
|
|
31
32
|
import { useDocBrowser } from '@/components/doc-browser';
|
|
32
33
|
import { SidebarActionItem, SidebarNavLinkItem, SidebarSelectItem } from '@/components/layout/sidebar-items';
|
|
33
|
-
import {
|
|
34
|
+
import { useSystemStatus } from '@/features/system-status';
|
|
34
35
|
import {
|
|
35
36
|
AlarmClock,
|
|
36
37
|
Bot,
|
|
@@ -130,16 +131,6 @@ function sessionTitle(session: SessionEntryView): string {
|
|
|
130
131
|
return chunks[chunks.length - 1] || session.key;
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
function resolveSessionTypeStatusText(option: {
|
|
134
|
-
ready?: boolean;
|
|
135
|
-
reasonMessage?: string | null;
|
|
136
|
-
}): string {
|
|
137
|
-
if (option.ready === false) {
|
|
138
|
-
return option.reasonMessage?.trim() || t('statusSetup');
|
|
139
|
-
}
|
|
140
|
-
return t('statusReady');
|
|
141
|
-
}
|
|
142
|
-
|
|
143
134
|
const navItems = [
|
|
144
135
|
{ target: '/cron', label: () => t('chatSidebarScheduledTasks'), icon: AlarmClock },
|
|
145
136
|
{ target: '/skills', label: () => t('chatSidebarSkills'), icon: BrainCircuit },
|
|
@@ -178,22 +169,17 @@ function useChatSessionUnreadState(
|
|
|
178
169
|
|
|
179
170
|
return optimisticReadAtBySessionKey;
|
|
180
171
|
}
|
|
181
|
-
|
|
182
172
|
export function ChatSidebar() {
|
|
183
173
|
const presenter = usePresenter();
|
|
184
174
|
const docBrowser = useDocBrowser();
|
|
185
175
|
const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
|
|
186
|
-
const [editingSessionKey, setEditingSessionKey] = useState<string | null>(null);
|
|
187
|
-
const [draftLabel, setDraftLabel] = useState('');
|
|
188
|
-
const [savingSessionKey, setSavingSessionKey] = useState<string | null>(null);
|
|
189
176
|
const inputSnapshot = useChatInputStore((state) => state.snapshot);
|
|
190
177
|
const listSnapshot = useChatSessionListStore((state) => state.snapshot);
|
|
191
|
-
const
|
|
178
|
+
const systemStatus = useSystemStatus();
|
|
192
179
|
const agentsQuery = useAgents();
|
|
193
180
|
const { isLoading, items } = useNcpSessionListView();
|
|
194
181
|
const { language, setLanguage } = useI18n();
|
|
195
182
|
const { theme, setTheme } = useTheme();
|
|
196
|
-
const updateSessionLabel = useChatSessionLabel();
|
|
197
183
|
const currentThemeLabel = t(THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? 'themeWarm');
|
|
198
184
|
const currentLanguageLabel = LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
|
|
199
185
|
const agentsById = useMemo(
|
|
@@ -230,39 +216,20 @@ export function ChatSidebar() {
|
|
|
230
216
|
listSnapshot.selectedSessionKey,
|
|
231
217
|
presenter.chatSessionListManager.markSessionRead,
|
|
232
218
|
);
|
|
219
|
+
const {
|
|
220
|
+
editingSessionKey,
|
|
221
|
+
draftLabel,
|
|
222
|
+
savingSessionKey,
|
|
223
|
+
setDraftLabel,
|
|
224
|
+
startEditingSessionLabel,
|
|
225
|
+
cancelEditingSessionLabel,
|
|
226
|
+
saveSessionLabel,
|
|
227
|
+
} = useChatSidebarSessionLabelEditor();
|
|
233
228
|
const handleLanguageSwitch = (nextLang: I18nLanguage) => {
|
|
234
229
|
if (language === nextLang) return;
|
|
235
230
|
setLanguage(nextLang);
|
|
236
231
|
window.location.reload();
|
|
237
232
|
};
|
|
238
|
-
const startEditingSessionLabel = (session: SessionEntryView) => {
|
|
239
|
-
setEditingSessionKey(session.key);
|
|
240
|
-
setDraftLabel(session.label?.trim() ?? '');
|
|
241
|
-
};
|
|
242
|
-
const cancelEditingSessionLabel = () => {
|
|
243
|
-
setEditingSessionKey(null);
|
|
244
|
-
setDraftLabel('');
|
|
245
|
-
setSavingSessionKey(null);
|
|
246
|
-
};
|
|
247
|
-
const saveSessionLabel = async (session: SessionEntryView) => {
|
|
248
|
-
const normalizedLabel = draftLabel.trim();
|
|
249
|
-
const currentLabel = session.label?.trim() ?? '';
|
|
250
|
-
if (normalizedLabel === currentLabel) {
|
|
251
|
-
cancelEditingSessionLabel();
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
setSavingSessionKey(session.key);
|
|
256
|
-
try {
|
|
257
|
-
await updateSessionLabel({
|
|
258
|
-
sessionKey: session.key,
|
|
259
|
-
label: normalizedLabel || null
|
|
260
|
-
});
|
|
261
|
-
cancelEditingSessionLabel();
|
|
262
|
-
} catch {
|
|
263
|
-
setSavingSessionKey(null);
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
233
|
const renderSessionItem = ({ session, runStatus }: NcpSessionListItemView) => {
|
|
267
234
|
const active = listSnapshot.selectedSessionKey === session.key;
|
|
268
235
|
const optimisticReadAt = optimisticReadAtBySessionKey[session.key];
|
|
@@ -315,7 +282,7 @@ export function ChatSidebar() {
|
|
|
315
282
|
<div className="px-5 pt-5 pb-3">
|
|
316
283
|
<BrandHeader
|
|
317
284
|
className="flex items-center gap-2.5 min-w-0"
|
|
318
|
-
suffix={<StatusBadge status={connectionStatus} />}
|
|
285
|
+
suffix={<StatusBadge status={systemStatus.connectionStatus} />}
|
|
319
286
|
/>
|
|
320
287
|
</div>
|
|
321
288
|
|
|
@@ -347,38 +314,23 @@ export function ChatSidebar() {
|
|
|
347
314
|
<ChevronDown className="h-4 w-4" />
|
|
348
315
|
</Button>
|
|
349
316
|
</PopoverTrigger>
|
|
350
|
-
<PopoverContent
|
|
351
|
-
|
|
317
|
+
<PopoverContent
|
|
318
|
+
align="end"
|
|
319
|
+
className="w-56 rounded-2xl border border-gray-200/80 bg-white p-1.5 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
|
|
320
|
+
>
|
|
321
|
+
<div className="px-3 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-gray-400">
|
|
352
322
|
{t('chatSessionTypeLabel')}
|
|
353
323
|
</div>
|
|
354
|
-
<div className="
|
|
324
|
+
<div className="space-y-1">
|
|
355
325
|
{nonDefaultSessionTypeOptions.map((option) => (
|
|
356
|
-
<
|
|
326
|
+
<ChatSessionTypeOptionItem
|
|
357
327
|
key={option.value}
|
|
358
|
-
|
|
359
|
-
|
|
328
|
+
option={option}
|
|
329
|
+
onSelect={() => {
|
|
360
330
|
presenter.chatSessionListManager.createSession(option.value);
|
|
361
331
|
setIsCreateMenuOpen(false);
|
|
362
332
|
}}
|
|
363
|
-
|
|
364
|
-
>
|
|
365
|
-
<div className="flex items-center justify-between gap-3">
|
|
366
|
-
<div className="text-[13px] font-medium text-gray-900">{option.label}</div>
|
|
367
|
-
<span
|
|
368
|
-
className={cn(
|
|
369
|
-
'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold',
|
|
370
|
-
option.ready === false
|
|
371
|
-
? 'bg-amber-100 text-amber-800'
|
|
372
|
-
: 'bg-emerald-100 text-emerald-700'
|
|
373
|
-
)}
|
|
374
|
-
>
|
|
375
|
-
{option.ready === false ? t('statusSetup') : t('statusReady')}
|
|
376
|
-
</span>
|
|
377
|
-
</div>
|
|
378
|
-
<div className="mt-0.5 text-[11px] text-gray-500">
|
|
379
|
-
{resolveSessionTypeStatusText(option)}
|
|
380
|
-
</div>
|
|
381
|
-
</button>
|
|
333
|
+
/>
|
|
382
334
|
))}
|
|
383
335
|
</div>
|
|
384
336
|
</PopoverContent>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { SessionEntryView } from '@/api/types';
|
|
3
|
+
import { useChatSessionLabel } from '@/components/chat/hooks/use-chat-session-label';
|
|
4
|
+
|
|
5
|
+
export function useChatSidebarSessionLabelEditor() {
|
|
6
|
+
const updateSessionLabel = useChatSessionLabel();
|
|
7
|
+
const [editingSessionKey, setEditingSessionKey] = useState<string | null>(null);
|
|
8
|
+
const [draftLabel, setDraftLabel] = useState('');
|
|
9
|
+
const [savingSessionKey, setSavingSessionKey] = useState<string | null>(null);
|
|
10
|
+
|
|
11
|
+
const startEditingSessionLabel = (session: SessionEntryView) => {
|
|
12
|
+
setEditingSessionKey(session.key);
|
|
13
|
+
setDraftLabel(session.label?.trim() ?? '');
|
|
14
|
+
};
|
|
15
|
+
const cancelEditingSessionLabel = () => {
|
|
16
|
+
setEditingSessionKey(null);
|
|
17
|
+
setDraftLabel('');
|
|
18
|
+
setSavingSessionKey(null);
|
|
19
|
+
};
|
|
20
|
+
const saveSessionLabel = async (session: SessionEntryView) => {
|
|
21
|
+
const normalizedLabel = draftLabel.trim();
|
|
22
|
+
const currentLabel = session.label?.trim() ?? '';
|
|
23
|
+
if (normalizedLabel === currentLabel) {
|
|
24
|
+
cancelEditingSessionLabel();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setSavingSessionKey(session.key);
|
|
29
|
+
try {
|
|
30
|
+
await updateSessionLabel({
|
|
31
|
+
sessionKey: session.key,
|
|
32
|
+
label: normalizedLabel || null,
|
|
33
|
+
});
|
|
34
|
+
cancelEditingSessionLabel();
|
|
35
|
+
} catch {
|
|
36
|
+
setSavingSessionKey(null);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
editingSessionKey,
|
|
42
|
+
draftLabel,
|
|
43
|
+
savingSessionKey,
|
|
44
|
+
setDraftLabel,
|
|
45
|
+
startEditingSessionLabel,
|
|
46
|
+
cancelEditingSessionLabel,
|
|
47
|
+
saveSessionLabel,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { adaptChatMessage } from '@/components/chat/adapters/chat-message.adapter';
|
|
3
|
+
import { adaptNcpMessageToUiMessage } from '../ncp-session-adapter';
|
|
4
|
+
|
|
5
|
+
const texts = {
|
|
6
|
+
roleLabels: {
|
|
7
|
+
user: 'User',
|
|
8
|
+
assistant: 'Assistant',
|
|
9
|
+
tool: 'Tool',
|
|
10
|
+
system: 'System',
|
|
11
|
+
fallback: 'Message',
|
|
12
|
+
},
|
|
13
|
+
reasoningLabel: 'Reasoning',
|
|
14
|
+
toolCallLabel: 'Tool Call',
|
|
15
|
+
toolResultLabel: 'Tool Result',
|
|
16
|
+
toolInputLabel: 'Input',
|
|
17
|
+
toolNoOutputLabel: 'No output',
|
|
18
|
+
toolOutputLabel: 'Output',
|
|
19
|
+
toolStatusPreparingLabel: 'Preparing',
|
|
20
|
+
toolStatusRunningLabel: 'Running',
|
|
21
|
+
toolStatusCompletedLabel: 'Completed',
|
|
22
|
+
toolStatusFailedLabel: 'Failed',
|
|
23
|
+
toolStatusCancelledLabel: 'Cancelled',
|
|
24
|
+
imageAttachmentLabel: 'Image',
|
|
25
|
+
fileAttachmentLabel: 'File',
|
|
26
|
+
unknownPartLabel: 'Unknown',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe('adaptNcpMessageToUiMessage cancelled tools', () => {
|
|
30
|
+
it('renders cancelled tool invocations as cancelled cards', () => {
|
|
31
|
+
const uiMessage = adaptNcpMessageToUiMessage({
|
|
32
|
+
id: 'ncp-message-tool-cancelled-1',
|
|
33
|
+
sessionId: 'ncp-session-1',
|
|
34
|
+
role: 'assistant',
|
|
35
|
+
status: 'final',
|
|
36
|
+
timestamp: '2026-04-01T00:00:00.000Z',
|
|
37
|
+
parts: [
|
|
38
|
+
{
|
|
39
|
+
type: 'tool-invocation',
|
|
40
|
+
toolCallId: 'tool-cancelled-1',
|
|
41
|
+
toolName: 'write_file',
|
|
42
|
+
state: 'cancelled',
|
|
43
|
+
args: JSON.stringify({
|
|
44
|
+
path: 'src/app.ts',
|
|
45
|
+
content: 'hello',
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const adapted = adaptChatMessage(
|
|
52
|
+
{
|
|
53
|
+
id: uiMessage.id,
|
|
54
|
+
role: uiMessage.role,
|
|
55
|
+
meta: {
|
|
56
|
+
timestamp: uiMessage.meta?.timestamp,
|
|
57
|
+
status: uiMessage.meta?.status,
|
|
58
|
+
},
|
|
59
|
+
parts: uiMessage.parts as never,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
formatTimestamp: (value) => value ?? '',
|
|
63
|
+
texts,
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(adapted.parts[0]).toMatchObject({
|
|
68
|
+
type: 'tool-card',
|
|
69
|
+
card: {
|
|
70
|
+
toolName: 'write_file',
|
|
71
|
+
statusTone: 'cancelled',
|
|
72
|
+
statusLabel: 'Cancelled',
|
|
73
|
+
titleLabel: 'Tool Result',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { systemStatusManager } from '@/features/system-status';
|
|
2
|
+
|
|
1
3
|
type FetchLike = typeof fetch;
|
|
2
4
|
|
|
3
5
|
function formatFetchTarget(input: RequestInfo | URL): string {
|
|
@@ -38,6 +40,7 @@ export function createNcpAppClientFetch(): FetchLike {
|
|
|
38
40
|
...init
|
|
39
41
|
});
|
|
40
42
|
} catch (error) {
|
|
43
|
+
systemStatusManager.reportTransportFailure(formatUnknownFetchError(error));
|
|
41
44
|
const method = (init?.method || 'GET').toUpperCase();
|
|
42
45
|
const target = formatFetchTarget(input);
|
|
43
46
|
throw createErrorWithCause(
|
|
@@ -21,10 +21,12 @@ import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-s
|
|
|
21
21
|
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
22
22
|
import type { ChatSessionListManager } from '@/components/chat/managers/chat-session-list.manager';
|
|
23
23
|
import { ChatSessionPreferenceSync } from '@/components/chat/chat-session-preference-sync';
|
|
24
|
+
import { isNcpChatSendDisabled } from '@/components/chat/chat-input/ncp-chat-input-availability.utils';
|
|
24
25
|
import { chatRecentModelsManager } from '@/components/chat/chat-recent-models.manager';
|
|
25
26
|
import { chatRecentSkillsManager } from '@/components/chat/chat-recent-skills.manager';
|
|
26
27
|
import type { ChatModelOption } from '@/components/chat/chat-input.types';
|
|
27
28
|
import { normalizeSessionType } from '@/components/chat/useChatSessionTypeState';
|
|
29
|
+
import { systemStatusManager } from '@/features/system-status';
|
|
28
30
|
|
|
29
31
|
export class NcpChatInputManager {
|
|
30
32
|
private readonly sessionPreferenceSync = new ChatSessionPreferenceSync(updateNcpSession);
|
|
@@ -139,7 +141,7 @@ export class NcpChatInputManager {
|
|
|
139
141
|
if (attachments.length === 0) {
|
|
140
142
|
return [];
|
|
141
143
|
}
|
|
142
|
-
const snapshot = useChatInputStore.getState()
|
|
144
|
+
const { snapshot } = useChatInputStore.getState();
|
|
143
145
|
const existingSignatures = new Set(snapshot.attachments.map(this.buildAttachmentSignature));
|
|
144
146
|
const nextAttachments = this.dedupeAttachments([...snapshot.attachments, ...attachments]);
|
|
145
147
|
const insertedAttachments = nextAttachments.filter(
|
|
@@ -173,12 +175,18 @@ export class NcpChatInputManager {
|
|
|
173
175
|
const sessionSnapshot = useChatSessionListStore.getState().snapshot;
|
|
174
176
|
const threadSnapshot = useChatThreadStore.getState().snapshot;
|
|
175
177
|
const message = inputSnapshot.draft.trim();
|
|
176
|
-
const attachments = inputSnapshot
|
|
178
|
+
const { attachments } = inputSnapshot;
|
|
177
179
|
const parts = deriveNcpMessagePartsFromComposer(inputSnapshot.composerNodes, attachments);
|
|
178
180
|
const hasSendableContent = parts.some(
|
|
179
181
|
(part) => part.type !== 'text' || part.text.trim().length > 0
|
|
180
182
|
);
|
|
181
|
-
if (
|
|
183
|
+
if (
|
|
184
|
+
isNcpChatSendDisabled({
|
|
185
|
+
snapshot: inputSnapshot,
|
|
186
|
+
hasSendableDraft: hasSendableContent,
|
|
187
|
+
isRuntimeBlocked: systemStatusManager.isChatInteractionBlocked(),
|
|
188
|
+
})
|
|
189
|
+
) {
|
|
182
190
|
return;
|
|
183
191
|
}
|
|
184
192
|
const { selectedSkills: requestedSkills, composerNodes } = inputSnapshot;
|
|
@@ -237,7 +245,7 @@ export class NcpChatInputManager {
|
|
|
237
245
|
};
|
|
238
246
|
|
|
239
247
|
setSelectedSkills = (next: SetStateAction<string[]>) => {
|
|
240
|
-
const snapshot = useChatInputStore.getState()
|
|
248
|
+
const { snapshot } = useChatInputStore.getState();
|
|
241
249
|
const { selectedSkills: prev } = snapshot;
|
|
242
250
|
const value = this.resolveUpdateValue(prev, next);
|
|
243
251
|
if (this.isSameStringArray(value, prev)) {
|
|
@@ -292,7 +300,7 @@ export class NcpChatInputManager {
|
|
|
292
300
|
};
|
|
293
301
|
|
|
294
302
|
private reconcileThinkingForModel = (model: string): void => {
|
|
295
|
-
const snapshot = useChatInputStore.getState()
|
|
303
|
+
const { snapshot } = useChatInputStore.getState();
|
|
296
304
|
const modelOption = snapshot.modelOptions.find((option) => option.value === model);
|
|
297
305
|
const { selectedThinkingLevel } = snapshot;
|
|
298
306
|
const nextThinking = this.resolveThinkingForModel(modelOption, selectedThinkingLevel);
|