@nextclaw/ui 0.14.0 → 0.14.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/assets/{api-B50yVdT8.js → api-C6EDHBDR.js} +1 -1
  3. package/dist/assets/channels-list-page-DKj6m1Hc.js +8 -0
  4. package/dist/assets/chat-page-BXngLJY6.js +105 -0
  5. package/dist/assets/config-split-page-CjlmJy8X.js +1 -0
  6. package/dist/assets/{confirm-dialog-DRSelLdI.js → confirm-dialog-B39X0pYp.js} +1 -1
  7. package/dist/assets/desktop-update-config-DfUQ8nly.js +1 -0
  8. package/dist/assets/{dist-NZRV-BxD.js → dist-BIYV2-Ia.js} +1 -1
  9. package/dist/assets/{dist-D19wiQwB.js → dist-CmMro3nV.js} +1 -1
  10. package/dist/assets/doc-browser-B6Wc_TIY.js +1 -0
  11. package/dist/assets/doc-browser-CDtUtcim.js +1 -0
  12. package/dist/assets/{doc-browser-context-C8KPbj-p.js → doc-browser-context-DZbC9zVB.js} +1 -1
  13. package/dist/assets/{ellipsis-BYy8TP0A.js → ellipsis-DDpgTzgx.js} +1 -1
  14. package/dist/assets/{external-link-528k6MDD.js → external-link-D2cBOCqn.js} +1 -1
  15. package/dist/assets/index-DTb_XVIH.js +103 -0
  16. package/dist/assets/index-ayiPYhf1.css +1 -0
  17. package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +1 -0
  18. package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +40 -0
  19. package/dist/assets/model-config-page-Bcb35AVv.js +1 -0
  20. package/dist/assets/plus-CYE1onU2.js +1 -0
  21. package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +1 -0
  22. package/dist/assets/providers-config-page-CQSibSwO.js +1 -0
  23. package/dist/assets/{react-Dsotw5vM.js → react-DYQXXGbH.js} +1 -1
  24. package/dist/assets/remote-BlJDbXOI.js +1 -0
  25. package/dist/assets/rotate-cw-BWbvFDAX.js +1 -0
  26. package/dist/assets/runtime-config-page-Tn2yLQ0c.js +1 -0
  27. package/dist/assets/{save-CHDnDTok.js → save-BNVTgBVn.js} +1 -1
  28. package/dist/assets/search-CKCzwLId.js +1 -0
  29. package/dist/assets/search-config-page-DG5Q2hF1.js +1 -0
  30. package/dist/assets/{secrets-config-page-95yaH1fa.js → secrets-config-page-BXn5Uuwp.js} +2 -2
  31. package/dist/assets/select-CIRwTNP4.js +41 -0
  32. package/dist/assets/{tag-chip-Cr8GwpRE.js → tag-chip-DKhVFtTE.js} +1 -1
  33. package/dist/assets/{use-config-TzwzTgg0.js → use-config-DgKM9VIU.js} +1 -1
  34. package/dist/assets/x-Cm3JjYut.js +1 -0
  35. package/dist/index.html +14 -27
  36. package/package.json +9 -9
  37. package/src/app/presenters/app.presenter.ts +2 -0
  38. package/src/features/account/components/account-panel.tsx +2 -2
  39. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +1 -0
  40. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +39 -80
  41. package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +118 -0
  42. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +2 -17
  43. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -24
  44. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +0 -27
  45. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +89 -0
  46. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +9 -1
  47. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +115 -0
  48. package/src/features/chat/features/message/components/chat-message-list.container.tsx +4 -0
  49. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +119 -1
  50. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +51 -0
  51. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +28 -5
  52. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +4 -0
  53. package/src/features/chat/features/ncp/hooks/__tests__/use-ui-show-content-event.test.tsx +1 -0
  54. package/src/features/chat/features/ncp/utils/ncp-chat-query-derived.utils.ts +11 -3
  55. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +27 -0
  56. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +19 -1
  57. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +46 -0
  58. package/src/features/chat/features/session/hooks/__tests__/use-selected-session-context-window-indicator.test.tsx +63 -0
  59. package/src/features/chat/features/session/hooks/use-selected-session-context-window-indicator.ts +7 -10
  60. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +8 -0
  61. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +2 -0
  62. package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +1 -0
  63. package/src/features/chat/features/session-type/utils/chat-session-type.utils.ts +4 -0
  64. package/src/features/chat/index.ts +1 -0
  65. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +28 -0
  66. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +129 -0
  67. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +34 -0
  68. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +143 -1
  69. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +30 -0
  70. package/src/features/chat/managers/__tests__/recent-selection.manager.test.ts +37 -14
  71. package/src/features/chat/managers/chat-draft-intent.manager.ts +46 -0
  72. package/src/features/chat/managers/chat-input.manager.ts +13 -13
  73. package/src/features/chat/managers/chat-run.manager.ts +3 -0
  74. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +19 -2
  75. package/src/features/chat/managers/chat-thread.manager.ts +4 -0
  76. package/src/features/chat/managers/chat-ui.manager.ts +2 -6
  77. package/src/features/chat/managers/recent-selection.manager.ts +21 -67
  78. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +47 -1
  79. package/src/features/chat/pages/ncp-chat-page.tsx +18 -0
  80. package/src/features/chat/stores/chat-input.store.ts +7 -1
  81. package/src/features/chat/stores/recent-selection.store.ts +85 -0
  82. package/src/features/chat/types/chat-input-bar.types.ts +0 -1
  83. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +137 -0
  84. package/src/features/panel-apps/components/panel-apps-list.tsx +119 -5
  85. package/src/features/panel-apps/index.ts +2 -0
  86. package/src/features/panel-apps/utils/panel-app-entry-match.utils.ts +17 -0
  87. package/src/shared/lib/api/chat-session-type.types.ts +7 -0
  88. package/src/shared/lib/api/ncp-session.types.ts +1 -0
  89. package/src/shared/lib/api/types.ts +3 -0
  90. package/src/shared/lib/i18n/locales/en-US/chat.json +4 -0
  91. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +10 -0
  92. package/src/shared/lib/i18n/locales/zh-CN/chat.json +4 -0
  93. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +10 -0
  94. package/dist/assets/app-presenter-provider-BI-ewLbO.js +0 -3
  95. package/dist/assets/arrow-left-BOCJyLbQ.js +0 -1
  96. package/dist/assets/book-open-Bd2Xbrrc.js +0 -1
  97. package/dist/assets/channels-list-page-moJkpxtU.js +0 -8
  98. package/dist/assets/chat-page-CO27cl4v.js +0 -105
  99. package/dist/assets/config-split-page-CsQ2rIdq.js +0 -1
  100. package/dist/assets/desktop-update-config-qa-SynOc.js +0 -1
  101. package/dist/assets/doc-browser-2ddUJCA0.js +0 -1
  102. package/dist/assets/doc-browser-BjHUczQb.js +0 -1
  103. package/dist/assets/es2015-BUIIKCNh.js +0 -41
  104. package/dist/assets/host-capabilities-9bB_998Z.js +0 -1
  105. package/dist/assets/index-CMNrRhnG.js +0 -101
  106. package/dist/assets/index-UTmTW1eU.css +0 -1
  107. package/dist/assets/key-round-D3vD9dj7.js +0 -1
  108. package/dist/assets/loader-circle-DV9TSRns.js +0 -1
  109. package/dist/assets/mcp-marketplace-page-CS5Lh3oA.js +0 -1
  110. package/dist/assets/mcp-marketplace-page-DEOMcWb0.js +0 -40
  111. package/dist/assets/model-config-page-BSc4o7pb.js +0 -1
  112. package/dist/assets/notice-card-qqlbsOoR.js +0 -1
  113. package/dist/assets/plus-Bf9qhPDO.js +0 -1
  114. package/dist/assets/popover-BnmPOXe3.js +0 -1
  115. package/dist/assets/provider-scoped-model-input-Ca56s475.js +0 -1
  116. package/dist/assets/providers-config-page-D8Kf4U3u.js +0 -1
  117. package/dist/assets/refresh-cw-Bmx4wc9P.js +0 -1
  118. package/dist/assets/remote-DNMEy_c4.js +0 -1
  119. package/dist/assets/rotate-cw-CL0sVBel.js +0 -1
  120. package/dist/assets/runtime-config-page-CUFtKr0U.js +0 -1
  121. package/dist/assets/search-D7L-TCH-.js +0 -1
  122. package/dist/assets/search-config-page-CA3RVAef.js +0 -1
  123. package/dist/assets/status-dot-Dm4h-bjn.js +0 -1
  124. package/dist/assets/tabs-CJvw5Lqp.js +0 -1
  125. package/dist/assets/tooltip-Bxzi6aDL.js +0 -1
  126. package/dist/assets/trash-2-CSfX-BvU.js +0 -1
  127. package/dist/assets/x-fkIWvi07.js +0 -1
  128. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-bar-controller.test.tsx +0 -152
  129. package/src/features/chat/features/input/hooks/use-chat-input-bar-controller.ts +0 -142
  130. /package/dist/assets/{config-hints-Cl2XAhzV.js → config-hints-CTqBnCDp.js} +0 -0
@@ -0,0 +1,118 @@
1
+ import { useMemo } from 'react';
2
+ import type { NcpSessionSummaryView, SessionSkillEntryView } from '@/shared/lib/api';
3
+ import { t } from '@/shared/lib/i18n';
4
+ import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
5
+ import {
6
+ buildNcpChatProviderModelOptions,
7
+ filterNcpChatModelOptionsBySessionType,
8
+ } from '@/features/chat/features/ncp/utils/ncp-chat-query-derived.utils';
9
+ import { useNcpChatProviderStateResolved } from '@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state';
10
+ import { useChatSessionTypeState } from '@/features/chat/features/session-type/hooks/use-chat-session-type-state';
11
+ import { adaptNcpSessionSummaries } from '@/features/chat/features/session/utils/ncp-session-adapter.utils';
12
+ import { resolveRecentSessionPreferredValue } from '@/features/chat/features/session/utils/chat-session-preference-governance.utils';
13
+ import { chatRecentModelsManager } from '@/features/chat/managers/chat-recent-models.manager';
14
+ import type { ChatInputSnapshot } from '@/features/chat/stores/chat-input.store';
15
+ import { useChatQueryStore } from '@/features/chat/stores/ncp-chat-query.store';
16
+ import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
17
+
18
+ const EMPTY_SESSION_SKILL_RECORDS: SessionSkillEntryView[] = [];
19
+ const EMPTY_NCP_SESSION_SUMMARIES: NcpSessionSummaryView[] = [];
20
+
21
+ export function useChatInputBarQueryState(snapshot: ChatInputSnapshot) {
22
+ const presenter = usePresenter();
23
+ const selectedSessionKey = useChatSessionListStore(
24
+ (state) => state.snapshot.selectedSessionKey,
25
+ );
26
+ const isProviderStateResolved = useNcpChatProviderStateResolved();
27
+ const querySnapshot = useChatQueryStore((state) => state.snapshot);
28
+ const config = querySnapshot.configQuery?.data ?? null;
29
+ const sessionSummaries =
30
+ querySnapshot.sessionsQuery?.data?.sessions ?? EMPTY_NCP_SESSION_SUMMARIES;
31
+ const skillRecords =
32
+ querySnapshot.sessionSkillsQuery?.data?.records ?? EMPTY_SESSION_SKILL_RECORDS;
33
+ const isSkillsLoading = Boolean(
34
+ querySnapshot.sessionSkillsQuery?.isLoading ||
35
+ querySnapshot.sessionSkillsQuery?.isFetching,
36
+ );
37
+ const sessions = useMemo(
38
+ () => adaptNcpSessionSummaries(sessionSummaries),
39
+ [sessionSummaries],
40
+ );
41
+ const selectedSession = useMemo(
42
+ () => sessions.find((session) => session.key === selectedSessionKey) ?? null,
43
+ [selectedSessionKey, sessions],
44
+ );
45
+ const sessionTypeState = useChatSessionTypeState({
46
+ selectedSession,
47
+ pendingSessionType: snapshot.pendingSessionType,
48
+ setPendingSessionType: presenter.chatInputManager.setPendingSessionType,
49
+ sessionTypesData: querySnapshot.sessionTypesQuery?.data ?? null,
50
+ });
51
+ const providerModelOptions = useMemo(
52
+ () =>
53
+ buildNcpChatProviderModelOptions({
54
+ config,
55
+ providersView: querySnapshot.providersQuery?.data ?? null,
56
+ templatesView: querySnapshot.providerTemplatesQuery?.data ?? null,
57
+ }),
58
+ [config, querySnapshot.providerTemplatesQuery?.data, querySnapshot.providersQuery?.data],
59
+ );
60
+ const modelOptions = useMemo(
61
+ () =>
62
+ filterNcpChatModelOptionsBySessionType({
63
+ modelOptions: providerModelOptions,
64
+ modelSelectionMode: sessionTypeState.selectedSessionTypeOption?.modelSelectionMode,
65
+ runtimeDefaultThinkingCapability:
66
+ sessionTypeState.selectedSessionTypeOption?.runtimeDefaultThinking ?? null,
67
+ runtimeDefaultModelLabel: t('chatRuntimeDefaultModel'),
68
+ supportedModels: sessionTypeState.selectedSessionTypeOption?.supportedModels,
69
+ }),
70
+ [
71
+ providerModelOptions,
72
+ sessionTypeState.selectedSessionTypeOption?.modelSelectionMode,
73
+ sessionTypeState.selectedSessionTypeOption?.runtimeDefaultThinking,
74
+ sessionTypeState.selectedSessionTypeOption?.supportedModels,
75
+ ],
76
+ );
77
+ const availableModelValueSet = useMemo(
78
+ () => new Set(modelOptions.map((option) => option.value)),
79
+ [modelOptions],
80
+ );
81
+ const recentSessionTypeModel = chatRecentModelsManager
82
+ .read({ namespace: sessionTypeState.selectedSessionType })
83
+ .find((value) => availableModelValueSet.has(value));
84
+ const fallbackPreferredModel = useMemo(
85
+ () =>
86
+ recentSessionTypeModel ??
87
+ resolveRecentSessionPreferredValue<string>({
88
+ sessions,
89
+ selectedSessionKey,
90
+ sessionType: sessionTypeState.selectedSessionType,
91
+ readPreference: (session) => session.preferredModel?.trim() || undefined,
92
+ }),
93
+ [recentSessionTypeModel, selectedSessionKey, sessionTypeState.selectedSessionType, sessions],
94
+ );
95
+ const fallbackPreferredThinking = useMemo(
96
+ () =>
97
+ resolveRecentSessionPreferredValue({
98
+ sessions,
99
+ selectedSessionKey,
100
+ sessionType: sessionTypeState.selectedSessionType,
101
+ readPreference: (session) => session.preferredThinking ?? undefined,
102
+ }),
103
+ [selectedSessionKey, sessionTypeState.selectedSessionType, sessions],
104
+ );
105
+
106
+ return {
107
+ defaultModel: config?.agents.defaults.model,
108
+ fallbackPreferredModel,
109
+ fallbackPreferredThinking,
110
+ isProviderStateResolved,
111
+ isSkillsLoading,
112
+ modelOptions,
113
+ selectedSession,
114
+ selectedSessionKey,
115
+ sessionTypeState,
116
+ skillRecords,
117
+ };
118
+ }
@@ -1,10 +1,9 @@
1
1
  import {
2
2
  buildChatSlashItems,
3
3
  buildModelToolbarSelect,
4
- buildSelectedSkillItems,
5
4
  buildSkillPickerModel
6
- } from '../chat-input-bar.utils';
7
- import type { ChatSkillRecord } from '../chat-input-bar.utils';
5
+ } from '@/features/chat/features/input/utils/chat-input-bar.utils';
6
+ import type { ChatSkillRecord } from '@/features/chat/features/input/utils/chat-input-bar.utils';
8
7
 
9
8
  function createSkillRecord(partial: Partial<ChatSkillRecord>): ChatSkillRecord {
10
9
  return {
@@ -72,20 +71,6 @@ describe('buildChatSlashItems', () => {
72
71
  });
73
72
  });
74
73
 
75
- describe('buildSelectedSkillItems', () => {
76
- it('keeps selected specs and resolves labels when available', () => {
77
- const chips = buildSelectedSkillItems(
78
- ['web-search', 'missing-skill'],
79
- [createSkillRecord({ key: 'web-search', label: 'Web Search' })]
80
- );
81
-
82
- expect(chips).toEqual([
83
- { key: 'web-search', label: 'Web Search' },
84
- { key: 'missing-skill', label: 'missing-skill' }
85
- ]);
86
- });
87
- });
88
-
89
74
  describe('buildSkillPickerModel', () => {
90
75
  it('builds a stable semantic model for toolbar skill picker', () => {
91
76
  const onSelectedKeysChange = vi.fn();
@@ -1,5 +1,4 @@
1
1
  import type {
2
- ChatSelectedItem,
3
2
  ChatSkillPickerOption,
4
3
  ChatSkillPickerProps,
5
4
  ChatSlashItem,
@@ -7,7 +6,6 @@ import type {
7
6
  export {
8
7
  buildModelStateHint,
9
8
  buildModelToolbarSelect,
10
- buildSessionTypeToolbarSelect,
11
9
  buildThinkingToolbarSelect,
12
10
  } from "./chat-input-toolbar.utils";
13
11
  import type {
@@ -35,14 +33,6 @@ const SLASH_ITEM_MATCH_SCORE = {
35
33
  fallback: 1
36
34
  } as const;
37
35
 
38
- export function resolveSlashQuery(draft: string): string | null {
39
- const match = /^\/([^\s]*)$/.exec(draft);
40
- if (!match) {
41
- return null;
42
- }
43
- return (match[1] ?? '').trim().toLowerCase();
44
- }
45
-
46
36
  function normalizeSearchText(value: string | null | undefined): string {
47
37
  return (value ?? '').trim().toLowerCase();
48
38
  }
@@ -192,20 +182,7 @@ export function buildChatSlashItems(
192
182
  }));
193
183
  }
194
184
 
195
- export function buildSelectedSkillItems(
196
- selectedSkills: string[],
197
- skillRecords: ChatSkillRecord[]
198
- ): ChatSelectedItem[] {
199
- return selectedSkills.map((spec) => {
200
- const matched = skillRecords.find((record) => record.key === spec);
201
- return {
202
- key: spec,
203
- label: matched?.label || spec
204
- };
205
- });
206
- }
207
-
208
- export function buildSkillPickerOptions(skillRecords: ChatSkillRecord[]): ChatSkillPickerOption[] {
185
+ function buildSkillPickerOptions(skillRecords: ChatSkillRecord[]): ChatSkillPickerOption[] {
209
186
  return skillRecords.map((record) => ({
210
187
  key: record.key,
211
188
  label: record.label,
@@ -128,33 +128,6 @@ export function buildModelToolbarSelect({
128
128
  };
129
129
  }
130
130
 
131
- export function buildSessionTypeToolbarSelect(params: {
132
- selectedSessionType?: string;
133
- selectedSessionTypeOption: { value: string; label: string } | null;
134
- sessionTypeOptions: Array<{ value: string; label: string }>;
135
- onValueChange: (value: string) => void;
136
- canEditSessionType: boolean;
137
- shouldShow: boolean;
138
- texts: Pick<ChatInputBarAdapterTexts, "sessionTypePlaceholder">;
139
- }): ChatToolbarSelect | null {
140
- if (!params.shouldShow) {
141
- return null;
142
- }
143
-
144
- return {
145
- key: "session-type",
146
- value: params.selectedSessionType,
147
- placeholder: params.texts.sessionTypePlaceholder,
148
- selectedLabel: params.selectedSessionTypeOption?.label,
149
- options: params.sessionTypeOptions.map((option) => ({
150
- value: option.value,
151
- label: option.label,
152
- })),
153
- disabled: !params.canEditSessionType,
154
- onValueChange: params.onValueChange,
155
- };
156
- }
157
-
158
131
  export function buildThinkingToolbarSelect(params: {
159
132
  supportedLevels: ChatThinkingLevel[];
160
133
  selectedThinkingLevel: ChatThinkingLevel | null;
@@ -0,0 +1,89 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import { beforeEach, expect, it, vi } from 'vitest';
3
+ import { ChatInlinePanelAppCard } from '@/features/chat/features/message/components/chat-inline-panel-app-card';
4
+
5
+ const mocks = vi.hoisted(() => ({
6
+ handleIframeMessage: vi.fn(),
7
+ open: vi.fn(),
8
+ openTarget: vi.fn(),
9
+ panelApps: {
10
+ data: {
11
+ entries: [
12
+ {
13
+ appId: 'weather-card',
14
+ contentPath: '/api/panel-apps/weather-card/content',
15
+ fileName: 'weather-card.panel.html',
16
+ icon: '*',
17
+ id: 'weather-card',
18
+ title: 'Weather',
19
+ },
20
+ ],
21
+ },
22
+ isLoading: false,
23
+ },
24
+ }));
25
+
26
+ vi.mock('@/app/presenters/app.presenter', () => ({
27
+ getPresenter: () => ({
28
+ panelAppBridgeManager: {
29
+ handleIframeMessage: mocks.handleIframeMessage,
30
+ },
31
+ }),
32
+ }));
33
+
34
+ vi.mock('@/features/panel-apps', () => ({
35
+ findPanelAppEntryByDisplayId: (entries: Array<{ appId: string; id: string; title: string }>, value: string) =>
36
+ entries.find((entry) => [entry.appId, entry.id, entry.title].includes(value)) ?? null,
37
+ usePanelApps: () => mocks.panelApps,
38
+ }));
39
+
40
+ vi.mock('@/shared/components/doc-browser', () => ({
41
+ useDocBrowser: () => ({
42
+ open: mocks.open,
43
+ openTarget: mocks.openTarget,
44
+ }),
45
+ }));
46
+
47
+ vi.mock('@/shared/lib/i18n', () => ({
48
+ t: (key: string) => key,
49
+ }));
50
+
51
+ beforeEach(() => {
52
+ mocks.handleIframeMessage.mockReset();
53
+ mocks.open.mockReset();
54
+ mocks.openTarget.mockReset();
55
+ });
56
+
57
+ it('renders inline panel apps as bounded card-mode iframes with an expand action', () => {
58
+ render(<ChatInlinePanelAppCard panelApp={{
59
+ action: {
60
+ kind: 'show-content',
61
+ label: 'Show content',
62
+ request: {
63
+ placement: 'side_panel',
64
+ target: {
65
+ payload: { appId: 'weather-card' },
66
+ type: 'panel_app',
67
+ },
68
+ title: 'Weather',
69
+ },
70
+ },
71
+ appId: 'weather-card',
72
+ title: 'Weather',
73
+ }} />);
74
+
75
+ const iframe = screen.getByTitle('Weather');
76
+ expect(iframe.getAttribute('src')).toBe(
77
+ '/api/panel-apps/weather-card/content?nextclawDisplayMode=card&nextclawPlacement=inline',
78
+ );
79
+ expect(iframe.getAttribute('scrolling')).toBe('auto');
80
+
81
+ fireEvent.click(screen.getByLabelText('chatPanelCardExpand'));
82
+
83
+ expect(mocks.openTarget).toHaveBeenCalledWith(expect.objectContaining({
84
+ dedupeKey: 'panel-app:weather-card',
85
+ kind: 'panel-app',
86
+ title: 'Weather',
87
+ url: '/api/panel-apps/weather-card/content',
88
+ }));
89
+ });
@@ -1,12 +1,13 @@
1
1
  import { render } from "@testing-library/react";
2
2
  import type { NcpMessage } from "@nextclaw/ncp";
3
3
  import { beforeEach, expect, it, vi } from "vitest";
4
- import { ChatMessageListContainer } from "../chat-message-list.container";
4
+ import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
5
5
 
6
6
  const captures = vi.hoisted(() => ({
7
7
  renders: [] as Array<{
8
8
  messages: unknown[];
9
9
  onToolAction?: (action: unknown) => void;
10
+ renderPanelAppCard?: (panelApp: unknown) => unknown;
10
11
  texts?: Record<string, unknown>;
11
12
  }>,
12
13
  language: "en",
@@ -21,6 +22,7 @@ vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
21
22
  ChatMessageList: (props: {
22
23
  messages: unknown[];
23
24
  onToolAction?: (action: unknown) => void;
25
+ renderPanelAppCard?: (panelApp: unknown) => unknown;
24
26
  texts?: Record<string, unknown>;
25
27
  }) => {
26
28
  captures.renders.push(props);
@@ -251,6 +253,12 @@ it("delegates tool actions to the chat thread manager owner", () => {
251
253
  expect(captures.handleToolAction).toHaveBeenCalledWith(toolAction);
252
254
  });
253
255
 
256
+ it("passes the inline panel app renderer to the shared chat UI", () => {
257
+ render(<ChatMessageListContainer messages={[]} isSending={false} />);
258
+
259
+ expect(captures.renders[captures.renders.length - 1]?.renderPanelAppCard).toEqual(expect.any(Function));
260
+ });
261
+
254
262
  it("renders context compaction as an in-flow divider instead of a chat message", () => {
255
263
  const beforeMessage = {
256
264
  id: "message-before",
@@ -0,0 +1,115 @@
1
+ import { useEffect, useId, useMemo, useRef } from 'react';
2
+ import type { ChatPanelAppCardViewModel } from '@nextclaw/agent-chat-ui';
3
+ import { AppWindow, Maximize2 } from 'lucide-react';
4
+ import { getPresenter } from '@/app/presenters/app.presenter';
5
+ import { findPanelAppEntryByDisplayId, usePanelApps } from '@/features/panel-apps';
6
+ import { createPanelAppRightPanelResourceTarget } from '@/features/right-panel-resources';
7
+ import {
8
+ createInlinePanelAppCardUrl,
9
+ createFallbackPanelAppContentPath,
10
+ createInlinePanelAppTab,
11
+ PANEL_APP_INLINE_CARD_MAX_HEIGHT_PX,
12
+ PANEL_APP_INLINE_IFRAME_SANDBOX,
13
+ } from '@/features/chat/features/message/utils/chat-inline-panel-app-card.utils';
14
+ import { useDocBrowser } from '@/shared/components/doc-browser';
15
+ import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
16
+ import { t } from '@/shared/lib/i18n';
17
+
18
+ export function ChatInlinePanelAppCard({
19
+ panelApp,
20
+ }: {
21
+ panelApp: ChatPanelAppCardViewModel;
22
+ }) {
23
+ const docBrowser = useDocBrowser();
24
+ const iframeRef = useRef<HTMLIFrameElement>(null);
25
+ const iframeId = useId();
26
+ const panelApps = usePanelApps();
27
+ const entry = useMemo(
28
+ () => findPanelAppEntryByDisplayId(panelApps.data?.entries ?? [], panelApp.appId),
29
+ [panelApp.appId, panelApps.data?.entries],
30
+ );
31
+ const target = entry ? createPanelAppRightPanelResourceTarget(entry) : null;
32
+ const title = panelApp.title ?? target?.title ?? panelApp.appId;
33
+ const url = panelApps.isLoading
34
+ ? ''
35
+ : target?.url ?? createFallbackPanelAppContentPath(panelApp.appId);
36
+ const cardUrl = useMemo(
37
+ () => (url ? createInlinePanelAppCardUrl(url) : ''),
38
+ [url],
39
+ );
40
+ const tab = useMemo(
41
+ () => createInlinePanelAppTab({
42
+ appId: entry?.id ?? panelApp.appId,
43
+ title,
44
+ url,
45
+ }),
46
+ [entry?.id, panelApp.appId, title, url],
47
+ );
48
+ const iframeInstanceId = `${tab.id}:${tab.navVersion}:${iframeId}`;
49
+ const openExpanded = () => {
50
+ if (target) {
51
+ docBrowser.openTarget(target);
52
+ return;
53
+ }
54
+ docBrowser.open(url, {
55
+ dedupeKey: tab.dedupeKey,
56
+ kind: tab.kind,
57
+ title,
58
+ });
59
+ };
60
+
61
+ useEffect(() => {
62
+ if (!url) {
63
+ return undefined;
64
+ }
65
+ const handleMessage = (event: MessageEvent) => {
66
+ getPresenter().panelAppBridgeManager.handleIframeMessage({
67
+ event,
68
+ iframe: iframeRef.current,
69
+ iframeInstanceId,
70
+ tab,
71
+ });
72
+ };
73
+ window.addEventListener('message', handleMessage);
74
+ return () => window.removeEventListener('message', handleMessage);
75
+ }, [iframeInstanceId, tab, url]);
76
+
77
+ if (!url) {
78
+ return (
79
+ <div
80
+ className="h-[320px] min-h-[220px] w-full animate-pulse rounded-lg border border-[#eee3d1] bg-gradient-to-b from-white to-amber-50/60"
81
+ style={{ maxHeight: PANEL_APP_INLINE_CARD_MAX_HEIGHT_PX }}
82
+ />
83
+ );
84
+ }
85
+
86
+ return (
87
+ <div className="w-full max-w-[42rem] overflow-hidden rounded-lg border border-[#eee3d1] bg-white shadow-[0_1px_2px_rgba(30,20,10,0.04)]">
88
+ <div className="flex h-9 items-center justify-between gap-2 border-b border-[#f1e7d4] bg-[#fffaf2] px-2.5">
89
+ <div className="flex min-w-0 items-center gap-2 text-xs font-medium text-[#5c4933]">
90
+ <AppWindow className="h-3.5 w-3.5 shrink-0 text-primary" />
91
+ <span className="truncate">{title}</span>
92
+ </div>
93
+ <IconActionButton
94
+ className="h-7 w-7 rounded-md text-[#7a6145] hover:bg-white hover:text-[#2f2212]"
95
+ icon={<Maximize2 className="h-3.5 w-3.5" />}
96
+ label={t('chatPanelCardExpand')}
97
+ onClick={openExpanded}
98
+ />
99
+ </div>
100
+ <div
101
+ className="relative h-[420px] min-h-[220px] max-h-[min(60vh,420px)] overflow-hidden bg-white"
102
+ >
103
+ <iframe
104
+ ref={iframeRef}
105
+ key={iframeInstanceId}
106
+ src={cardUrl}
107
+ title={title}
108
+ sandbox={PANEL_APP_INLINE_IFRAME_SANDBOX}
109
+ scrolling="auto"
110
+ className="h-full w-full border-0 bg-white"
111
+ />
112
+ </div>
113
+ </div>
114
+ );
115
+ }
@@ -17,6 +17,7 @@ import {
17
17
  type ContextCompactionTimelineView,
18
18
  } from "@/features/chat/features/session/utils/ncp-session-context-metadata.utils";
19
19
  import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
20
+ import { ChatInlinePanelAppCard } from "@/features/chat/features/message/components/chat-inline-panel-app-card";
20
21
  import { useI18n } from "@/app/components/i18n-provider";
21
22
  import { formatDateTime, t } from "@/shared/lib/i18n";
22
23
 
@@ -309,6 +310,9 @@ export function ChatMessageListContainer({
309
310
  className="h-4 w-4 shrink-0"
310
311
  />
311
312
  )}
313
+ renderPanelAppCard={(panelApp) => (
314
+ <ChatInlinePanelAppCard panelApp={panelApp} />
315
+ )}
312
316
  />
313
317
  ),
314
318
  )}
@@ -1,5 +1,32 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { buildShowContentToolCard } from "../chat-message-show-content-tool-card.utils";
2
+ import { buildShowContentToolCard } from "@/features/chat/features/message/utils/chat-message-show-content-tool-card.utils";
3
+ import { buildToolCard } from "@/features/chat/features/message/utils/chat-message-tool-card.utils";
4
+ import type { ChatMessageAdapterTexts } from "@/features/chat/types/chat-message.types";
5
+
6
+ const TEXTS: ChatMessageAdapterTexts = {
7
+ fileAttachmentLabel: "File",
8
+ imageAttachmentLabel: "Image",
9
+ reasoningLabel: "Reasoning",
10
+ roleLabels: {
11
+ assistant: "Assistant",
12
+ fallback: "Message",
13
+ system: "System",
14
+ tool: "Tool",
15
+ user: "User",
16
+ },
17
+ showContentActionLabel: "Show content",
18
+ toolCallLabel: "Tool call",
19
+ toolInputLabel: "Input",
20
+ toolNoOutputLabel: "No output",
21
+ toolOutputLabel: "Output",
22
+ toolResultLabel: "Tool result",
23
+ toolStatusCancelledLabel: "Cancelled",
24
+ toolStatusCompletedLabel: "Completed",
25
+ toolStatusFailedLabel: "Failed",
26
+ toolStatusPreparingLabel: "Preparing",
27
+ toolStatusRunningLabel: "Running",
28
+ unknownPartLabel: "Unknown",
29
+ };
3
30
 
4
31
  describe("buildShowContentToolCard", () => {
5
32
  it("builds a show-content action from a normalized show_content result", () => {
@@ -47,6 +74,97 @@ describe("buildShowContentToolCard", () => {
47
74
  },
48
75
  title: "Example",
49
76
  purpose: "read",
77
+ placement: "side_panel",
78
+ },
79
+ },
80
+ });
81
+ });
82
+
83
+ it("builds an inline panel app card while keeping the expand action on side panel placement", () => {
84
+ const card = buildShowContentToolCard({
85
+ invocation: {
86
+ status: "result",
87
+ toolCallId: "call-show-content",
88
+ toolName: "show_content",
89
+ result: {
90
+ ok: true,
91
+ action: "showContent",
92
+ request: {
93
+ target: {
94
+ type: "panel_app",
95
+ payload: {
96
+ appId: "reader",
97
+ },
98
+ },
99
+ title: "Reader",
100
+ purpose: "interact",
101
+ placement: "inline",
102
+ },
103
+ },
104
+ },
105
+ actionLabel: "Show content",
106
+ statusLabel: "Completed",
107
+ });
108
+
109
+ expect(card).toMatchObject({
110
+ panelApp: {
111
+ appId: "reader",
112
+ title: "Reader",
113
+ action: {
114
+ kind: "show-content",
115
+ label: "Show content",
116
+ request: {
117
+ target: {
118
+ type: "panel_app",
119
+ payload: {
120
+ appId: "reader",
121
+ },
122
+ },
123
+ title: "Reader",
124
+ purpose: "interact",
125
+ placement: "side_panel",
126
+ },
127
+ },
128
+ },
129
+ });
130
+ });
131
+
132
+ it("preserves inline panel app data when converting to the shared chat tool view model", () => {
133
+ const card = buildShowContentToolCard({
134
+ invocation: {
135
+ status: "result",
136
+ toolCallId: "call-show-content",
137
+ toolName: "show_content",
138
+ result: {
139
+ ok: true,
140
+ action: "showContent",
141
+ request: {
142
+ target: {
143
+ type: "panel_app",
144
+ payload: {
145
+ appId: "weather-card",
146
+ },
147
+ },
148
+ title: "Weather",
149
+ purpose: "interact",
150
+ placement: "inline",
151
+ },
152
+ },
153
+ },
154
+ actionLabel: "Show content",
155
+ statusLabel: "Completed",
156
+ });
157
+
158
+ expect(card).not.toBeNull();
159
+ expect(buildToolCard(card!, TEXTS)).toMatchObject({
160
+ panelApp: {
161
+ appId: "weather-card",
162
+ title: "Weather",
163
+ action: {
164
+ kind: "show-content",
165
+ request: {
166
+ placement: "side_panel",
167
+ },
50
168
  },
51
169
  },
52
170
  });
@@ -0,0 +1,51 @@
1
+ import {
2
+ createPanelAppResourceUri,
3
+ RIGHT_PANEL_PANEL_APP_TAB_KIND,
4
+ } from '@/features/right-panel-resources';
5
+ import type { DocBrowserTab } from '@/shared/components/doc-browser/doc-browser-context';
6
+
7
+ export const PANEL_APP_INLINE_IFRAME_SANDBOX = [
8
+ 'allow-scripts',
9
+ 'allow-forms',
10
+ 'allow-modals',
11
+ 'allow-popups',
12
+ 'allow-popups-to-escape-sandbox',
13
+ 'allow-downloads',
14
+ 'allow-pointer-lock',
15
+ 'allow-presentation',
16
+ ].join(' ');
17
+
18
+ export const PANEL_APP_INLINE_CARD_MAX_HEIGHT_PX = 420;
19
+
20
+ const PANEL_APP_INLINE_CARD_SEARCH_PARAMS = {
21
+ nextclawDisplayMode: 'card',
22
+ nextclawPlacement: 'inline',
23
+ } as const;
24
+
25
+ export function createFallbackPanelAppContentPath(appId: string): string {
26
+ return `/api/panel-apps/${encodeURIComponent(appId)}/content`;
27
+ }
28
+
29
+ export function createInlinePanelAppCardUrl(url: string): string {
30
+ const separator = url.includes('?') ? '&' : '?';
31
+ return `${url}${separator}${new URLSearchParams(PANEL_APP_INLINE_CARD_SEARCH_PARAMS).toString()}`;
32
+ }
33
+
34
+ export function createInlinePanelAppTab(params: {
35
+ appId: string;
36
+ title: string;
37
+ url: string;
38
+ }): DocBrowserTab {
39
+ const { appId, title, url } = params;
40
+ return {
41
+ currentUrl: url,
42
+ dedupeKey: `panel-app:${appId}`,
43
+ history: [url],
44
+ historyIndex: 0,
45
+ id: `inline-panel-app:${appId}`,
46
+ kind: RIGHT_PANEL_PANEL_APP_TAB_KIND,
47
+ navVersion: 0,
48
+ resourceUri: createPanelAppResourceUri(appId),
49
+ title,
50
+ };
51
+ }