@nextclaw/ui 0.14.1 → 0.14.2
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 +29 -0
- package/dist/assets/{api-DPcvCLfa.js → api-a14tR_z1.js} +7 -7
- package/dist/assets/channels-list-page-CFGkm4DT.js +8 -0
- package/dist/assets/chat-page-CEdavJXc.js +105 -0
- package/dist/assets/{config-split-page-BCJhqdK9.js → config-split-page-B5b9B0uE.js} +1 -1
- package/dist/assets/{confirm-dialog-CWh5FfK2.js → confirm-dialog-ClH_Ec7n.js} +1 -1
- package/dist/assets/{desktop-update-config-DsRhBizi.js → desktop-update-config-DPNDnVpk.js} +1 -1
- package/dist/assets/{dist-DPrgcEC0.js → dist-BtCS2Ucq.js} +1 -1
- package/dist/assets/{dist-DsE4OsSS.js → dist-CLGGLm8b.js} +1 -1
- package/dist/assets/doc-browser-5nMWYKBD.js +1 -0
- package/dist/assets/doc-browser-BJRLhztv.js +1 -0
- package/dist/assets/doc-browser-context-D4JwMMxK.js +1 -0
- package/dist/assets/{ellipsis-B81FStLm.js → ellipsis-CtJXjIol.js} +1 -1
- package/dist/assets/{external-link-C_dmG-WJ.js → external-link-CdJiG13p.js} +1 -1
- package/dist/assets/index-BDN1zzMQ.js +105 -0
- package/dist/assets/index-g3uSZXoc.css +1 -0
- package/dist/assets/mcp-marketplace-page-BU0kIjhR.js +40 -0
- package/dist/assets/mcp-marketplace-page-sMNFSq_T.js +1 -0
- package/dist/assets/middleware-CkFT3R4v.js +1 -0
- package/dist/assets/model-config-page-D6hYXROd.js +1 -0
- package/dist/assets/plus-DPjb0piO.js +1 -0
- package/dist/assets/provider-scoped-model-input-DAX2HgmR.js +1 -0
- package/dist/assets/providers-config-page-BzAeaACA.js +1 -0
- package/dist/assets/{react-sZz5HSCm.js → react-C6cZgP44.js} +1 -1
- package/dist/assets/remote-B0G80uPr.js +1 -0
- package/dist/assets/rotate-cw-DnEB1Rju.js +1 -0
- package/dist/assets/runtime-config-page-C5xKaV3i.js +1 -0
- package/dist/assets/{save-MzjoiHri.js → save-DKA0x4zk.js} +1 -1
- package/dist/assets/{search-B29dE4GN.js → search-BNlcoj5c.js} +1 -1
- package/dist/assets/{search-config-page-Dl5sZzob.js → search-config-page-Cq9uJ-N8.js} +1 -1
- package/dist/assets/{secrets-config-page-C_l-IFCK.js → secrets-config-page-CxSbZLBo.js} +2 -2
- package/dist/assets/{select-BKmh05hd.js → select-CvTlDZTQ.js} +1 -1
- package/dist/assets/{tag-chip-BS_7Ueom.js → tag-chip-8ffF2y2C.js} +1 -1
- package/dist/assets/use-config-DmT0o1gv.js +1 -0
- package/dist/assets/x-BiSSpgqB.js +1 -0
- package/dist/index.html +15 -14
- package/package.json +9 -9
- package/src/app/components/app-presenter-provider.tsx +11 -2
- package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
- package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
- package/src/app/components/layout/app-layout.tsx +18 -4
- package/src/app/components/layout/sidebar-items.tsx +194 -71
- package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
- package/src/app/components/layout/sidebar.tsx +340 -83
- package/src/app/managers/viewport-layout.manager.ts +10 -0
- package/src/app/stores/viewport-layout.store.ts +39 -3
- package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
- package/src/features/agents/components/agents-page.tsx +10 -6
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
- package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
- package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +14 -15
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +18 -18
- package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
- package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +16 -14
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
- package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +80 -7
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +82 -25
- package/src/features/chat/components/layout/chat-sidebar.tsx +59 -92
- package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +289 -0
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +427 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +204 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
- package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +32 -17
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
- package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
- package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
- package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
- package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
- package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
- package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
- package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
- package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
- package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +93 -12
- package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
- package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
- package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
- package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
- package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
- package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
- package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
- package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +2 -2
- package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +2 -0
- package/src/features/chat/features/session-type/components/chat-session-type-menu.tsx +2 -2
- package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +2 -2
- package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
- package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -2
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
- package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +2 -2
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +2 -2
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
- package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
- package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
- package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
- package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
- package/src/features/chat/index.ts +0 -1
- package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
- package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
- package/src/features/chat/managers/chat-thread.manager.ts +108 -34
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
- package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
- package/src/features/chat/presenters/chat.presenter.ts +2 -19
- package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
- package/src/features/chat/stores/chat-thread.store.ts +42 -26
- package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
- package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
- package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
- package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
- package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
- package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
- package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
- package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
- package/src/shared/lib/ui-document-title/index.ts +50 -21
- package/dist/assets/channels-list-page-D6jtziu4.js +0 -8
- package/dist/assets/chat-page-BBnNJoKG.js +0 -105
- package/dist/assets/doc-browser-C8xOF_9F.js +0 -1
- package/dist/assets/doc-browser-DgQedeAn.js +0 -1
- package/dist/assets/doc-browser-context-B46tQGmO.js +0 -1
- package/dist/assets/index-7o1WdUbU.js +0 -103
- package/dist/assets/index-DQWmEWzk.css +0 -1
- package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +0 -1
- package/dist/assets/mcp-marketplace-page-Dzull_4-.js +0 -40
- package/dist/assets/model-config-page-Cv0O3BIj.js +0 -1
- package/dist/assets/plus-Bnt56oqG.js +0 -1
- package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +0 -1
- package/dist/assets/providers-config-page-CPDz0Lxc.js +0 -1
- package/dist/assets/remote-B0LMs8u7.js +0 -1
- package/dist/assets/rotate-cw-BfyG_KEF.js +0 -1
- package/dist/assets/runtime-config-page-q0fvv8ME.js +0 -1
- package/dist/assets/use-config-DZkBWIdL.js +0 -1
- package/dist/assets/x-CW2KO-Hz.js +0 -1
- package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
- package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
- package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
- package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
- package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
- package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
- package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
- package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
- package/src/features/chat/managers/chat-input.manager.ts +0 -430
- package/src/features/chat/managers/chat-run.manager.ts +0 -110
- package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
- package/src/features/chat/stores/chat-input.store.ts +0 -104
- /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts
CHANGED
|
@@ -6,39 +6,18 @@ import {
|
|
|
6
6
|
isNcpChatModelOptionsLoading,
|
|
7
7
|
isNcpChatSendDisabled,
|
|
8
8
|
} from '@/features/chat/features/input/utils/ncp-chat-input-availability.utils';
|
|
9
|
-
import type { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
type AvailabilitySnapshot = {
|
|
11
|
+
readonly isProviderStateResolved: boolean;
|
|
12
|
+
readonly modelOptions: readonly unknown[];
|
|
13
|
+
readonly sessionTypeUnavailable: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function createSnapshot(overrides: Partial<AvailabilitySnapshot> = {}): AvailabilitySnapshot {
|
|
14
17
|
return {
|
|
15
18
|
isProviderStateResolved: false,
|
|
16
|
-
composerNodes: [],
|
|
17
|
-
attachments: [],
|
|
18
|
-
draft: '',
|
|
19
|
-
pendingSessionType: 'native',
|
|
20
|
-
pendingProjectRoot: null,
|
|
21
|
-
pendingProjectRootSessionKey: null,
|
|
22
|
-
defaultProjectRoot: null,
|
|
23
|
-
defaultSessionType: 'native',
|
|
24
|
-
canStopGeneration: false,
|
|
25
|
-
stopDisabledReason: null,
|
|
26
|
-
sendError: null,
|
|
27
|
-
isSending: false,
|
|
28
19
|
modelOptions: [],
|
|
29
|
-
selectedModel: '',
|
|
30
|
-
selectedThinkingLevel: null,
|
|
31
|
-
sessionTypeOptions: [],
|
|
32
|
-
selectedSessionType: 'native',
|
|
33
|
-
stopSupported: false,
|
|
34
|
-
stopReason: undefined,
|
|
35
|
-
canEditSessionType: true,
|
|
36
20
|
sessionTypeUnavailable: false,
|
|
37
|
-
sessionTypeUnavailableMessage: null,
|
|
38
|
-
skillRecords: [],
|
|
39
|
-
isSkillsLoading: false,
|
|
40
|
-
selectedSkills: [],
|
|
41
|
-
composerFocusRequest: null,
|
|
42
21
|
...overrides,
|
|
43
22
|
};
|
|
44
23
|
}
|
|
@@ -12,6 +12,7 @@ import type { NcpMessagePart } from '@nextclaw/ncp';
|
|
|
12
12
|
import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
|
|
13
13
|
|
|
14
14
|
const CHAT_SKILL_TOKEN_PREFIX = '$';
|
|
15
|
+
const CHAT_PANEL_APP_TOKEN_PREFIX = '@panel-app:';
|
|
15
16
|
|
|
16
17
|
function appendTextPart(parts: NcpMessagePart[], text: string): NcpMessagePart[] {
|
|
17
18
|
if (text.length === 0) {
|
|
@@ -114,6 +115,11 @@ export function deriveNcpMessagePartsFromComposer(
|
|
|
114
115
|
continue;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
if (node.tokenKind === 'panel_app') {
|
|
119
|
+
parts = appendTextPart(parts, `${CHAT_PANEL_APP_TOKEN_PREFIX}${node.tokenKey}`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
117
123
|
if (node.tokenKind !== 'file') {
|
|
118
124
|
continue;
|
|
119
125
|
}
|
|
@@ -2,13 +2,11 @@ import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
|
|
|
2
2
|
|
|
3
3
|
export const CHAT_UI_INLINE_TOKENS_METADATA_KEY = 'ui_inline_tokens';
|
|
4
4
|
const CHAT_SKILL_TOKEN_PREFIX = '$';
|
|
5
|
+
const CHAT_PANEL_APP_TOKEN_PREFIX = '@panel-app:';
|
|
6
|
+
const CHAT_PANEL_APP_TOKEN_PATTERN = /@panel-app:([A-Za-z0-9_-]+)/g;
|
|
5
7
|
|
|
6
8
|
export type ChatInlineTokenSource = { kind: string; key: string; label: string; rawText: string };
|
|
7
9
|
|
|
8
|
-
export type ChatInlineTextFragment =
|
|
9
|
-
| { type: 'text'; text: string }
|
|
10
|
-
| { type: 'token'; token: ChatInlineTokenSource };
|
|
11
|
-
|
|
12
10
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
13
11
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
12
|
}
|
|
@@ -51,6 +49,30 @@ export function buildInlineSkillTokensFromComposer(nodes: readonly ChatComposerN
|
|
|
51
49
|
return dedupeInlineTokens(tokens);
|
|
52
50
|
}
|
|
53
51
|
|
|
52
|
+
export function buildInlineTokensFromTextProtocol(text: string): ChatInlineTokenSource[] {
|
|
53
|
+
const tokens: ChatInlineTokenSource[] = [];
|
|
54
|
+
for (const match of text.matchAll(CHAT_PANEL_APP_TOKEN_PATTERN)) {
|
|
55
|
+
const key = match[1];
|
|
56
|
+
if (!key) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
tokens.push({
|
|
60
|
+
kind: 'panel_app',
|
|
61
|
+
key,
|
|
62
|
+
label: key,
|
|
63
|
+
rawText: `${CHAT_PANEL_APP_TOKEN_PREFIX}${key}`
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return dedupeInlineTokens(tokens);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function resolveInlineTokensForText(
|
|
70
|
+
text: string,
|
|
71
|
+
tokens: readonly ChatInlineTokenSource[]
|
|
72
|
+
): ChatInlineTokenSource[] {
|
|
73
|
+
return dedupeInlineTokens([...tokens, ...buildInlineTokensFromTextProtocol(text)]);
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
export function readInlineTokensFromMetadata(
|
|
55
77
|
metadata: Record<string, unknown> | undefined
|
|
56
78
|
): ChatInlineTokenSource[] {
|
|
@@ -80,50 +102,3 @@ export function readInlineTokensFromMetadata(
|
|
|
80
102
|
|
|
81
103
|
return dedupeInlineTokens(tokens);
|
|
82
104
|
}
|
|
83
|
-
|
|
84
|
-
export function splitTextByInlineTokens(
|
|
85
|
-
text: string,
|
|
86
|
-
tokens: readonly ChatInlineTokenSource[]
|
|
87
|
-
): ChatInlineTextFragment[] {
|
|
88
|
-
if (text.length === 0 || tokens.length === 0) {
|
|
89
|
-
return text.length === 0 ? [] : [{ type: 'text', text }];
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const orderedTokens = [...tokens].sort((left, right) => right.rawText.length - left.rawText.length);
|
|
93
|
-
const fragments: ChatInlineTextFragment[] = [];
|
|
94
|
-
let cursor = 0;
|
|
95
|
-
|
|
96
|
-
while (cursor < text.length) {
|
|
97
|
-
let matchedToken: ChatInlineTokenSource | null = null;
|
|
98
|
-
for (const token of orderedTokens) {
|
|
99
|
-
if (text.startsWith(token.rawText, cursor)) {
|
|
100
|
-
matchedToken = token;
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (!matchedToken) {
|
|
106
|
-
let nextCursor = cursor + 1;
|
|
107
|
-
while (nextCursor < text.length) {
|
|
108
|
-
if (orderedTokens.some((token) => text.startsWith(token.rawText, nextCursor))) {
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
nextCursor += 1;
|
|
112
|
-
}
|
|
113
|
-
fragments.push({
|
|
114
|
-
type: 'text',
|
|
115
|
-
text: text.slice(cursor, nextCursor)
|
|
116
|
-
});
|
|
117
|
-
cursor = nextCursor;
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
fragments.push({
|
|
122
|
-
type: 'token',
|
|
123
|
-
token: matchedToken
|
|
124
|
-
});
|
|
125
|
-
cursor += matchedToken.rawText.length;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return fragments;
|
|
129
|
-
}
|
|
@@ -178,6 +178,8 @@ export function buildChatSlashItems(
|
|
|
178
178
|
`${texts.slashSkillSpecLabel}: ${record.key}`,
|
|
179
179
|
...(record.scopeLabel ? [`${texts.slashSkillScopeLabel}: ${record.scopeLabel}`] : [])
|
|
180
180
|
],
|
|
181
|
+
tokenKind: 'skill',
|
|
182
|
+
tokenKey: record.key,
|
|
181
183
|
value: record.key
|
|
182
184
|
}));
|
|
183
185
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
>;
|
|
1
|
+
type ChatInputAvailabilitySnapshot = {
|
|
2
|
+
readonly isProviderStateResolved: boolean;
|
|
3
|
+
readonly modelOptions: readonly unknown[];
|
|
4
|
+
readonly sessionTypeUnavailable?: boolean;
|
|
5
|
+
};
|
|
7
6
|
|
|
8
7
|
export function hasNcpChatModelOptions(
|
|
9
8
|
snapshot: ChatInputAvailabilitySnapshot
|
package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render } from "@testing-library/react";
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
2
|
import type { NcpMessage } from "@nextclaw/ncp";
|
|
3
3
|
import { beforeEach, expect, it, vi } from "vitest";
|
|
4
4
|
import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
|
|
@@ -129,7 +129,7 @@ it("keeps historical adapted message references stable when only the streaming m
|
|
|
129
129
|
expect(secondMessages[1]).not.toBe(firstMessages[1]);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
it("adapts persisted inline token metadata into
|
|
132
|
+
it("adapts persisted inline token metadata into markdown token data", () => {
|
|
133
133
|
const message = {
|
|
134
134
|
id: "user-inline-token",
|
|
135
135
|
sessionId: "session-1",
|
|
@@ -156,24 +156,59 @@ it("adapts persisted inline token metadata into rich message parts", () => {
|
|
|
156
156
|
expect(renderedMessages[0]).toMatchObject({
|
|
157
157
|
parts: [
|
|
158
158
|
{
|
|
159
|
-
type: "
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
type: "markdown",
|
|
160
|
+
text: "please use $weather now",
|
|
161
|
+
inlineTokens: [
|
|
162
162
|
{
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
label: "Weather",
|
|
168
|
-
},
|
|
163
|
+
kind: "skill",
|
|
164
|
+
key: "weather",
|
|
165
|
+
label: "Weather",
|
|
166
|
+
rawText: "$weather",
|
|
169
167
|
},
|
|
170
|
-
{ type: "markdown", text: " now" },
|
|
171
168
|
],
|
|
172
169
|
},
|
|
173
170
|
],
|
|
174
171
|
});
|
|
175
172
|
});
|
|
176
173
|
|
|
174
|
+
it("renders context inheritance as a divider without repeating inherited messages", () => {
|
|
175
|
+
const inheritedMessage = {
|
|
176
|
+
id: "child-session:inherited:1",
|
|
177
|
+
sessionId: "child-session",
|
|
178
|
+
role: "user",
|
|
179
|
+
status: "final",
|
|
180
|
+
timestamp: "2026-03-31T10:00:00.000Z",
|
|
181
|
+
metadata: {
|
|
182
|
+
inherited_from_session_id: "parent-session",
|
|
183
|
+
inherited_from_message_id: "parent-message-1",
|
|
184
|
+
},
|
|
185
|
+
parts: [{ type: "text", text: "parent context" }],
|
|
186
|
+
} satisfies NcpMessage;
|
|
187
|
+
const childMessage = {
|
|
188
|
+
id: "child-message-1",
|
|
189
|
+
sessionId: "child-session",
|
|
190
|
+
role: "user",
|
|
191
|
+
status: "final",
|
|
192
|
+
timestamp: "2026-03-31T10:00:01.000Z",
|
|
193
|
+
parts: [{ type: "text", text: "child visible request" }],
|
|
194
|
+
} satisfies NcpMessage;
|
|
195
|
+
|
|
196
|
+
const { container } = render(
|
|
197
|
+
<ChatMessageListContainer
|
|
198
|
+
messages={[inheritedMessage, childMessage]}
|
|
199
|
+
isSending={false}
|
|
200
|
+
/>,
|
|
201
|
+
);
|
|
202
|
+
const renderedMessages = captures.renders.flatMap((rendered) => rendered.messages);
|
|
203
|
+
const renderedPayload = JSON.stringify(renderedMessages);
|
|
204
|
+
|
|
205
|
+
expect(screen.getByText("chatContextInheritanceInherited")).toBeTruthy();
|
|
206
|
+
expect(container.querySelector('[title*="parent-session"]')).toBeTruthy();
|
|
207
|
+
expect(container.querySelector('[title*="chatContextInheritanceMessages: 1"]')).toBeTruthy();
|
|
208
|
+
expect(renderedPayload).toContain("child visible request");
|
|
209
|
+
expect(renderedPayload).not.toContain("parent context");
|
|
210
|
+
});
|
|
211
|
+
|
|
177
212
|
it("keeps Hermes tool invocation parts as tool cards instead of flattening them into plain text", () => {
|
|
178
213
|
const message = {
|
|
179
214
|
id: "assistant-hermes-tool-1",
|
|
@@ -27,11 +27,22 @@ type ChatMessageListContainerProps = {
|
|
|
27
27
|
className?: string;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
const INHERITED_FROM_SESSION_METADATA_KEY = "inherited_from_session_id";
|
|
31
|
+
|
|
30
32
|
const messageViewModelCache = new WeakMap<
|
|
31
33
|
NcpMessage,
|
|
32
34
|
{ language: string; viewModel: ChatMessageViewModel }
|
|
33
35
|
>();
|
|
34
36
|
|
|
37
|
+
type ContextInheritanceTimelineView = {
|
|
38
|
+
sourceSessionId: string;
|
|
39
|
+
inheritedMessageCount: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type ContextInheritanceTimelineBoundary = ContextInheritanceTimelineView & {
|
|
43
|
+
boundaryIndex: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
35
46
|
type ChatTimelineItem =
|
|
36
47
|
| {
|
|
37
48
|
kind: "messages";
|
|
@@ -42,6 +53,11 @@ type ChatTimelineItem =
|
|
|
42
53
|
kind: "compaction";
|
|
43
54
|
key: string;
|
|
44
55
|
checkpoint: ContextCompactionTimelineView;
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
kind: "context-inheritance";
|
|
59
|
+
key: string;
|
|
60
|
+
inheritance: ContextInheritanceTimelineView;
|
|
45
61
|
};
|
|
46
62
|
|
|
47
63
|
function buildChatMessageAdapterTexts(
|
|
@@ -129,35 +145,81 @@ function ChatContextCompactionDivider({
|
|
|
129
145
|
);
|
|
130
146
|
}
|
|
131
147
|
|
|
148
|
+
function ChatContextInheritanceDivider({
|
|
149
|
+
inheritance,
|
|
150
|
+
}: {
|
|
151
|
+
inheritance: ContextInheritanceTimelineView;
|
|
152
|
+
}) {
|
|
153
|
+
const title = [
|
|
154
|
+
`${t("chatContextInheritanceSourceSession")}: ${inheritance.sourceSessionId}`,
|
|
155
|
+
`${t("chatContextInheritanceMessages")}: ${inheritance.inheritedMessageCount}`,
|
|
156
|
+
].join("\n");
|
|
157
|
+
return (
|
|
158
|
+
<div className="my-4 flex items-center gap-3 text-[11px] text-emerald-700" title={title}>
|
|
159
|
+
<div className="h-px flex-1 bg-emerald-100" />
|
|
160
|
+
<div className="inline-flex items-center gap-2 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1">
|
|
161
|
+
<span className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
|
|
162
|
+
<span>{t("chatContextInheritanceInherited")}</span>
|
|
163
|
+
</div>
|
|
164
|
+
<div className="h-px flex-1 bg-emerald-100" />
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
132
169
|
function resolveCompactionBoundaryIndex(params: {
|
|
133
170
|
rawMessages: readonly NcpMessage[];
|
|
134
|
-
|
|
171
|
+
visibleRawMessages: readonly NcpMessage[];
|
|
135
172
|
rawMessageId: string;
|
|
136
173
|
}): number {
|
|
137
174
|
const {
|
|
138
|
-
normalRawMessages,
|
|
139
175
|
rawMessageId,
|
|
140
176
|
rawMessages,
|
|
177
|
+
visibleRawMessages,
|
|
141
178
|
} = params;
|
|
142
179
|
const physicalIndex = rawMessages.findIndex(
|
|
143
180
|
(message) => message.id === rawMessageId,
|
|
144
181
|
);
|
|
145
182
|
if (physicalIndex < 0) {
|
|
146
|
-
return
|
|
183
|
+
return visibleRawMessages.length - 1;
|
|
147
184
|
}
|
|
148
|
-
const
|
|
185
|
+
const previousVisibleCount = rawMessages
|
|
149
186
|
.slice(0, physicalIndex)
|
|
150
|
-
.filter(
|
|
151
|
-
return
|
|
187
|
+
.filter(isVisibleChatMessage).length;
|
|
188
|
+
return previousVisibleCount - 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function readInheritedSourceSessionId(message: NcpMessage): string | null {
|
|
192
|
+
const value = message.metadata?.[INHERITED_FROM_SESSION_METADATA_KEY];
|
|
193
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isVisibleChatMessage(message: NcpMessage): boolean {
|
|
197
|
+
return !readContextCompactionTimeline(message) && !readInheritedSourceSessionId(message);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveContextInheritanceBoundary(
|
|
201
|
+
messages: readonly NcpMessage[],
|
|
202
|
+
): ContextInheritanceTimelineBoundary | null {
|
|
203
|
+
const boundaryIndex = messages.findIndex((message) => readInheritedSourceSessionId(message));
|
|
204
|
+
if (boundaryIndex < 0) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const sourceSessionId = readInheritedSourceSessionId(messages[boundaryIndex]);
|
|
208
|
+
if (!sourceSessionId) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
boundaryIndex: messages.slice(0, boundaryIndex).filter(isVisibleChatMessage).length,
|
|
213
|
+
sourceSessionId,
|
|
214
|
+
inheritedMessageCount: messages.filter((message) => readInheritedSourceSessionId(message) === sourceSessionId).length,
|
|
215
|
+
};
|
|
152
216
|
}
|
|
153
217
|
|
|
154
218
|
function buildTimelineItems(params: {
|
|
155
219
|
rawMessages: readonly NcpMessage[];
|
|
156
220
|
messages: ChatMessageViewModel[];
|
|
157
221
|
}): ChatTimelineItem[] {
|
|
158
|
-
const
|
|
159
|
-
(message) => !readContextCompactionTimeline(message),
|
|
160
|
-
);
|
|
222
|
+
const visibleRawMessages = params.rawMessages.filter(isVisibleChatMessage);
|
|
161
223
|
const checkpoints = params.rawMessages
|
|
162
224
|
.map((message) => ({
|
|
163
225
|
rawMessageId: message.id,
|
|
@@ -172,11 +234,12 @@ function buildTimelineItems(params: {
|
|
|
172
234
|
checkpoint: entry.checkpoint,
|
|
173
235
|
boundaryIndex: resolveCompactionBoundaryIndex({
|
|
174
236
|
rawMessages: params.rawMessages,
|
|
175
|
-
|
|
237
|
+
visibleRawMessages,
|
|
176
238
|
rawMessageId: entry.rawMessageId,
|
|
177
239
|
}),
|
|
178
240
|
}))
|
|
179
241
|
.sort((left, right) => left.boundaryIndex - right.boundaryIndex);
|
|
242
|
+
const contextInheritance = resolveContextInheritanceBoundary(params.rawMessages);
|
|
180
243
|
|
|
181
244
|
const items: ChatTimelineItem[] = [];
|
|
182
245
|
let pendingMessages: ChatMessageViewModel[] = [];
|
|
@@ -193,7 +256,15 @@ function buildTimelineItems(params: {
|
|
|
193
256
|
pendingMessages = [];
|
|
194
257
|
};
|
|
195
258
|
|
|
196
|
-
|
|
259
|
+
visibleRawMessages.forEach((rawMessage, index) => {
|
|
260
|
+
if (contextInheritance?.boundaryIndex === index) {
|
|
261
|
+
flushPendingMessages("messages-before-context-inheritance");
|
|
262
|
+
items.push({
|
|
263
|
+
kind: "context-inheritance",
|
|
264
|
+
key: "context-inheritance",
|
|
265
|
+
inheritance: contextInheritance,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
197
268
|
const message = params.messages[index];
|
|
198
269
|
if (message) {
|
|
199
270
|
pendingMessages.push(message);
|
|
@@ -209,6 +280,14 @@ function buildTimelineItems(params: {
|
|
|
209
280
|
checkpointCursor += 1;
|
|
210
281
|
}
|
|
211
282
|
});
|
|
283
|
+
if (contextInheritance?.boundaryIndex === visibleRawMessages.length) {
|
|
284
|
+
flushPendingMessages("messages-before-context-inheritance");
|
|
285
|
+
items.push({
|
|
286
|
+
kind: "context-inheritance",
|
|
287
|
+
key: "context-inheritance",
|
|
288
|
+
inheritance: contextInheritance,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
212
291
|
while (checkpointCursor < checkpoints.length) {
|
|
213
292
|
const currentCheckpoint = checkpoints[checkpointCursor];
|
|
214
293
|
flushPendingMessages(`messages-before-${currentCheckpoint.key}`);
|
|
@@ -244,7 +323,7 @@ export function ChatMessageListContainer({
|
|
|
244
323
|
|
|
245
324
|
const messages = useMemo(() => {
|
|
246
325
|
return rawMessages.flatMap((message) => {
|
|
247
|
-
if (
|
|
326
|
+
if (!isVisibleChatMessage(message)) {
|
|
248
327
|
return [];
|
|
249
328
|
}
|
|
250
329
|
const cached = messageViewModelCache.get(message);
|
|
@@ -295,6 +374,8 @@ export function ChatMessageListContainer({
|
|
|
295
374
|
{timelineItems.map((item, index) =>
|
|
296
375
|
item.kind === "compaction" ? (
|
|
297
376
|
<ChatContextCompactionDivider key={item.key} checkpoint={item.checkpoint} />
|
|
377
|
+
) : item.kind === "context-inheritance" ? (
|
|
378
|
+
<ChatContextInheritanceDivider key={item.key} inheritance={item.inheritance} />
|
|
298
379
|
) : (
|
|
299
380
|
<ChatMessageList
|
|
300
381
|
key={item.key}
|
|
@@ -432,7 +432,7 @@ it("maps file parts into previewable attachment view models", () => {
|
|
|
432
432
|
});
|
|
433
433
|
});
|
|
434
434
|
|
|
435
|
-
it("
|
|
435
|
+
it("attaches inline skill tokens to markdown parts", () => {
|
|
436
436
|
const adapted = adapt([
|
|
437
437
|
{
|
|
438
438
|
id: "user-inline-skill",
|
|
@@ -452,19 +452,38 @@ it("renders inline skill tokens as structured inline content parts", () => {
|
|
|
452
452
|
] as unknown as ChatMessageSource[]);
|
|
453
453
|
|
|
454
454
|
expect(adapted[0]?.parts[0]).toEqual({
|
|
455
|
-
type: "
|
|
456
|
-
|
|
457
|
-
|
|
455
|
+
type: "markdown",
|
|
456
|
+
text: "please use $weather now",
|
|
457
|
+
inlineTokens: [
|
|
458
458
|
{
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
459
|
+
kind: "skill",
|
|
460
|
+
key: "weather",
|
|
461
|
+
label: "Weather",
|
|
462
|
+
rawText: "$weather",
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("attaches inline panel app text protocol tokens without metadata", () => {
|
|
469
|
+
const adapted = adapt([
|
|
470
|
+
{
|
|
471
|
+
id: "user-inline-panel-app",
|
|
472
|
+
role: "user",
|
|
473
|
+
parts: [{ type: "text", text: "review @panel-app:task-board now" }],
|
|
474
|
+
},
|
|
475
|
+
] as unknown as ChatMessageSource[]);
|
|
476
|
+
|
|
477
|
+
expect(adapted[0]?.parts[0]).toEqual({
|
|
478
|
+
type: "markdown",
|
|
479
|
+
text: "review @panel-app:task-board now",
|
|
480
|
+
inlineTokens: [
|
|
481
|
+
{
|
|
482
|
+
kind: "panel_app",
|
|
483
|
+
key: "task-board",
|
|
484
|
+
label: "task-board",
|
|
485
|
+
rawText: "@panel-app:task-board",
|
|
466
486
|
},
|
|
467
|
-
{ type: "markdown", text: " now" },
|
|
468
487
|
],
|
|
469
488
|
});
|
|
470
489
|
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveInlineTokensForText,
|
|
3
|
+
type ChatInlineTokenSource,
|
|
4
|
+
} from "@/features/chat/features/input/utils/chat-inline-token.utils";
|
|
5
|
+
import type { ChatMessagePartViewModel } from "@nextclaw/agent-chat-ui";
|
|
6
|
+
import type { ChatMessagePartSource } from "./chat-message-part.utils";
|
|
7
|
+
|
|
8
|
+
const INVISIBLE_ONLY_TEXT_PATTERN = /\u200B|\u200C|\u200D|\u2060|\uFEFF/g;
|
|
9
|
+
|
|
10
|
+
function toRenderableText(value: string): string | null {
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
if (!trimmed) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const visible = trimmed.replace(INVISIBLE_ONLY_TEXT_PATTERN, "").trim();
|
|
16
|
+
return visible ? trimmed : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildTextPart(
|
|
20
|
+
part: Extract<ChatMessagePartSource, { type: "text" }>,
|
|
21
|
+
inlineTokens: readonly ChatInlineTokenSource[],
|
|
22
|
+
): Extract<ChatMessagePartViewModel, { type: "markdown" }> | null {
|
|
23
|
+
const text = toRenderableText(part.text);
|
|
24
|
+
if (!text) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const resolvedInlineTokens = resolveInlineTokensForText(part.text, inlineTokens);
|
|
28
|
+
return {
|
|
29
|
+
type: "markdown",
|
|
30
|
+
text,
|
|
31
|
+
inlineTokens:
|
|
32
|
+
resolvedInlineTokens.length > 0 ? resolvedInlineTokens : undefined,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildRenderableText(value: string): string | null {
|
|
37
|
+
return toRenderableText(value);
|
|
38
|
+
}
|
|
@@ -6,7 +6,7 @@ import { type ChatInlineTokenSource } from "@/features/chat/features/input/utils
|
|
|
6
6
|
import {
|
|
7
7
|
buildRenderableText,
|
|
8
8
|
buildTextPart,
|
|
9
|
-
} from "./chat-message-
|
|
9
|
+
} from "./chat-message-markdown-part.utils";
|
|
10
10
|
import {
|
|
11
11
|
buildToolCard,
|
|
12
12
|
buildToolInvocationInput,
|
|
@@ -3,14 +3,14 @@ import { Plus } from 'lucide-react';
|
|
|
3
3
|
import { ChatSessionTypeMenu } from "@/features/chat/features/session-type/components/chat-session-type-menu";
|
|
4
4
|
import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
|
|
5
5
|
import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
|
|
6
|
-
import type {
|
|
6
|
+
import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
|
|
7
7
|
import type { NcpSessionListItemView } from '@/features/chat/features/ncp/hooks/use-ncp-session-list-view';
|
|
8
8
|
import type { ChatSidebarProjectGroup } from '@/features/chat/features/session/utils/chat-sidebar-session-groups.utils';
|
|
9
9
|
import { t } from '@/shared/lib/i18n';
|
|
10
10
|
|
|
11
11
|
export type { ChatSidebarProjectGroup };
|
|
12
12
|
|
|
13
|
-
type SessionTypeOption =
|
|
13
|
+
type SessionTypeOption = ChatSessionTypeOption;
|
|
14
14
|
|
|
15
15
|
type ChatSidebarProjectGroupsProps = {
|
|
16
16
|
groups: ChatSidebarProjectGroup[];
|
|
@@ -6,10 +6,10 @@ import type {
|
|
|
6
6
|
ChatSidebarProjectGroup,
|
|
7
7
|
} from "@/features/chat/features/session/utils/chat-sidebar-session-groups.utils";
|
|
8
8
|
import { ChatSidebarProjectGroups } from "@/features/chat/features/session/components/chat-sidebar-project-groups";
|
|
9
|
-
import type {
|
|
9
|
+
import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
|
|
10
10
|
import { t } from "@/shared/lib/i18n";
|
|
11
11
|
|
|
12
|
-
type SessionTypeOption =
|
|
12
|
+
type SessionTypeOption = ChatSessionTypeOption;
|
|
13
13
|
|
|
14
14
|
type ChatSidebarSessionListProps = {
|
|
15
15
|
isLoading: boolean;
|