@nextclaw/ui 0.14.0 → 0.14.1-beta.1

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 (194) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/{api-B50yVdT8.js → api-dQ6uQSZG.js} +3 -3
  3. package/dist/assets/channels-list-page-DfpjB8XB.js +8 -0
  4. package/dist/assets/chat-page-D3QaEqjs.js +105 -0
  5. package/dist/assets/config-split-page-CeFfbaqc.js +1 -0
  6. package/dist/assets/{confirm-dialog-DRSelLdI.js → confirm-dialog-C95Y4QPC.js} +1 -1
  7. package/dist/assets/desktop-update-config-CcK6jiEb.js +1 -0
  8. package/dist/assets/{dist-D19wiQwB.js → dist-DTGzeil6.js} +1 -1
  9. package/dist/assets/{dist-NZRV-BxD.js → dist-fGogBmPA.js} +1 -1
  10. package/dist/assets/doc-browser-CuBTcBEZ.js +1 -0
  11. package/dist/assets/doc-browser-DVHhEb96.js +1 -0
  12. package/dist/assets/{doc-browser-context-C8KPbj-p.js → doc-browser-context-BfMXwnrz.js} +1 -1
  13. package/dist/assets/{ellipsis-BYy8TP0A.js → ellipsis-D2F4KMiU.js} +1 -1
  14. package/dist/assets/{external-link-528k6MDD.js → external-link-CcgtBK26.js} +1 -1
  15. package/dist/assets/index-ByJCbQqr.css +1 -0
  16. package/dist/assets/index-DjXqiloY.js +103 -0
  17. package/dist/assets/mcp-marketplace-page-98-V__3u.js +1 -0
  18. package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +40 -0
  19. package/dist/assets/model-config-page-aAUkZkSX.js +1 -0
  20. package/dist/assets/plus-1Rxl5FlV.js +1 -0
  21. package/dist/assets/provider-scoped-model-input-we4_Z4gP.js +1 -0
  22. package/dist/assets/providers-config-page-CtMkPSyu.js +1 -0
  23. package/dist/assets/{react-Dsotw5vM.js → react-CTqdhFMk.js} +1 -1
  24. package/dist/assets/remote-fQ7N_r8u.js +1 -0
  25. package/dist/assets/rotate-cw-CzfIeAJj.js +1 -0
  26. package/dist/assets/runtime-config-page-TD1UrTbG.js +1 -0
  27. package/dist/assets/{save-CHDnDTok.js → save-C9fKWfVB.js} +1 -1
  28. package/dist/assets/search-D5FQ5q0X.js +1 -0
  29. package/dist/assets/search-config-page-AMEJJNIS.js +1 -0
  30. package/dist/assets/secrets-config-page-CcgJnvBO.js +3 -0
  31. package/dist/assets/select-Cu5n0E-3.js +41 -0
  32. package/dist/assets/{tag-chip-Cr8GwpRE.js → tag-chip-C5aOtjxu.js} +1 -1
  33. package/dist/assets/use-config-D8TESeo6.js +1 -0
  34. package/dist/assets/x-1qETdQ70.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/__tests__/chat-conversation-panel.test.tsx +12 -63
  40. package/src/features/chat/components/conversation/chat-conversation-content.tsx +11 -50
  41. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +1 -0
  42. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +15 -2
  43. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +122 -85
  44. package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +120 -0
  45. package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +87 -0
  46. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +66 -41
  47. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +1 -0
  48. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +22 -36
  49. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +80 -52
  50. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +89 -0
  51. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +9 -1
  52. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +115 -0
  53. package/src/features/chat/features/message/components/chat-message-list.container.tsx +4 -0
  54. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +310 -0
  55. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +119 -1
  56. package/src/features/chat/features/message/utils/__tests__/chat-message-test.utils.ts +43 -0
  57. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +1 -346
  58. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +51 -0
  59. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +28 -5
  60. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +14 -9
  61. package/src/features/chat/features/message/utils/file-operation/diff.utils.ts +39 -36
  62. package/src/features/chat/features/message/utils/file-operation/line-builder.utils.ts +46 -38
  63. package/src/features/chat/features/message/utils/file-operation/parsed-block.utils.ts +18 -15
  64. package/src/features/chat/features/ncp/hooks/__tests__/use-ui-show-content-event.test.tsx +1 -0
  65. package/src/features/chat/features/ncp/utils/ncp-chat-query-derived.utils.ts +11 -3
  66. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +27 -0
  67. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +25 -25
  68. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +19 -1
  69. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +46 -0
  70. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +15 -2
  71. package/src/features/chat/features/session/hooks/__tests__/use-selected-session-context-window-indicator.test.tsx +63 -0
  72. package/src/features/chat/features/session/hooks/use-selected-session-context-window-indicator.ts +7 -10
  73. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +8 -0
  74. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +2 -0
  75. package/src/features/chat/features/session-type/components/__tests__/chat-sidebar-create-menu.test.tsx +24 -0
  76. package/src/features/chat/features/session-type/components/chat-sidebar-create-menu.tsx +10 -1
  77. package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +1 -0
  78. package/src/features/chat/features/session-type/utils/chat-session-type.utils.ts +4 -0
  79. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +193 -0
  80. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +123 -0
  81. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +104 -0
  82. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +72 -0
  83. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +54 -0
  84. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +132 -0
  85. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +82 -0
  86. package/src/features/chat/features/welcome/components/chat-welcome.tsx +121 -0
  87. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +53 -0
  88. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-selection.utils.test.ts +63 -0
  89. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-visibility.utils.test.ts +34 -0
  90. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +71 -0
  91. package/src/features/chat/features/welcome/utils/chat-welcome-selection.utils.ts +79 -0
  92. package/src/features/chat/features/welcome/utils/chat-welcome-visibility.utils.ts +17 -0
  93. package/src/features/chat/index.ts +1 -0
  94. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +93 -0
  95. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +129 -0
  96. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +34 -0
  97. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +143 -1
  98. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +30 -0
  99. package/src/features/chat/managers/__tests__/recent-selection.manager.test.ts +37 -14
  100. package/src/features/chat/managers/chat-draft-intent.manager.ts +46 -0
  101. package/src/features/chat/managers/chat-input.manager.ts +37 -15
  102. package/src/features/chat/managers/chat-run.manager.ts +3 -0
  103. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +19 -2
  104. package/src/features/chat/managers/chat-thread.manager.ts +4 -0
  105. package/src/features/chat/managers/chat-ui.manager.ts +2 -6
  106. package/src/features/chat/managers/recent-selection.manager.ts +21 -67
  107. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +47 -1
  108. package/src/features/chat/pages/ncp-chat-page.tsx +18 -0
  109. package/src/features/chat/stores/chat-input.store.ts +9 -1
  110. package/src/features/chat/stores/recent-selection.store.ts +85 -0
  111. package/src/features/chat/types/chat-input-bar.types.ts +5 -1
  112. package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +27 -23
  113. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +137 -0
  114. package/src/features/panel-apps/components/panel-apps-list.tsx +119 -5
  115. package/src/features/panel-apps/index.ts +2 -0
  116. package/src/features/panel-apps/utils/panel-app-entry-match.utils.ts +17 -0
  117. package/src/features/settings/components/config/provider-form.tsx +284 -387
  118. package/src/features/settings/components/config/provider-status-badge.tsx +4 -4
  119. package/src/features/settings/components/config/secrets-config-form.tsx +11 -135
  120. package/src/features/settings/hooks/use-provider-auth-flow.ts +140 -0
  121. package/src/features/settings/pages/__tests__/model-config-page.test.tsx +16 -0
  122. package/src/features/settings/pages/secrets-config-page.tsx +2 -1
  123. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +60 -1
  124. package/src/features/settings/utils/__tests__/secrets-config-form.utils.test.ts +66 -0
  125. package/src/features/settings/utils/provider-form-context.utils.ts +131 -0
  126. package/src/features/settings/utils/provider-form-model.utils.ts +101 -0
  127. package/src/features/settings/utils/provider-form-support.utils.ts +102 -1
  128. package/src/features/settings/utils/secrets-config-form.utils.ts +139 -0
  129. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +17 -11
  130. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +14 -9
  131. package/src/features/system-status/components/config/runtime-config-editor.tsx +8 -5
  132. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +14 -9
  133. package/src/features/system-status/components/config/runtime-settings-card.tsx +16 -7
  134. package/src/features/system-status/components/runtime-security-card.tsx +79 -25
  135. package/src/shared/components/common/__tests__/searchable-model-input.test.tsx +34 -0
  136. package/src/shared/components/common/provider-scoped-model-input.tsx +1 -0
  137. package/src/shared/components/common/searchable-model-input.tsx +102 -87
  138. package/src/shared/components/ui/README.md +7 -0
  139. package/src/shared/components/ui/popover.tsx +19 -3
  140. package/src/shared/components/ui/select.tsx +14 -4
  141. package/src/shared/hooks/use-infinite-scroll-loader.ts +14 -10
  142. package/src/shared/lib/api/chat-session-type.types.ts +7 -0
  143. package/src/shared/lib/api/index.ts +2 -0
  144. package/src/shared/lib/api/ncp-session.types.ts +1 -0
  145. package/src/shared/lib/api/preferences/preference.types.ts +22 -0
  146. package/src/shared/lib/api/preferences/preference.utils.ts +40 -0
  147. package/src/shared/lib/api/types.ts +3 -0
  148. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -1
  149. package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
  150. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +10 -0
  151. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -1
  152. package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
  153. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +10 -0
  154. package/dist/assets/app-presenter-provider-BI-ewLbO.js +0 -3
  155. package/dist/assets/arrow-left-BOCJyLbQ.js +0 -1
  156. package/dist/assets/book-open-Bd2Xbrrc.js +0 -1
  157. package/dist/assets/channels-list-page-moJkpxtU.js +0 -8
  158. package/dist/assets/chat-page-CO27cl4v.js +0 -105
  159. package/dist/assets/config-split-page-CsQ2rIdq.js +0 -1
  160. package/dist/assets/desktop-update-config-qa-SynOc.js +0 -1
  161. package/dist/assets/doc-browser-2ddUJCA0.js +0 -1
  162. package/dist/assets/doc-browser-BjHUczQb.js +0 -1
  163. package/dist/assets/es2015-BUIIKCNh.js +0 -41
  164. package/dist/assets/host-capabilities-9bB_998Z.js +0 -1
  165. package/dist/assets/index-CMNrRhnG.js +0 -101
  166. package/dist/assets/index-UTmTW1eU.css +0 -1
  167. package/dist/assets/key-round-D3vD9dj7.js +0 -1
  168. package/dist/assets/loader-circle-DV9TSRns.js +0 -1
  169. package/dist/assets/mcp-marketplace-page-CS5Lh3oA.js +0 -1
  170. package/dist/assets/mcp-marketplace-page-DEOMcWb0.js +0 -40
  171. package/dist/assets/model-config-page-BSc4o7pb.js +0 -1
  172. package/dist/assets/notice-card-qqlbsOoR.js +0 -1
  173. package/dist/assets/plus-Bf9qhPDO.js +0 -1
  174. package/dist/assets/popover-BnmPOXe3.js +0 -1
  175. package/dist/assets/provider-scoped-model-input-Ca56s475.js +0 -1
  176. package/dist/assets/providers-config-page-D8Kf4U3u.js +0 -1
  177. package/dist/assets/refresh-cw-Bmx4wc9P.js +0 -1
  178. package/dist/assets/remote-DNMEy_c4.js +0 -1
  179. package/dist/assets/rotate-cw-CL0sVBel.js +0 -1
  180. package/dist/assets/runtime-config-page-CUFtKr0U.js +0 -1
  181. package/dist/assets/search-D7L-TCH-.js +0 -1
  182. package/dist/assets/search-config-page-CA3RVAef.js +0 -1
  183. package/dist/assets/secrets-config-page-95yaH1fa.js +0 -3
  184. package/dist/assets/status-dot-Dm4h-bjn.js +0 -1
  185. package/dist/assets/tabs-CJvw5Lqp.js +0 -1
  186. package/dist/assets/tooltip-Bxzi6aDL.js +0 -1
  187. package/dist/assets/trash-2-CSfX-BvU.js +0 -1
  188. package/dist/assets/use-config-TzwzTgg0.js +0 -1
  189. package/dist/assets/x-fkIWvi07.js +0 -1
  190. package/src/features/chat/components/__tests__/chat-welcome.test.tsx +0 -45
  191. package/src/features/chat/components/chat-welcome.tsx +0 -102
  192. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-bar-controller.test.tsx +0 -152
  193. package/src/features/chat/features/input/hooks/use-chat-input-bar-controller.ts +0 -142
  194. /package/dist/assets/{config-hints-Cl2XAhzV.js → config-hints-CTqBnCDp.js} +0 -0
@@ -483,6 +483,7 @@ describe('ChatThreadManager showContent', () => {
483
483
  },
484
484
  title: 'Example URL',
485
485
  purpose: 'read',
486
+ placement: undefined,
486
487
  });
487
488
  await manager.handleUiShowContentEvent({
488
489
  id: 'tool:call-show-content-1:show-content',
@@ -495,6 +496,7 @@ describe('ChatThreadManager showContent', () => {
495
496
  },
496
497
  title: 'Example URL',
497
498
  purpose: 'read',
499
+ placement: undefined,
498
500
  });
499
501
 
500
502
  expect(uiManager.showContent).toHaveBeenCalledTimes(1);
@@ -541,6 +543,34 @@ describe('ChatThreadManager showContent', () => {
541
543
  title: 'Reader',
542
544
  });
543
545
  });
546
+
547
+ });
548
+
549
+ describe('ChatThreadManager inline showContent', () => {
550
+ it('does not auto-open inline panel app content outside the message card', async () => {
551
+ const uiManager = createUiManager();
552
+ const manager = new ChatThreadManager(
553
+ uiManager,
554
+ {} as ConstructorParameters<typeof ChatThreadManager>[1],
555
+ {} as ConstructorParameters<typeof ChatThreadManager>[2],
556
+ );
557
+
558
+ await manager.handleUiShowContentEvent({
559
+ id: 'tool:call-show-content-inline:show-content',
560
+ toolCallId: 'call-show-content-inline',
561
+ target: {
562
+ type: 'panel_app',
563
+ payload: {
564
+ appId: 'reader',
565
+ },
566
+ },
567
+ title: 'Reader',
568
+ purpose: 'interact',
569
+ placement: 'inline',
570
+ });
571
+
572
+ expect(uiManager.showContent).not.toHaveBeenCalled();
573
+ });
544
574
  });
545
575
 
546
576
  describe('ChatThreadManager visible workspace selection', () => {
@@ -1,22 +1,16 @@
1
- import { RecentSelectionManager } from '../recent-selection.manager';
1
+ import { RecentSelectionManager } from '@/features/chat/managers/recent-selection.manager';
2
+ import { useRecentSelectionStore } from '@/features/chat/stores/recent-selection.store';
2
3
 
3
4
  describe('RecentSelectionManager', () => {
4
5
  const storageKey = 'test.recent-selection-manager';
5
- let storageState: Record<string, string>;
6
- let storage: Pick<Storage, 'getItem' | 'setItem'>;
7
6
 
8
7
  beforeEach(() => {
9
- storageState = {};
10
- storage = {
11
- getItem: (key) => storageState[key] ?? null,
12
- setItem: (key, value) => {
13
- storageState[key] = value;
14
- }
15
- };
8
+ window.localStorage.clear();
9
+ useRecentSelectionStore.setState({ entriesByKey: {} });
16
10
  });
17
11
 
18
12
  it('stores recent values in LRU order and respects the size limit', () => {
19
- const manager = new RecentSelectionManager({ storageKey, limit: 3, storage });
13
+ const manager = new RecentSelectionManager({ storageKey, limit: 3 });
20
14
 
21
15
  manager.remember('openai/gpt-5');
22
16
  manager.remember('anthropic/claude-sonnet-4');
@@ -31,7 +25,7 @@ describe('RecentSelectionManager', () => {
31
25
  });
32
26
 
33
27
  it('filters recent values by the currently available list and threshold', () => {
34
- const manager = new RecentSelectionManager({ storageKey, limit: 4, storage });
28
+ const manager = new RecentSelectionManager({ storageKey, limit: 4 });
35
29
  manager.remember('openai/gpt-5');
36
30
  manager.remember('anthropic/claude-sonnet-4');
37
31
  manager.remember('missing/model');
@@ -60,9 +54,38 @@ describe('RecentSelectionManager', () => {
60
54
  });
61
55
 
62
56
  it('returns an empty list when storage content is malformed', () => {
63
- storageState[storageKey] = '{broken-json';
64
- const manager = new RecentSelectionManager({ storageKey, limit: 3, storage });
57
+ window.localStorage.setItem(storageKey, '{broken-json');
58
+ const manager = new RecentSelectionManager({ storageKey, limit: 3 });
65
59
 
66
60
  expect(manager.read()).toEqual([]);
67
61
  });
62
+
63
+ it('does not mutate an empty store while reading recent values', () => {
64
+ const manager = new RecentSelectionManager({ storageKey, limit: 3 });
65
+
66
+ expect(manager.read()).toEqual([]);
67
+ expect(useRecentSelectionStore.getState().entriesByKey).toEqual({});
68
+ });
69
+
70
+ it('keeps namespaced recent values separate from the global list', () => {
71
+ const manager = new RecentSelectionManager({ storageKey, limit: 3 });
72
+
73
+ manager.remember('openai/gpt-5');
74
+ manager.remember('anthropic/claude-sonnet-4', { namespace: 'codex' });
75
+ manager.remember('minimax/MiniMax-M2.7', { namespace: 'hermes' });
76
+ manager.remember('openai/gpt-5.1', { namespace: 'codex' });
77
+
78
+ expect(manager.read()).toEqual(['openai/gpt-5']);
79
+ expect(manager.read({ namespace: 'codex' })).toEqual([
80
+ 'openai/gpt-5.1',
81
+ 'anthropic/claude-sonnet-4'
82
+ ]);
83
+ expect(
84
+ manager.resolveVisible({
85
+ namespace: 'hermes',
86
+ availableValues: ['minimax/MiniMax-M2.7', 'openai/gpt-5'],
87
+ minAvailableCount: 0
88
+ })
89
+ ).toEqual(['minimax/MiniMax-M2.7']);
90
+ });
68
91
  });
@@ -0,0 +1,46 @@
1
+ export type ChatDraftIntent = {
2
+ id: number;
3
+ prompt: string;
4
+ };
5
+
6
+ type ChatDraftIntentListener = (intent: ChatDraftIntent) => void;
7
+
8
+ export class ChatDraftIntentManager {
9
+ private nextId = 0;
10
+ private pendingIntent: ChatDraftIntent | null = null;
11
+ private readonly listeners = new Set<ChatDraftIntentListener>();
12
+
13
+ requestDraft = (prompt: string) => {
14
+ const normalizedPrompt = prompt.trim();
15
+ if (!normalizedPrompt) {
16
+ return;
17
+ }
18
+ const intent: ChatDraftIntent = {
19
+ id: this.nextId + 1,
20
+ prompt: normalizedPrompt,
21
+ };
22
+ this.nextId = intent.id;
23
+ this.pendingIntent = intent;
24
+ this.listeners.forEach((listener) => listener(intent));
25
+ };
26
+
27
+ consumePending = (): ChatDraftIntent | null => {
28
+ const intent = this.pendingIntent;
29
+ this.pendingIntent = null;
30
+ return intent;
31
+ };
32
+
33
+ markConsumed = (intentId: number) => {
34
+ if (this.pendingIntent?.id !== intentId) {
35
+ return;
36
+ }
37
+ this.pendingIntent = null;
38
+ };
39
+
40
+ subscribe = (listener: ChatDraftIntentListener) => {
41
+ this.listeners.add(listener);
42
+ return () => {
43
+ this.listeners.delete(listener);
44
+ };
45
+ };
46
+ }
@@ -29,11 +29,9 @@ import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
29
29
  import { normalizeSessionType } from '@/features/chat/features/session-type/utils/chat-session-type.utils';
30
30
  import { systemStatusManager } from '@/features/system-status';
31
31
  import { shouldClearPendingProjectRootOverride } from '@/features/chat/features/session/utils/chat-run-metadata.utils';
32
+ import { normalizeSessionProjectRootValue } from '@/shared/lib/session-project';
32
33
 
33
34
  function resolveModelForSend(value: string): string | undefined {
34
- if (isRuntimeDefaultModelValue(value)) {
35
- return undefined;
36
- }
37
35
  return value || undefined;
38
36
  }
39
37
 
@@ -55,16 +53,6 @@ export class ChatInputManager {
55
53
  private sessionListManager: ChatSessionListManager
56
54
  ) {}
57
55
 
58
- private hasSnapshotChanges = (patch: Partial<ChatInputSnapshot>): boolean => {
59
- const current = useChatInputStore.getState().snapshot;
60
- for (const [key, value] of Object.entries(patch) as Array<[keyof ChatInputSnapshot, ChatInputSnapshot[keyof ChatInputSnapshot]]>) {
61
- if (!Object.is(current[key], value)) {
62
- return true;
63
- }
64
- }
65
- return false;
66
- };
67
-
68
56
  private resolveUpdateValue = <T>(prev: T, next: SetStateAction<T>): T => {
69
57
  if (typeof next === 'function') {
70
58
  return (next as (value: T) => T)(prev);
@@ -75,6 +63,16 @@ export class ChatInputManager {
75
63
  private isSameStringArray = (left: string[], right: string[]): boolean =>
76
64
  left.length === right.length && left.every((value, index) => value === right[index]);
77
65
 
66
+ private hasSnapshotChanges = (patch: Partial<ChatInputSnapshot>): boolean => {
67
+ const current = useChatInputStore.getState().snapshot;
68
+ for (const [key, value] of Object.entries(patch) as Array<[keyof ChatInputSnapshot, ChatInputSnapshot[keyof ChatInputSnapshot]]>) {
69
+ if (!Object.is(current[key], value)) {
70
+ return true;
71
+ }
72
+ }
73
+ return false;
74
+ };
75
+
78
76
  private isRuntimeBlockedForSend = (): boolean =>
79
77
  isNcpChatRuntimeBlocked(systemStatusManager.getStatusView());
80
78
 
@@ -145,15 +143,19 @@ export class ChatInputManager {
145
143
  sessionKey: string | null | undefined;
146
144
  selectedSessionProjectRoot: string | null | undefined;
147
145
  }): string | null => {
146
+ const { sessionKey, selectedSessionProjectRoot } = params;
148
147
  const { pendingProjectRoot, pendingProjectRootSessionKey } =
149
148
  useChatInputStore.getState().snapshot;
150
149
  if (
151
150
  pendingProjectRoot !== null &&
152
- (!params.sessionKey || params.sessionKey === pendingProjectRootSessionKey)
151
+ (!sessionKey || sessionKey === pendingProjectRootSessionKey)
153
152
  ) {
154
153
  return pendingProjectRoot;
155
154
  }
156
- return params.selectedSessionProjectRoot ?? null;
155
+ if (!sessionKey) {
156
+ return useChatInputStore.getState().snapshot.defaultProjectRoot;
157
+ }
158
+ return selectedSessionProjectRoot ?? null;
157
159
  };
158
160
 
159
161
  clearPendingProjectRootOverrideForSession = (params: {
@@ -197,6 +199,15 @@ export class ChatInputManager {
197
199
  this.syncComposerSnapshot(createChatComposerNodesFromDraft(value));
198
200
  };
199
201
 
202
+ applyPromptSuggestion = (prompt: string) => {
203
+ const normalizedPrompt = prompt.trim();
204
+ if (!normalizedPrompt) {
205
+ return;
206
+ }
207
+ this.setDraft(normalizedPrompt);
208
+ this.requestComposerFocusAtEnd();
209
+ };
210
+
200
211
  requestComposerFocusAtEnd = () => {
201
212
  const currentRequest = useChatInputStore.getState().snapshot.composerFocusRequest;
202
213
  useChatInputStore.getState().setSnapshot({
@@ -260,6 +271,14 @@ export class ChatInputManager {
260
271
  });
261
272
  };
262
273
 
274
+ setPendingProjectRoot = (projectRoot: string | null) => {
275
+ const normalizedProjectRoot = normalizeSessionProjectRootValue(projectRoot);
276
+ useChatInputStore.getState().setSnapshot({
277
+ pendingProjectRoot: normalizedProjectRoot,
278
+ pendingProjectRootSessionKey: null,
279
+ });
280
+ };
281
+
263
282
  send = async () => {
264
283
  const inputSnapshot = useChatInputStore.getState().snapshot;
265
284
  const sessionSnapshot = useChatSessionListStore.getState().snapshot;
@@ -351,6 +370,9 @@ export class ChatInputManager {
351
370
 
352
371
  selectModel = (value: string) => {
353
372
  this.setSelectedModel(value);
373
+ chatRecentModelsManager.remember(value, {
374
+ namespace: useChatInputStore.getState().snapshot.selectedSessionType,
375
+ });
354
376
  if (!isRuntimeDefaultModelValue(value)) {
355
377
  chatRecentModelsManager.remember(value);
356
378
  }
@@ -61,6 +61,8 @@ export class ChatRunManager {
61
61
 
62
62
  applyRunSnapshot = (snapshot: ChatRunSnapshot) => {
63
63
  const isSending = snapshot.isSending || snapshot.isRunning;
64
+ const sessionKey =
65
+ snapshot.routeSessionKey ?? snapshot.materializedSessionKey ?? null;
64
66
  useChatInputStore.getState().setSnapshot({
65
67
  canStopGeneration: snapshot.isRunning,
66
68
  stopDisabledReason: snapshot.isRunning ? null : '__preparing__',
@@ -70,6 +72,7 @@ export class ChatRunManager {
70
72
  isSending,
71
73
  });
72
74
  useChatThreadStore.getState().setSnapshot({
75
+ sessionKey,
73
76
  isHistoryLoading: snapshot.isHydrating,
74
77
  messages: snapshot.visibleMessages,
75
78
  isSending,
@@ -45,6 +45,7 @@ export class ChatSessionPreferenceSync {
45
45
  };
46
46
 
47
47
  syncInputSelection = (params: {
48
+ selectedSessionKey?: string | null;
48
49
  selectedSessionExists: boolean;
49
50
  selectedSessionPreferredModel?: string;
50
51
  fallbackPreferredModel?: string;
@@ -56,15 +57,14 @@ export class ChatSessionPreferenceSync {
56
57
  defaultModel,
57
58
  fallbackPreferredModel,
58
59
  fallbackPreferredThinking,
60
+ selectedSessionKey = null,
59
61
  selectedSessionExists,
60
62
  selectedSessionPreferredModel,
61
63
  selectedSessionPreferredThinking,
62
64
  } = params;
63
65
  const { snapshot } = useChatInputStore.getState();
64
66
  const { modelOptions } = snapshot;
65
- const selectedSessionKey = useChatThreadStore.getState().snapshot.sessionKey;
66
67
  const sessionChanged = this.previousPreferenceSessionKey !== selectedSessionKey;
67
- this.previousPreferenceSessionKey = selectedSessionKey;
68
68
  const preserveCurrentPreference = sessionChanged && Boolean(selectedSessionKey) && !selectedSessionExists;
69
69
  const selectedModel = resolveSelectedModelValue({
70
70
  currentSelectedModel: snapshot.selectedModel,
@@ -88,10 +88,27 @@ export class ChatSessionPreferenceSync {
88
88
  preferSessionPreferredThinking: sessionChanged,
89
89
  preserveCurrentSelectedThinkingOnSessionChange: preserveCurrentPreference,
90
90
  });
91
+ if (
92
+ snapshot.selectedModel === selectedModel &&
93
+ snapshot.selectedThinkingLevel === selectedThinking
94
+ ) {
95
+ this.recordSyncedPreferenceSessionKey(selectedSessionKey, selectedSessionExists);
96
+ return;
97
+ }
91
98
  useChatInputStore.getState().setSnapshot({
92
99
  selectedModel,
93
100
  selectedThinkingLevel: selectedThinking,
94
101
  });
102
+ this.recordSyncedPreferenceSessionKey(selectedSessionKey, selectedSessionExists);
103
+ };
104
+
105
+ private recordSyncedPreferenceSessionKey = (
106
+ selectedSessionKey: string | null,
107
+ selectedSessionExists: boolean,
108
+ ): void => {
109
+ if (!selectedSessionKey || selectedSessionExists) {
110
+ this.previousPreferenceSessionKey = selectedSessionKey;
111
+ }
95
112
  };
96
113
 
97
114
  private enqueue = (next: QueuedSessionPreferenceSync): void => {
@@ -296,10 +296,14 @@ export class ChatThreadManager {
296
296
  target: payload.target,
297
297
  title: payload.title,
298
298
  purpose: payload.purpose,
299
+ placement: payload.placement,
299
300
  });
300
301
  };
301
302
 
302
303
  private showContent = async (request: ChatUiShowContentRequest): Promise<void> => {
304
+ if (request.placement === 'inline') {
305
+ return;
306
+ }
303
307
  if (request.target.type === 'file') {
304
308
  this.openFilePreview({
305
309
  path: request.target.payload.path,
@@ -6,6 +6,7 @@ import {
6
6
  createPanelAppResourceUri,
7
7
  createPanelAppRightPanelResourceTarget,
8
8
  } from '@/features/right-panel-resources';
9
+ import { findPanelAppEntryByDisplayId } from '@/features/panel-apps';
9
10
  import type { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
10
11
  import { nextclawClient, type PanelAppEntryView } from '@/shared/lib/api';
11
12
 
@@ -26,11 +27,6 @@ type ChatUiDisplayContentTarget = Extract<ChatUiShowContentRequest['target'], {
26
27
 
27
28
  const noopConfirm: ChatUiActions['confirm'] = async () => false;
28
29
 
29
- function isPanelAppEntryMatch(entry: PanelAppEntryView, value: string): boolean {
30
- const normalizedValue = value.trim();
31
- return [entry.id, entry.appId, entry.fileName, entry.title].some((candidate) => candidate.trim() === normalizedValue);
32
- }
33
-
34
30
  export class ChatUiManager {
35
31
  private state: ChatUiState = {
36
32
  pathname: ''
@@ -84,7 +80,7 @@ export class ChatUiManager {
84
80
  return null;
85
81
  }
86
82
  try {
87
- return (await nextclawClient.panelApps.listPanelApps()).entries.find((entry) => isPanelAppEntryMatch(entry, normalizedValue)) ?? null;
83
+ return findPanelAppEntryByDisplayId((await nextclawClient.panelApps.listPanelApps()).entries, normalizedValue);
88
84
  } catch {
89
85
  return null;
90
86
  }
@@ -1,49 +1,51 @@
1
+ import {
2
+ useRecentSelectionStore,
3
+ normalizeRecentSelectionList,
4
+ normalizeRecentSelectionValue,
5
+ resolveRecentSelectionKey,
6
+ type RecentSelectionScope,
7
+ } from '@/features/chat/stores/recent-selection.store';
8
+
1
9
  type RecentSelectionManagerOptions = {
2
10
  storageKey: string;
3
11
  limit: number;
4
- storage?: Pick<Storage, 'getItem' | 'setItem'> | null;
5
12
  };
6
13
 
7
14
  type VisibleRecentSelectionParams = {
8
15
  availableValues: string[];
9
16
  minAvailableCount: number;
10
17
  limit?: number;
11
- };
18
+ } & RecentSelectionScope;
12
19
 
13
20
  export class RecentSelectionManager {
14
21
  constructor(private readonly options: RecentSelectionManagerOptions) {}
15
22
 
16
- read = (): string[] => {
17
- const storage = this.getStorage();
18
- if (!storage) {
19
- return [];
20
- }
21
- try {
22
- return this.normalizeList(JSON.parse(storage.getItem(this.options.storageKey) ?? '[]'));
23
- } catch {
24
- return [];
25
- }
23
+ read = (scope: RecentSelectionScope = {}): string[] => {
24
+ const key = resolveRecentSelectionKey(this.options, scope);
25
+ return normalizeRecentSelectionList(useRecentSelectionStore.getState().entriesByKey[key], this.options.limit);
26
26
  };
27
27
 
28
- remember = (value: string): string[] => {
29
- const normalizedValue = this.normalizeValue(value);
28
+ remember = (value: string, scope: RecentSelectionScope = {}): string[] => {
29
+ const normalizedValue = normalizeRecentSelectionValue(value);
30
30
  if (!normalizedValue) {
31
- return this.read();
31
+ return this.read(scope);
32
32
  }
33
- const next = [normalizedValue, ...this.read().filter((item) => item !== normalizedValue)].slice(0, this.options.limit);
34
- this.write(next);
33
+ const key = resolveRecentSelectionKey(this.options, scope);
34
+ const next = [normalizedValue, ...this.read(scope).filter((item) => item !== normalizedValue)]
35
+ .slice(0, this.options.limit);
36
+ useRecentSelectionStore.getState().setEntries(key, next);
35
37
  return next;
36
38
  };
37
39
 
38
40
  resolveVisible = (params: VisibleRecentSelectionParams): string[] => {
39
- const availableValues = this.normalizeList(params.availableValues, Number.POSITIVE_INFINITY);
41
+ const availableValues = normalizeRecentSelectionList(params.availableValues, Number.POSITIVE_INFINITY);
40
42
  if (availableValues.length <= params.minAvailableCount) {
41
43
  return [];
42
44
  }
43
45
  const availableSet = new Set(availableValues);
44
46
  const visible: string[] = [];
45
47
  const maxVisibleItems = Math.max(1, params.limit ?? this.options.limit);
46
- for (const value of this.read()) {
48
+ for (const value of this.read(params)) {
47
49
  if (!availableSet.has(value) || visible.includes(value)) {
48
50
  continue;
49
51
  }
@@ -54,52 +56,4 @@ export class RecentSelectionManager {
54
56
  }
55
57
  return visible;
56
58
  };
57
-
58
- private write = (values: string[]): void => {
59
- const storage = this.getStorage();
60
- if (!storage) {
61
- return;
62
- }
63
- try {
64
- storage.setItem(this.options.storageKey, JSON.stringify(this.normalizeList(values)));
65
- } catch {
66
- // Ignore storage write failures and keep the runtime behavior deterministic.
67
- }
68
- };
69
-
70
- private getStorage = (): Storage | null => {
71
- if (Object.prototype.hasOwnProperty.call(this.options, 'storage')) {
72
- return (this.options.storage as Storage | null | undefined) ?? null;
73
- }
74
- if (typeof window === 'undefined') {
75
- return null;
76
- }
77
- return window.localStorage;
78
- };
79
-
80
- private normalizeList = (values: unknown, limit = this.options.limit): string[] => {
81
- if (!Array.isArray(values)) {
82
- return [];
83
- }
84
- const deduped: string[] = [];
85
- for (const value of values) {
86
- const normalized = this.normalizeValue(value);
87
- if (!normalized || deduped.includes(normalized)) {
88
- continue;
89
- }
90
- deduped.push(normalized);
91
- if (deduped.length >= limit) {
92
- break;
93
- }
94
- }
95
- return deduped;
96
- };
97
-
98
- private normalizeValue = (value: unknown): string | null => {
99
- if (typeof value !== 'string') {
100
- return null;
101
- }
102
- const normalized = value.trim();
103
- return normalized.length > 0 ? normalized : null;
104
- };
105
59
  }
@@ -11,7 +11,9 @@ import {
11
11
  buildChatRunMetadata,
12
12
  shouldClearPendingProjectRootOverride
13
13
  } from '@/features/chat/features/session/utils/chat-run-metadata.utils';
14
- import { filterNcpChatModelOptionsBySessionType } from '@/features/chat/features/ncp/utils/ncp-chat-query-derived.utils';
14
+ import {
15
+ filterNcpChatModelOptionsBySessionType,
16
+ } from '@/features/chat/features/ncp/utils/ncp-chat-query-derived.utils';
15
17
 
16
18
  const modelOptions: ChatModelOption[] = [
17
19
  {
@@ -83,6 +85,29 @@ describe('filterNcpChatModelOptionsBySessionType', () => {
83
85
  }]);
84
86
  });
85
87
 
88
+ it('attaches runtime default thinking capability when the runtime publishes one', () => {
89
+ expect(
90
+ filterNcpChatModelOptionsBySessionType({
91
+ modelOptions,
92
+ modelSelectionMode: 'runtime-default',
93
+ runtimeDefaultModelLabel: 'Runtime default',
94
+ runtimeDefaultThinkingCapability: {
95
+ supported: ['minimal', 'low', 'medium', 'high'],
96
+ default: 'high',
97
+ },
98
+ })
99
+ ).toEqual([{
100
+ value: RUNTIME_DEFAULT_MODEL_VALUE,
101
+ modelLabel: 'Runtime default',
102
+ providerLabel: '',
103
+ isRuntimeDefault: true,
104
+ thinkingCapability: {
105
+ supported: ['minimal', 'low', 'medium', 'high'],
106
+ default: 'high',
107
+ },
108
+ }]);
109
+ });
110
+
86
111
  it('adds the runtime default option beside supported models for optional runtime selection', () => {
87
112
  expect(
88
113
  filterNcpChatModelOptionsBySessionType({
@@ -102,6 +127,27 @@ describe('filterNcpChatModelOptionsBySessionType', () => {
102
127
  modelOptions[1],
103
128
  ]);
104
129
  });
130
+
131
+ it('adds runtime default thinking capability beside supported models for optional runtime selection', () => {
132
+ expect(
133
+ filterNcpChatModelOptionsBySessionType({
134
+ modelOptions,
135
+ modelSelectionMode: 'optional',
136
+ runtimeDefaultModelLabel: 'Runtime default',
137
+ runtimeDefaultThinkingCapability: {
138
+ supported: ['minimal', 'low', 'medium', 'high'],
139
+ default: 'high',
140
+ },
141
+ supportedModels: ['openai/gpt-5'],
142
+ })[0]
143
+ ).toMatchObject({
144
+ value: RUNTIME_DEFAULT_MODEL_VALUE,
145
+ thinkingCapability: {
146
+ supported: ['minimal', 'low', 'medium', 'high'],
147
+ default: 'high',
148
+ },
149
+ });
150
+ });
105
151
  });
106
152
 
107
153
  describe('buildChatRunMetadata', () => {
@@ -30,6 +30,23 @@ import { isNcpChatRuntimeBlocked, resolveNcpChatSendErrorMessage } from "@/featu
30
30
  import { useUiShowContentEvent } from "@/features/chat/features/ncp/hooks/use-ui-show-content-event";
31
31
  import { readNcpContextWindowValue } from "@/features/chat/features/session/utils/ncp-session-context-metadata.utils";
32
32
 
33
+ function useChatDraftIntentConsumer() {
34
+ const appPresenter = useAppPresenter();
35
+ const presenter = usePresenter();
36
+ useEffect(() => {
37
+ const applyIntent = (intent: { id: number; prompt: string }) => {
38
+ presenter.startAgentCreationDraft(intent.prompt);
39
+ appPresenter.chatDraftIntentManager.markConsumed(intent.id);
40
+ };
41
+ const unsubscribe = appPresenter.chatDraftIntentManager.subscribe(applyIntent);
42
+ const pendingIntent = appPresenter.chatDraftIntentManager.consumePending();
43
+ if (pendingIntent) {
44
+ applyIntent(pendingIntent);
45
+ }
46
+ return unsubscribe;
47
+ }, [appPresenter, presenter]);
48
+ }
49
+
33
50
  function useNcpChatRouteSelection() {
34
51
  const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
35
52
  const routeSessionKey = useMemo(
@@ -170,6 +187,7 @@ function NcpChatPageContent({ view }: ChatPageProps) {
170
187
  syncRouteSessionSelection:
171
188
  presenter.chatSessionListManager.syncRouteSessionSelection,
172
189
  });
190
+ useChatDraftIntentConsumer();
173
191
  useUiShowContentEvent();
174
192
  useChatRunSnapshotSync({
175
193
  agent,
@@ -2,7 +2,12 @@ import { create } from 'zustand';
2
2
  import type { ChatComposerNode } from '@nextclaw/agent-chat-ui';
3
3
  import type { NcpDraftAttachment } from '@nextclaw/ncp-react';
4
4
  import type { RuntimeModelSelectionMode } from '@nextclaw/shared';
5
- import type { SessionSkillEntryView, SessionTypeIconView, ThinkingLevel } from '@/shared/lib/api';
5
+ import type {
6
+ RuntimeDefaultThinkingView,
7
+ SessionSkillEntryView,
8
+ SessionTypeIconView,
9
+ ThinkingLevel,
10
+ } from '@/shared/lib/api';
6
11
  import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
7
12
  import { createInitialChatComposerNodes } from '@/features/chat/features/input/utils/chat-composer-state.utils';
8
13
 
@@ -14,6 +19,7 @@ export type ChatInputSnapshot = {
14
19
  pendingSessionType: string;
15
20
  pendingProjectRoot: string | null;
16
21
  pendingProjectRootSessionKey: string | null;
22
+ defaultProjectRoot: string | null;
17
23
  defaultSessionType: string;
18
24
  canStopGeneration: boolean;
19
25
  stopDisabledReason: string | null;
@@ -32,6 +38,7 @@ export type ChatInputSnapshot = {
32
38
  supportedModels?: string[];
33
39
  recommendedModel?: string | null;
34
40
  modelSelectionMode?: RuntimeModelSelectionMode;
41
+ runtimeDefaultThinking?: RuntimeDefaultThinkingView | null;
35
42
  cta?: {
36
43
  kind: string;
37
44
  label?: string;
@@ -63,6 +70,7 @@ const initialSnapshot: ChatInputSnapshot = {
63
70
  pendingSessionType: 'native',
64
71
  pendingProjectRoot: null,
65
72
  pendingProjectRootSessionKey: null,
73
+ defaultProjectRoot: null,
66
74
  defaultSessionType: 'native',
67
75
  canStopGeneration: false,
68
76
  stopDisabledReason: null,