@nextclaw/ui 0.12.24 → 0.12.26

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 (210) hide show
  1. package/CHANGELOG.md +136 -29
  2. package/dist/assets/api-DGD9_Bg4.js +15 -0
  3. package/dist/assets/app-manager-provider-oYdeYPSv.js +1 -0
  4. package/dist/assets/{book-open-DDlN5MvX.js → book-open-BcnAiKde.js} +1 -1
  5. package/dist/assets/channels-list-page-HgLgrEg4.js +8 -0
  6. package/dist/assets/chat-page-DAKMFDrS.js +1 -0
  7. package/dist/assets/config-split-page-CcrEUtwu.js +1 -0
  8. package/dist/assets/cpu-DPPwMzoC.js +3 -0
  9. package/dist/assets/{createLucideIcon-BLMK3QUd.js → createLucideIcon-DzY6wN61.js} +1 -1
  10. package/dist/assets/desktop-DVUbOWbR.js +3 -0
  11. package/dist/assets/desktop-update-config-CP8dFYXK.js +1 -0
  12. package/dist/assets/{dialog-C3D7Be0p.js → dialog-BKo0RItd.js} +1 -1
  13. package/dist/assets/{dist-CPlbUgwU.js → dist-CFiwgaLs.js} +1 -1
  14. package/dist/assets/doc-browser-CAhfnm0D.js +1 -0
  15. package/dist/assets/{doc-browser-context-BJuMaI3o.js → doc-browser-context-FukQHvyo.js} +1 -1
  16. package/dist/assets/doc-browser-p9DDNPWB.js +1 -0
  17. package/dist/assets/doc-browser-rZIQIjuw.js +1 -0
  18. package/dist/assets/download-CMM8po31.js +1 -0
  19. package/dist/assets/{es2015-xqN1slyW.js → es2015-BhznEEyJ.js} +1 -1
  20. package/dist/assets/{external-link-DwfSfTLB.js → external-link-CpEvG65F.js} +1 -1
  21. package/dist/assets/i18n-D1144VAA.js +1 -0
  22. package/dist/assets/index-Cuwst6cc.js +100 -0
  23. package/dist/assets/index-dlcqieQ0.css +1 -0
  24. package/dist/assets/{key-round-CJ5gDAAG.js → key-round-DUq47t0P.js} +1 -1
  25. package/dist/assets/marketplace-page-BeFbwxR-.js +105 -0
  26. package/dist/assets/marketplace-page-CR4xq-TM.js +1 -0
  27. package/dist/assets/mcp-marketplace-page-DlRrSCj3.js +1 -0
  28. package/dist/assets/mcp-marketplace-page-DwnaLNTx.js +40 -0
  29. package/dist/assets/model-config-L2l6YAlQ.js +1 -0
  30. package/dist/assets/{notice-card-BFDbKQDA.js → notice-card-Dr6xCwva.js} +1 -1
  31. package/dist/assets/play-AqrNslHI.js +1 -0
  32. package/dist/assets/plus-B-YHtTNC.js +1 -0
  33. package/dist/assets/{popover-B86Dbfhf.js → popover-BDFNiLlg.js} +1 -1
  34. package/dist/assets/provider-scoped-model-input-BMTp4BEH.js +1 -0
  35. package/dist/assets/providers-list-DYAEunOp.js +1 -0
  36. package/dist/assets/refresh-cw-CrbD8EkT.js +1 -0
  37. package/dist/assets/remote-Dr3jcfWP.js +1 -0
  38. package/dist/assets/{rotate-cw-BZ2JObNs.js → rotate-cw-BN9yjccP.js} +1 -1
  39. package/dist/assets/runtime-config-page-BdeU8PEK.js +1 -0
  40. package/dist/assets/{save-euRxl8pI.js → save-CO_4qf6b.js} +1 -1
  41. package/dist/assets/{search-CLd7m0M7.js → search-CRtQwr-h.js} +1 -1
  42. package/dist/assets/search-config-CQUhd5RU.js +1 -0
  43. package/dist/assets/secrets-config-D-NWlW9q.js +3 -0
  44. package/dist/assets/{select-CJ0wbo3D.js → select-BUTwE_lC.js} +1 -1
  45. package/dist/assets/{setting-row-D1Yygqp7.js → setting-row-BavcnXw1.js} +1 -1
  46. package/dist/assets/settings-MWL2SMyk.js +1 -0
  47. package/dist/assets/{sparkles-DVfeSVJQ.js → sparkles-BmgOD4nY.js} +1 -1
  48. package/dist/assets/{status-dot-ChvPCib9.js → status-dot-l3kPFdq_.js} +1 -1
  49. package/dist/assets/{tabs-custom-Hia_ong0.js → tabs-custom-D48zdZoc.js} +1 -1
  50. package/dist/assets/{tag-chip-FrkmkT8r.js → tag-chip-Dm2Lqnpu.js} +1 -1
  51. package/dist/assets/use-config-Cyv5IuSt.js +1 -0
  52. package/dist/assets/use-infinite-scroll-loader-CFVdPpNv.js +1 -0
  53. package/dist/assets/x-BeyYA_h6.js +1 -0
  54. package/dist/index.html +29 -40
  55. package/package.json +9 -9
  56. package/src/app/components/layout/sidebar.layout.test.tsx +2 -4
  57. package/src/app/components/theme-provider.tsx +1 -0
  58. package/src/app/configs/app-navigation.config.ts +0 -6
  59. package/src/app/index.tsx +4 -7
  60. package/src/features/agents/components/agents-page.test.tsx +25 -15
  61. package/src/features/agents/components/agents-page.tsx +133 -172
  62. package/src/features/channels/components/config/channel-form.test.tsx +1 -0
  63. package/src/features/channels/components/config/channel-form.tsx +4 -3
  64. package/src/features/channels/components/config/weixin-channel-auth-section.test.tsx +38 -1
  65. package/src/features/channels/components/config/weixin-channel-auth-section.tsx +137 -40
  66. package/src/features/channels/index.ts +1 -1
  67. package/src/features/channels/utils/channel-form-fields.utils.test.ts +26 -0
  68. package/src/features/channels/utils/channel-form-fields.utils.ts +32 -18
  69. package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +23 -4
  70. package/src/features/chat/components/chat-session-workspace-panel.tsx +53 -35
  71. package/src/features/chat/components/chat-sidebar-session-item.tsx +16 -12
  72. package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +74 -0
  73. package/src/features/chat/components/conversation/chat-conversation-header.tsx +8 -2
  74. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +262 -114
  75. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +210 -174
  76. package/src/features/chat/components/conversation/chat-input-bar.container.tsx +11 -1
  77. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.test.tsx +24 -0
  78. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +27 -6
  79. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +174 -0
  80. package/src/features/chat/components/layout/chat-sidebar.test.tsx +45 -8
  81. package/src/features/chat/components/layout/chat-sidebar.tsx +29 -46
  82. package/src/features/chat/components/providers/chat-presenter.provider.tsx +4 -0
  83. package/src/features/chat/components/workspace/session-cron-job-content.tsx +103 -0
  84. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +153 -80
  85. package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
  86. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +1 -1
  87. package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
  88. package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
  89. package/src/features/chat/managers/chat-session-list.manager.test.ts +7 -9
  90. package/src/features/chat/managers/chat-session-list.manager.ts +5 -10
  91. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +20 -2
  92. package/src/features/chat/managers/ncp-chat-input.manager.ts +18 -0
  93. package/src/features/chat/managers/ncp-chat-presenter.manager.ts +7 -0
  94. package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
  95. package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
  96. package/src/features/chat/pages/ncp-chat-page.tsx +9 -5
  97. package/src/features/chat/stores/chat-input.store.ts +3 -1
  98. package/src/features/chat/stores/chat-session-list.store.ts +0 -2
  99. package/src/features/chat/stores/chat-thread.store.ts +4 -0
  100. package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
  101. package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
  102. package/src/features/chat/utils/ncp-chat-input-availability.utils.test.ts +1 -0
  103. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
  104. package/src/features/chat/utils/ncp-session-adapter.utils.ts +32 -0
  105. package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
  106. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
  107. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
  108. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
  109. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
  110. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
  111. package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
  112. package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
  113. package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
  114. package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
  115. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
  116. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  117. package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
  118. package/src/features/marketplace/components/marketplace-page.tsx +154 -132
  119. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
  120. package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
  121. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
  122. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
  123. package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
  124. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
  125. package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
  126. package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
  127. package/src/features/system-status/components/runtime-control-card.tsx +7 -6
  128. package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
  129. package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
  130. package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
  131. package/src/features/system-status/utils/system-status.utils.ts +31 -6
  132. package/src/index.css +8 -0
  133. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +68 -0
  134. package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
  135. package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
  136. package/src/platforms/desktop/index.ts +6 -0
  137. package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
  138. package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
  139. package/src/shared/components/common/brand-header.tsx +36 -16
  140. package/src/shared/components/config/provider-form-support.ts +2 -22
  141. package/src/shared/components/cron-config.tsx +12 -58
  142. package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
  143. package/src/shared/components/ui/select.tsx +19 -7
  144. package/src/shared/lib/api/channel-auth.types.ts +1 -0
  145. package/src/shared/lib/api/ncp-session.types.ts +9 -0
  146. package/src/shared/lib/api/types.ts +12 -1
  147. package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
  148. package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
  149. package/src/shared/lib/cron/index.ts +1 -0
  150. package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
  151. package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
  152. package/src/shared/lib/i18n/index.ts +20 -59
  153. package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
  154. package/src/shared/lib/provider-models/index.test.ts +39 -0
  155. package/src/shared/lib/provider-models/index.ts +1 -3
  156. package/src/shared/lib/ui-document-title/index.ts +0 -1
  157. package/tsconfig.json +1 -0
  158. package/vite.config.ts +1 -1
  159. package/vitest.config.ts +1 -1
  160. package/dist/assets/api-D2xRKmZd.js +0 -15
  161. package/dist/assets/app-manager-provider-CNaZboG4.js +0 -1
  162. package/dist/assets/app-navigation.config-Ihhrrt--.js +0 -1
  163. package/dist/assets/channels-list-page-p26lgxLk.js +0 -8
  164. package/dist/assets/chat-Dkh2qtuz.js +0 -61
  165. package/dist/assets/chat-page-DoTmE2wx.js +0 -1
  166. package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
  167. package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
  168. package/dist/assets/desktop-update-config-DlpzDfKM.js +0 -1
  169. package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
  170. package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
  171. package/dist/assets/doc-browser-p82AdNO-.js +0 -1
  172. package/dist/assets/folder-CeJKPx5P.js +0 -1
  173. package/dist/assets/hash-BqxRTZW5.js +0 -1
  174. package/dist/assets/i18n-DnTGDIRw.js +0 -1
  175. package/dist/assets/index-D8MKmXtO.css +0 -1
  176. package/dist/assets/index-pBvbJ5Mt.js +0 -2
  177. package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
  178. package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
  179. package/dist/assets/logos-C4sYP1Vl.js +0 -1
  180. package/dist/assets/marketplace-page-Cql0kDi-.js +0 -1
  181. package/dist/assets/marketplace-page-m4P5g_Ht.js +0 -49
  182. package/dist/assets/mcp-marketplace-page-9WVKl1m1.js +0 -1
  183. package/dist/assets/mcp-marketplace-page-ByzBQZcx.js +0 -40
  184. package/dist/assets/message-square-z_osm9c0.js +0 -1
  185. package/dist/assets/model-config-Dbr_0APb.js +0 -1
  186. package/dist/assets/play-Dv6Nr1Ew.js +0 -1
  187. package/dist/assets/plus-D8eKFY7h.js +0 -1
  188. package/dist/assets/provider-scoped-model-input-DFm6N2f7.js +0 -1
  189. package/dist/assets/providers-list-BJcLOjun.js +0 -1
  190. package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
  191. package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
  192. package/dist/assets/remote-BOxo9iwd.js +0 -1
  193. package/dist/assets/runtime-config-page-CjLhnbSl.js +0 -1
  194. package/dist/assets/search-config-J4Htco-P.js +0 -1
  195. package/dist/assets/secrets-config-CUdERjco.js +0 -3
  196. package/dist/assets/sessions-config-page-DpK991fs.js +0 -2
  197. package/dist/assets/settings-drbWqzA4.js +0 -1
  198. package/dist/assets/skeleton-BK1SOSRA.js +0 -1
  199. package/dist/assets/theme-provider-0hxjiPc_.js +0 -2
  200. package/dist/assets/tooltip-Cj4yA0gH.js +0 -1
  201. package/dist/assets/trash-2-CBsHCfqq.js +0 -1
  202. package/dist/assets/use-config-38Ur-89i.js +0 -1
  203. package/dist/assets/use-confirm-dialog-DPQThaeU.js +0 -1
  204. package/dist/assets/use-infinite-scroll-loader-5Gf1xQi7.js +0 -1
  205. package/dist/assets/use-viewport-layout-D1XzKeip.js +0 -1
  206. package/dist/assets/x-CM-XDMpk.js +0 -1
  207. package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
  208. package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
  209. package/src/features/chat/pages/sessions-config-page.tsx +0 -192
  210. /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
@@ -5,6 +5,8 @@ import {
5
5
  type NcpEndpointEvent,
6
6
  type NcpEndpointManifest,
7
7
  type NcpEndpointSubscriber,
8
+ type NcpMessage,
9
+ type NcpStreamRequestPayload,
8
10
  NcpEventType,
9
11
  } from "@nextclaw/ncp";
10
12
  import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -31,9 +33,57 @@ class DeferredSendClient implements NcpAgentClientEndpoint {
31
33
  readonly stream = vi.fn(async () => {});
32
34
  readonly abort = vi.fn(async () => {});
33
35
  private listeners = new Set<NcpEndpointSubscriber>();
34
- private releaseCompletion: (() => void) | null = null;
35
- private completionGate = new Promise<void>((resolve) => {
36
- this.releaseCompletion = resolve;
36
+
37
+ emit = async (event: NcpEndpointEvent): Promise<void> => {
38
+ this.publish(event);
39
+ };
40
+
41
+ subscribe = (listener: NcpEndpointSubscriber): (() => void) => {
42
+ this.listeners.add(listener);
43
+ return () => {
44
+ this.listeners.delete(listener);
45
+ };
46
+ };
47
+
48
+ send = vi.fn(async (envelope: NcpAgentSendEnvelope) => ({
49
+ sessionId: "session-created",
50
+ userMessageId: envelope.message.id,
51
+ assistantMessageId: "assistant-1",
52
+ runId: "run-1",
53
+ ...(envelope.correlationId ? { correlationId: envelope.correlationId } : {}),
54
+ }));
55
+
56
+ private publish = (event: NcpEndpointEvent): void => {
57
+ for (const listener of this.listeners) {
58
+ listener(event);
59
+ }
60
+ };
61
+ }
62
+
63
+ class ExistingSessionLiveClient implements NcpAgentClientEndpoint {
64
+ readonly manifest: NcpEndpointManifest = {
65
+ endpointKind: "agent",
66
+ endpointId: "existing-session-live-client",
67
+ version: "0.1.0",
68
+ supportsStreaming: true,
69
+ supportsAbort: true,
70
+ supportsProactiveMessages: false,
71
+ supportsLiveSessionStream: true,
72
+ supportedPartTypes: ["text"],
73
+ expectedLatency: "seconds",
74
+ };
75
+
76
+ readonly start = vi.fn(async () => {});
77
+ readonly abort = vi.fn(async () => {});
78
+ private listeners = new Set<NcpEndpointSubscriber>();
79
+ private liveStreamActive = false;
80
+
81
+ stop = vi.fn(async () => {
82
+ this.liveStreamActive = false;
83
+ });
84
+
85
+ stream = vi.fn(async (_payload: NcpStreamRequestPayload) => {
86
+ this.liveStreamActive = true;
37
87
  });
38
88
 
39
89
  emit = async (event: NcpEndpointEvent): Promise<void> => {
@@ -47,78 +97,59 @@ class DeferredSendClient implements NcpAgentClientEndpoint {
47
97
  };
48
98
  };
49
99
 
50
- send = vi.fn(async (_envelope: NcpAgentSendEnvelope): Promise<void> => {
51
- this.publish({
52
- type: NcpEventType.MessageSent,
53
- payload: {
54
- sessionId: "session-created",
55
- message: {
56
- id: "user-1",
57
- sessionId: "session-created",
58
- role: "user",
59
- status: "final",
60
- parts: [{ type: "text", text: "hello" }],
61
- timestamp: now,
100
+ send = vi.fn(async (envelope: NcpAgentSendEnvelope) => {
101
+ const events: NcpEndpointEvent[] = [
102
+ {
103
+ type: NcpEventType.RunStarted,
104
+ payload: {
105
+ sessionId: "session-existing",
106
+ messageId: "assistant-1",
107
+ runId: "run-1",
62
108
  },
63
109
  },
64
- });
65
- this.publish({
66
- type: NcpEventType.RunStarted,
67
- payload: {
68
- sessionId: "session-created",
69
- messageId: "assistant-1",
70
- runId: "run-1",
71
- },
72
- });
73
- await this.completionGate;
74
- this.publish({
75
- type: NcpEventType.MessageTextStart,
76
- payload: {
77
- sessionId: "session-created",
78
- messageId: "assistant-1",
79
- },
80
- });
81
- this.publish({
82
- type: NcpEventType.MessageTextDelta,
83
- payload: {
84
- sessionId: "session-created",
85
- messageId: "assistant-1",
86
- delta: "done",
110
+ {
111
+ type: NcpEventType.MessageTextStart,
112
+ payload: {
113
+ sessionId: "session-existing",
114
+ messageId: "assistant-1",
115
+ },
87
116
  },
88
- });
89
- this.publish({
90
- type: NcpEventType.MessageTextEnd,
91
- payload: {
92
- sessionId: "session-created",
93
- messageId: "assistant-1",
117
+ {
118
+ type: NcpEventType.MessageTextDelta,
119
+ payload: {
120
+ sessionId: "session-existing",
121
+ messageId: "assistant-1",
122
+ delta: "done",
123
+ },
94
124
  },
95
- });
96
- this.publish({
97
- type: NcpEventType.MessageCompleted,
98
- payload: {
99
- sessionId: "session-created",
100
- message: {
101
- id: "assistant-1",
102
- sessionId: "session-created",
103
- role: "assistant",
104
- status: "final",
105
- parts: [{ type: "text", text: "done" }],
106
- timestamp: now,
125
+ {
126
+ type: NcpEventType.MessageTextEnd,
127
+ payload: {
128
+ sessionId: "session-existing",
129
+ messageId: "assistant-1",
107
130
  },
108
131
  },
109
- });
110
- this.publish({
111
- type: NcpEventType.RunFinished,
112
- payload: {
113
- sessionId: "session-created",
114
- runId: "run-1",
132
+ {
133
+ type: NcpEventType.RunFinished,
134
+ payload: {
135
+ sessionId: "session-existing",
136
+ runId: "run-1",
137
+ },
115
138
  },
116
- });
117
- });
139
+ ];
118
140
 
119
- release = (): void => {
120
- this.releaseCompletion?.();
121
- };
141
+ if (this.liveStreamActive) {
142
+ for (const event of events) {
143
+ this.publish(event);
144
+ }
145
+ }
146
+ return {
147
+ sessionId: "session-existing",
148
+ userMessageId: envelope.message.id,
149
+ assistantMessageId: "assistant-1",
150
+ runId: "run-1",
151
+ };
152
+ });
122
153
 
123
154
  private publish = (event: NcpEndpointEvent): void => {
124
155
  for (const listener of this.listeners) {
@@ -127,12 +158,20 @@ class DeferredSendClient implements NcpAgentClientEndpoint {
127
158
  };
128
159
  }
129
160
 
161
+ function readAssistantText(messages: readonly NcpMessage[]): string {
162
+ const assistant = messages.find((message) => message.role === "assistant");
163
+ return assistant?.parts
164
+ .filter((part) => part.type === "text")
165
+ .map((part) => part.text ?? "")
166
+ .join("") ?? "";
167
+ }
168
+
130
169
  describe("useNcpAgentRuntime", () => {
131
170
  beforeEach(() => {
132
171
  vi.clearAllMocks();
133
172
  });
134
173
 
135
- it("keeps the active send stream alive when a new root chat materializes a session id", async () => {
174
+ it("returns a command handle when a new root chat materializes a session id", async () => {
136
175
  const client = new DeferredSendClient();
137
176
  const manager = new DefaultNcpAgentConversationStateManager();
138
177
  const envelope: NcpAgentSendEnvelope = {
@@ -150,30 +189,64 @@ describe("useNcpAgentRuntime", () => {
150
189
  { initialProps: { sessionId: undefined as string | undefined } },
151
190
  );
152
191
 
153
- let sendPromise: Promise<void>;
154
- act(() => {
155
- sendPromise = result.current.send(envelope);
192
+ let handle: Awaited<ReturnType<typeof result.current.send>> | null = null;
193
+ await act(async () => {
194
+ handle = await result.current.send(envelope);
156
195
  });
157
196
 
158
- await waitFor(() => {
159
- expect(result.current.snapshot.activeRun?.sessionId).toBe("session-created");
197
+ expect(handle).toEqual({
198
+ sessionId: "session-created",
199
+ userMessageId: "user-1",
200
+ assistantMessageId: "assistant-1",
201
+ runId: "run-1",
160
202
  });
203
+ expect(result.current.visibleMessages).toEqual([]);
204
+ expect(client.stop).not.toHaveBeenCalled();
161
205
 
162
206
  rerender({ sessionId: "session-created" });
207
+ });
163
208
 
164
- expect(client.stop).not.toHaveBeenCalled();
209
+ it("aborts by session id even before a hydrated active run reaches local state", async () => {
210
+ const client = new DeferredSendClient();
211
+ const manager = new DefaultNcpAgentConversationStateManager();
212
+ const { result } = renderHook(() =>
213
+ useNcpAgentRuntime({ sessionId: "session-running", client, manager: manager as never }),
214
+ );
215
+
216
+ await act(async () => {
217
+ await result.current.abort();
218
+ });
219
+
220
+ expect(client.abort).toHaveBeenCalledWith({ sessionId: "session-running" });
221
+ });
222
+
223
+ it("uses the hydrated live stream as the only event source while sending to an existing session", async () => {
224
+ const client = new ExistingSessionLiveClient();
225
+ const manager = new DefaultNcpAgentConversationStateManager();
226
+ await client.stream({ sessionId: "session-existing" });
227
+ const { result } = renderHook(() =>
228
+ useNcpAgentRuntime({ sessionId: "session-existing", client, manager: manager as never }),
229
+ );
165
230
 
166
231
  await act(async () => {
167
- client.release();
168
- await sendPromise;
232
+ await result.current.send({
233
+ sessionId: "session-existing",
234
+ message: {
235
+ id: "user-1",
236
+ sessionId: "session-existing",
237
+ role: "user",
238
+ status: "final",
239
+ parts: [{ type: "text", text: "hello" }],
240
+ timestamp: now,
241
+ },
242
+ });
169
243
  });
170
244
 
171
245
  await waitFor(() => {
172
246
  expect(result.current.snapshot.activeRun).toBeNull();
247
+ expect(readAssistantText(result.current.visibleMessages)).toBe("done");
173
248
  });
174
- expect(result.current.visibleMessages.map((message) => message.id)).toEqual([
175
- "user-1",
176
- "assistant-1",
177
- ]);
249
+ expect(client.stop).not.toHaveBeenCalled();
250
+ expect(client.stream).toHaveBeenCalledTimes(1);
178
251
  });
179
252
  });
@@ -0,0 +1,70 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { useNcpChatPageData } from './use-ncp-chat-page-data';
4
+
5
+ const useNcpSessionSkillsMock = vi.fn();
6
+
7
+ vi.mock('@/shared/hooks/use-config', () => ({
8
+ useConfig: () => ({
9
+ data: {
10
+ agents: { defaults: {} },
11
+ providers: {}
12
+ },
13
+ isFetched: true,
14
+ isSuccess: true
15
+ }),
16
+ useConfigMeta: () => ({
17
+ data: { providers: [] },
18
+ isFetched: true,
19
+ isSuccess: true
20
+ }),
21
+ useNcpSessions: () => ({
22
+ data: { sessions: [] }
23
+ }),
24
+ useNcpSessionSkills: (params: unknown) => useNcpSessionSkillsMock(params)
25
+ }));
26
+
27
+ vi.mock('./use-ncp-chat-session-types', () => ({
28
+ useNcpChatSessionTypes: () => ({
29
+ data: {
30
+ defaultType: 'native',
31
+ options: [{ value: 'native', label: 'Native' }]
32
+ }
33
+ })
34
+ }));
35
+
36
+ function renderPageData(params: { sessionKey: string | null }) {
37
+ return renderHook(() =>
38
+ useNcpChatPageData({
39
+ sessionKey: params.sessionKey,
40
+ query: '',
41
+ currentSelectedModel: '',
42
+ pendingSessionType: '',
43
+ setPendingSessionType: vi.fn(),
44
+ setSelectedModel: vi.fn(),
45
+ setSelectedThinkingLevel: vi.fn()
46
+ })
47
+ );
48
+ }
49
+
50
+ describe('useNcpChatPageData skills query', () => {
51
+ it('loads draft-session skills before a new chat materializes', () => {
52
+ useNcpSessionSkillsMock.mockReturnValue({ data: { records: [] }, isLoading: false });
53
+
54
+ renderPageData({ sessionKey: null });
55
+
56
+ expect(useNcpSessionSkillsMock).toHaveBeenCalledWith({
57
+ sessionId: 'draft-session'
58
+ });
59
+ });
60
+
61
+ it('loads real session skills after materialization', () => {
62
+ useNcpSessionSkillsMock.mockReturnValue({ data: { records: [] }, isLoading: false });
63
+
64
+ renderPageData({ sessionKey: 'session-1' });
65
+
66
+ expect(useNcpSessionSkillsMock).toHaveBeenCalledWith({
67
+ sessionId: 'session-1'
68
+ });
69
+ });
70
+ });
@@ -158,7 +158,7 @@ export function useNcpChatPageData(params: UseNcpChatPageDataParams) {
158
158
  const sessionsQuery = useNcpSessions({ limit: 200 });
159
159
  const sessionTypesQuery = useNcpChatSessionTypes();
160
160
  const sessionSkillsQuery = useNcpSessionSkills({
161
- sessionId: sessionKey ?? null,
161
+ sessionId: sessionKey?.trim() || 'draft-session',
162
162
  ...(Object.prototype.hasOwnProperty.call(params, 'projectRootOverride')
163
163
  ? { projectRoot: projectRootOverride ?? null }
164
164
  : {})
@@ -49,16 +49,10 @@ export function useNcpChildSessionTabsView(
49
49
  return new Map(sessions.map((session) => [session.key, session]));
50
50
  }, [summaries]);
51
51
 
52
- const summaryByKey = useMemo(
53
- () => new Map(summaries.map((summary) => [summary.sessionId, summary])),
54
- [summaries],
55
- );
56
-
57
52
  return useMemo(
58
53
  () =>
59
54
  tabs.map((tab) => {
60
55
  const session = sessionByKey.get(tab.sessionKey) ?? null;
61
- const summary = summaryByKey.get(tab.sessionKey) ?? null;
62
56
  const agentId = tab.agentId?.trim() || session?.agentId || null;
63
57
  return {
64
58
  sessionKey: tab.sessionKey,
@@ -68,7 +62,7 @@ export function useNcpChildSessionTabsView(
68
62
  updatedAt: session?.updatedAt ?? null,
69
63
  lastMessageAt: session?.lastMessageAt ?? null,
70
64
  readAt: session?.readAt ?? null,
71
- runStatus: summary?.status === "running" ? "running" : undefined,
65
+ runStatus: session?.status === "running" ? "running" : undefined,
72
66
  sessionTypeLabel: session?.sessionType
73
67
  ? resolveSessionTypeLabel(session.sessionType)
74
68
  : null,
@@ -77,6 +71,6 @@ export function useNcpChildSessionTabsView(
77
71
  projectRoot: session?.projectRoot?.trim() || null,
78
72
  };
79
73
  }),
80
- [sessionByKey, summaryByKey, tabs],
74
+ [sessionByKey, tabs],
81
75
  );
82
76
  }
@@ -32,11 +32,10 @@ export function useNcpSessionListView(params: { limit?: number } = {}) {
32
32
  shouldShowSessionInSidebar,
33
33
  );
34
34
  const filteredSessions = filterSessionsByQuery(sessions, query);
35
- const summaryBySessionId = new Map(summaries.map((summary) => [summary.sessionId, summary]));
36
35
 
37
36
  return filteredSessions.map((session) => ({
38
37
  session,
39
- runStatus: summaryBySessionId.get(session.key)?.status === 'running' ? 'running' : undefined
38
+ runStatus: session.status === 'running' ? 'running' : undefined
40
39
  }));
41
40
  }, [query, sessionsQuery.data?.sessions]);
42
41
 
@@ -35,7 +35,6 @@ describe('ChatSessionListManager', () => {
35
35
  snapshot: {
36
36
  ...useChatSessionListStore.getState().snapshot,
37
37
  selectedSessionKey: 'session-1',
38
- draftSessionKey: 'draft-root-1',
39
38
  listMode: 'time-first'
40
39
  }
41
40
  });
@@ -65,8 +64,8 @@ describe('ChatSessionListManager', () => {
65
64
  expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
66
65
  expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
67
66
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
68
- expect(useChatSessionListStore.getState().snapshot.draftSessionKey).toBeNull();
69
67
  expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
68
+ expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(false);
70
69
  expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
71
70
  expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
72
71
  expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
@@ -89,8 +88,8 @@ describe('ChatSessionListManager', () => {
89
88
  expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
90
89
  expect(useChatSessionListStore.getState().snapshot.selectedAgentId).toBe('researcher');
91
90
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
92
- expect(useChatSessionListStore.getState().snapshot.draftSessionKey).toBeNull();
93
91
  expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
92
+ expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(false);
94
93
  expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
95
94
  expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
96
95
  expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
@@ -117,8 +116,7 @@ describe('ChatSessionListManager', () => {
117
116
  useChatSessionListStore.setState({
118
117
  snapshot: {
119
118
  ...useChatSessionListStore.getState().snapshot,
120
- selectedSessionKey: null,
121
- draftSessionKey: 'draft-root-2'
119
+ selectedSessionKey: null
122
120
  }
123
121
  });
124
122
  const uiManager = {
@@ -137,6 +135,7 @@ describe('ChatSessionListManager', () => {
137
135
  expect(uiManager.goToChatRoot).not.toHaveBeenCalled();
138
136
  expect(uiManager.goToSession).not.toHaveBeenCalled();
139
137
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
138
+ expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(true);
140
139
  });
141
140
 
142
141
  it('does not eagerly replace the old selected session before the route finishes switching', () => {
@@ -225,12 +224,11 @@ describe('ChatSessionListManager', () => {
225
224
  expect(mocks.updateNcpSession).not.toHaveBeenCalled();
226
225
  });
227
226
 
228
- it('routes to the backend-materialized root session after the first send starts', () => {
227
+ it('routes to the backend-materialized root session without duplicating route-owned selection state', () => {
229
228
  useChatSessionListStore.setState({
230
229
  snapshot: {
231
230
  ...useChatSessionListStore.getState().snapshot,
232
231
  selectedSessionKey: null,
233
- draftSessionKey: 'draft-root-2',
234
232
  }
235
233
  });
236
234
  const uiManager = {
@@ -246,8 +244,8 @@ describe('ChatSessionListManager', () => {
246
244
  manager.ensureDraftSession('native');
247
245
  manager.materializeRootSessionRoute('ncp-materialized-session');
248
246
 
249
- expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('ncp-materialized-session');
250
- expect(useChatThreadStore.getState().snapshot.sessionKey).toBe('ncp-materialized-session');
247
+ expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
248
+ expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
251
249
  expect(uiManager.goToSession).toHaveBeenCalledWith('ncp-materialized-session', { replace: true });
252
250
  });
253
251
  });
@@ -12,7 +12,7 @@ export class ChatSessionListManager {
12
12
  private streamActionsManager: ChatStreamActionsManager
13
13
  ) {}
14
14
 
15
- private syncDraftThreadState = () => {
15
+ private syncDraftThreadState = (hasSubmittedDraftMessage = false) => {
16
16
  useChatThreadStore.getState().setSnapshot({
17
17
  sessionKey: null,
18
18
  sessionDisplayName: undefined,
@@ -21,9 +21,11 @@ export class ChatSessionListManager {
21
21
  messages: [],
22
22
  isSending: false,
23
23
  isAwaitingAssistantOutput: false,
24
+ hasSubmittedDraftMessage,
24
25
  parentSessionKey: null,
25
26
  parentSessionLabel: null,
26
27
  workspacePanelParentKey: null,
28
+ activeWorkspacePanelKind: null,
27
29
  childSessionTabs: [],
28
30
  activeChildSessionKey: null,
29
31
  activeWorkspaceFileKey: null,
@@ -108,7 +110,6 @@ export class ChatSessionListManager {
108
110
  this.streamActionsManager.resetStreamState();
109
111
  useChatSessionListStore.getState().setSnapshot({
110
112
  selectedSessionKey: null,
111
- draftSessionKey: null
112
113
  });
113
114
  this.syncDraftThreadState();
114
115
  useChatInputStore.getState().setSnapshot({
@@ -134,7 +135,7 @@ export class ChatSessionListManager {
134
135
  typeof sessionType === 'string' && sessionType.trim().length > 0
135
136
  ? sessionType.trim()
136
137
  : null;
137
- this.syncDraftThreadState();
138
+ this.syncDraftThreadState(true);
138
139
  if (normalizedSessionType) {
139
140
  useChatInputStore.getState().setSnapshot({ pendingSessionType: normalizedSessionType });
140
141
  }
@@ -149,19 +150,13 @@ export class ChatSessionListManager {
149
150
  if (!this.uiManager.isAtChatRoot()) {
150
151
  return;
151
152
  }
152
- useChatSessionListStore.getState().setSnapshot({
153
- selectedSessionKey: normalizedSessionKey,
154
- draftSessionKey: null,
155
- });
156
- useChatThreadStore.getState().setSnapshot({
157
- sessionKey: normalizedSessionKey,
158
- });
159
153
  this.uiManager.goToSession(normalizedSessionKey, { replace: true });
160
154
  };
161
155
 
162
156
  selectSession = (sessionKey: string) => {
163
157
  useChatThreadStore.getState().setSnapshot({
164
158
  workspacePanelParentKey: null,
159
+ activeWorkspacePanelKind: null,
165
160
  activeChildSessionKey: null,
166
161
  activeWorkspaceFileKey: null,
167
162
  });
@@ -15,6 +15,7 @@ describe('NcpChatInputManager', () => {
15
15
  composerNodes: [createChatComposerTextNode('hello from current thread')],
16
16
  attachments: [],
17
17
  selectedSkills: [],
18
+ composerFocusRequest: null,
18
19
  selectedSessionType: 'native',
19
20
  selectedModel: 'gpt-5',
20
21
  selectedThinkingLevel: null,
@@ -58,7 +59,6 @@ describe('NcpChatInputManager', () => {
58
59
  snapshot: {
59
60
  ...useChatSessionListStore.getState().snapshot,
60
61
  selectedSessionKey: 'stale-selected-session',
61
- draftSessionKey: 'draft-root-session',
62
62
  selectedAgentId: 'main',
63
63
  },
64
64
  });
@@ -109,7 +109,6 @@ describe('NcpChatInputManager', () => {
109
109
  snapshot: {
110
110
  ...useChatSessionListStore.getState().snapshot,
111
111
  selectedSessionKey: null,
112
- draftSessionKey: null,
113
112
  },
114
113
  });
115
114
  const streamActionsManager = {
@@ -211,4 +210,23 @@ describe('NcpChatInputManager', () => {
211
210
  expect(streamActionsManager.sendMessage).toHaveBeenCalledTimes(1);
212
211
  expect(sessionListManager.materializeRootSessionRoute).not.toHaveBeenCalled();
213
212
  });
213
+
214
+ it('creates and consumes one-shot composer focus requests', () => {
215
+ const manager = new NcpChatInputManager(
216
+ {} as ConstructorParameters<typeof NcpChatInputManager>[0],
217
+ {} as ConstructorParameters<typeof NcpChatInputManager>[1],
218
+ {} as ConstructorParameters<typeof NcpChatInputManager>[2],
219
+ );
220
+
221
+ manager.requestComposerFocusAtEnd();
222
+
223
+ const request = useChatInputStore.getState().snapshot.composerFocusRequest;
224
+ expect(request).toEqual({ id: 1, placement: 'end' });
225
+
226
+ manager.consumeComposerFocusRequest(999);
227
+ expect(useChatInputStore.getState().snapshot.composerFocusRequest).toEqual(request);
228
+
229
+ manager.consumeComposerFocusRequest(request!.id);
230
+ expect(useChatInputStore.getState().snapshot.composerFocusRequest).toBeNull();
231
+ });
214
232
  });
@@ -132,6 +132,24 @@ export class NcpChatInputManager {
132
132
  this.syncComposerSnapshot(createChatComposerNodesFromDraft(value));
133
133
  };
134
134
 
135
+ requestComposerFocusAtEnd = () => {
136
+ const currentRequest = useChatInputStore.getState().snapshot.composerFocusRequest;
137
+ useChatInputStore.getState().setSnapshot({
138
+ composerFocusRequest: {
139
+ id: (currentRequest?.id ?? 0) + 1,
140
+ placement: 'end',
141
+ },
142
+ });
143
+ };
144
+
145
+ consumeComposerFocusRequest = (requestId: number) => {
146
+ const currentRequest = useChatInputStore.getState().snapshot.composerFocusRequest;
147
+ if (currentRequest?.id !== requestId) {
148
+ return;
149
+ }
150
+ useChatInputStore.getState().setSnapshot({ composerFocusRequest: null });
151
+ };
152
+
135
153
  setComposerNodes = (next: SetStateAction<ChatComposerNode[]>) => {
136
154
  const prev = useChatInputStore.getState().snapshot.composerNodes;
137
155
  const value = this.resolveUpdateValue(prev, next);
@@ -18,4 +18,11 @@ export class NcpChatPresenter {
18
18
  this.chatSessionListManager,
19
19
  this.chatStreamActionsManager
20
20
  );
21
+
22
+ startAgentCreationDraft = (prompt: string) => {
23
+ this.chatSessionListManager.createSession();
24
+ this.chatSessionListManager.setSelectedAgentId('main');
25
+ this.chatInputManager.setDraft(prompt);
26
+ this.chatInputManager.requestComposerFocusAtEnd();
27
+ };
21
28
  }