@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.
Files changed (165) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/assets/{api-DPcvCLfa.js → api-a14tR_z1.js} +7 -7
  3. package/dist/assets/channels-list-page-CFGkm4DT.js +8 -0
  4. package/dist/assets/chat-page-CEdavJXc.js +105 -0
  5. package/dist/assets/{config-split-page-BCJhqdK9.js → config-split-page-B5b9B0uE.js} +1 -1
  6. package/dist/assets/{confirm-dialog-CWh5FfK2.js → confirm-dialog-ClH_Ec7n.js} +1 -1
  7. package/dist/assets/{desktop-update-config-DsRhBizi.js → desktop-update-config-DPNDnVpk.js} +1 -1
  8. package/dist/assets/{dist-DPrgcEC0.js → dist-BtCS2Ucq.js} +1 -1
  9. package/dist/assets/{dist-DsE4OsSS.js → dist-CLGGLm8b.js} +1 -1
  10. package/dist/assets/doc-browser-5nMWYKBD.js +1 -0
  11. package/dist/assets/doc-browser-BJRLhztv.js +1 -0
  12. package/dist/assets/doc-browser-context-D4JwMMxK.js +1 -0
  13. package/dist/assets/{ellipsis-B81FStLm.js → ellipsis-CtJXjIol.js} +1 -1
  14. package/dist/assets/{external-link-C_dmG-WJ.js → external-link-CdJiG13p.js} +1 -1
  15. package/dist/assets/index-BDN1zzMQ.js +105 -0
  16. package/dist/assets/index-g3uSZXoc.css +1 -0
  17. package/dist/assets/mcp-marketplace-page-BU0kIjhR.js +40 -0
  18. package/dist/assets/mcp-marketplace-page-sMNFSq_T.js +1 -0
  19. package/dist/assets/middleware-CkFT3R4v.js +1 -0
  20. package/dist/assets/model-config-page-D6hYXROd.js +1 -0
  21. package/dist/assets/plus-DPjb0piO.js +1 -0
  22. package/dist/assets/provider-scoped-model-input-DAX2HgmR.js +1 -0
  23. package/dist/assets/providers-config-page-BzAeaACA.js +1 -0
  24. package/dist/assets/{react-sZz5HSCm.js → react-C6cZgP44.js} +1 -1
  25. package/dist/assets/remote-B0G80uPr.js +1 -0
  26. package/dist/assets/rotate-cw-DnEB1Rju.js +1 -0
  27. package/dist/assets/runtime-config-page-C5xKaV3i.js +1 -0
  28. package/dist/assets/{save-MzjoiHri.js → save-DKA0x4zk.js} +1 -1
  29. package/dist/assets/{search-B29dE4GN.js → search-BNlcoj5c.js} +1 -1
  30. package/dist/assets/{search-config-page-Dl5sZzob.js → search-config-page-Cq9uJ-N8.js} +1 -1
  31. package/dist/assets/{secrets-config-page-C_l-IFCK.js → secrets-config-page-CxSbZLBo.js} +2 -2
  32. package/dist/assets/{select-BKmh05hd.js → select-CvTlDZTQ.js} +1 -1
  33. package/dist/assets/{tag-chip-BS_7Ueom.js → tag-chip-8ffF2y2C.js} +1 -1
  34. package/dist/assets/use-config-DmT0o1gv.js +1 -0
  35. package/dist/assets/x-BiSSpgqB.js +1 -0
  36. package/dist/index.html +15 -14
  37. package/package.json +9 -9
  38. package/src/app/components/app-presenter-provider.tsx +11 -2
  39. package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
  40. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
  41. package/src/app/components/layout/app-layout.tsx +18 -4
  42. package/src/app/components/layout/sidebar-items.tsx +194 -71
  43. package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
  44. package/src/app/components/layout/sidebar.tsx +340 -83
  45. package/src/app/managers/viewport-layout.manager.ts +10 -0
  46. package/src/app/stores/viewport-layout.store.ts +39 -3
  47. package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
  48. package/src/features/agents/components/agents-page.tsx +10 -6
  49. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
  50. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
  51. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
  52. package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
  53. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +14 -15
  54. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +18 -18
  55. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
  56. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +16 -14
  58. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
  59. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +80 -7
  60. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +82 -25
  61. package/src/features/chat/components/layout/chat-sidebar.tsx +59 -92
  62. package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
  63. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +289 -0
  64. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +427 -0
  65. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +204 -0
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
  67. package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +32 -17
  68. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
  69. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
  70. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
  71. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
  72. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
  73. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
  74. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
  75. package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
  76. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
  77. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
  78. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
  79. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
  80. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
  81. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
  82. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
  83. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
  84. package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
  85. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
  86. package/src/features/chat/features/message/components/chat-message-list.container.tsx +93 -12
  87. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
  88. package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
  89. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  90. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  91. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  92. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
  93. package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
  94. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
  95. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +2 -2
  96. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +2 -0
  97. package/src/features/chat/features/session-type/components/chat-session-type-menu.tsx +2 -2
  98. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +2 -2
  99. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
  100. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -2
  101. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
  102. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +2 -2
  103. package/src/features/chat/features/welcome/components/chat-welcome.tsx +2 -2
  104. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
  106. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
  107. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
  108. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
  109. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
  110. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
  111. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
  112. package/src/features/chat/index.ts +0 -1
  113. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
  114. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
  115. package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
  116. package/src/features/chat/managers/chat-thread.manager.ts +108 -34
  117. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
  118. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
  119. package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
  120. package/src/features/chat/presenters/chat.presenter.ts +2 -19
  121. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
  122. package/src/features/chat/stores/chat-thread.store.ts +42 -26
  123. package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
  124. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
  125. package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
  126. package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
  127. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
  128. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
  129. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
  130. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  131. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
  132. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  133. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
  134. package/src/shared/lib/ui-document-title/index.ts +50 -21
  135. package/dist/assets/channels-list-page-D6jtziu4.js +0 -8
  136. package/dist/assets/chat-page-BBnNJoKG.js +0 -105
  137. package/dist/assets/doc-browser-C8xOF_9F.js +0 -1
  138. package/dist/assets/doc-browser-DgQedeAn.js +0 -1
  139. package/dist/assets/doc-browser-context-B46tQGmO.js +0 -1
  140. package/dist/assets/index-7o1WdUbU.js +0 -103
  141. package/dist/assets/index-DQWmEWzk.css +0 -1
  142. package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +0 -1
  143. package/dist/assets/mcp-marketplace-page-Dzull_4-.js +0 -40
  144. package/dist/assets/model-config-page-Cv0O3BIj.js +0 -1
  145. package/dist/assets/plus-Bnt56oqG.js +0 -1
  146. package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +0 -1
  147. package/dist/assets/providers-config-page-CPDz0Lxc.js +0 -1
  148. package/dist/assets/remote-B0LMs8u7.js +0 -1
  149. package/dist/assets/rotate-cw-BfyG_KEF.js +0 -1
  150. package/dist/assets/runtime-config-page-q0fvv8ME.js +0 -1
  151. package/dist/assets/use-config-DZkBWIdL.js +0 -1
  152. package/dist/assets/x-CW2KO-Hz.js +0 -1
  153. package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
  154. package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
  155. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
  156. package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
  157. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
  158. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
  159. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
  160. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
  161. package/src/features/chat/managers/chat-input.manager.ts +0 -430
  162. package/src/features/chat/managers/chat-run.manager.ts +0 -110
  163. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
  164. package/src/features/chat/stores/chat-input.store.ts +0 -104
  165. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
@@ -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
- function createSnapshot(
12
- overrides: Partial<ChatInputSnapshot> = {}
13
- ): ChatInputSnapshot {
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
- import type { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
2
-
3
- type ChatInputAvailabilitySnapshot = Pick<
4
- ChatInputSnapshot,
5
- 'isProviderStateResolved' | 'modelOptions' | 'sessionTypeUnavailable'
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
@@ -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 rich message parts", () => {
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: "inline-content",
160
- segments: [
161
- { type: "markdown", text: "please use " },
159
+ type: "markdown",
160
+ text: "please use $weather now",
161
+ inlineTokens: [
162
162
  {
163
- type: "token",
164
- token: {
165
- kind: "skill",
166
- key: "weather",
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
- normalRawMessages: readonly NcpMessage[];
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 normalRawMessages.length - 1;
183
+ return visibleRawMessages.length - 1;
147
184
  }
148
- const previousNormalCount = rawMessages
185
+ const previousVisibleCount = rawMessages
149
186
  .slice(0, physicalIndex)
150
- .filter((message) => !readContextCompactionTimeline(message)).length;
151
- return previousNormalCount - 1;
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 normalRawMessages = params.rawMessages.filter(
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
- normalRawMessages,
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
- normalRawMessages.forEach((rawMessage, index) => {
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 (readContextCompactionTimeline(message)) {
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("renders inline skill tokens as structured inline content parts", () => {
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: "inline-content",
456
- segments: [
457
- { type: "markdown", text: "please use " },
455
+ type: "markdown",
456
+ text: "please use $weather now",
457
+ inlineTokens: [
458
458
  {
459
- type: "token",
460
- token: {
461
- kind: "skill",
462
- key: "weather",
463
- label: "Weather",
464
- rawText: "$weather",
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-inline-content.utils";
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 { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
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 = ChatInputSnapshot['sessionTypeOptions'][number];
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 { ChatInputSnapshot } from "@/features/chat/stores/chat-input.store";
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 = ChatInputSnapshot["sessionTypeOptions"][number];
12
+ type SessionTypeOption = ChatSessionTypeOption;
13
13
 
14
14
  type ChatSidebarSessionListProps = {
15
15
  isLoading: boolean;