@nextclaw/ui 0.14.1 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/assets/{api-DPcvCLfa.js → api-a14tR_z1.js} +7 -7
  3. package/dist/assets/channels-list-page-CFGkm4DT.js +8 -0
  4. package/dist/assets/chat-page-CEdavJXc.js +105 -0
  5. package/dist/assets/{config-split-page-BCJhqdK9.js → config-split-page-B5b9B0uE.js} +1 -1
  6. package/dist/assets/{confirm-dialog-CWh5FfK2.js → confirm-dialog-ClH_Ec7n.js} +1 -1
  7. package/dist/assets/{desktop-update-config-DsRhBizi.js → desktop-update-config-DPNDnVpk.js} +1 -1
  8. package/dist/assets/{dist-DPrgcEC0.js → dist-BtCS2Ucq.js} +1 -1
  9. package/dist/assets/{dist-DsE4OsSS.js → dist-CLGGLm8b.js} +1 -1
  10. package/dist/assets/doc-browser-5nMWYKBD.js +1 -0
  11. package/dist/assets/doc-browser-BJRLhztv.js +1 -0
  12. package/dist/assets/doc-browser-context-D4JwMMxK.js +1 -0
  13. package/dist/assets/{ellipsis-B81FStLm.js → ellipsis-CtJXjIol.js} +1 -1
  14. package/dist/assets/{external-link-C_dmG-WJ.js → external-link-CdJiG13p.js} +1 -1
  15. package/dist/assets/index-BDN1zzMQ.js +105 -0
  16. package/dist/assets/index-g3uSZXoc.css +1 -0
  17. package/dist/assets/mcp-marketplace-page-BU0kIjhR.js +40 -0
  18. package/dist/assets/mcp-marketplace-page-sMNFSq_T.js +1 -0
  19. package/dist/assets/middleware-CkFT3R4v.js +1 -0
  20. package/dist/assets/model-config-page-D6hYXROd.js +1 -0
  21. package/dist/assets/plus-DPjb0piO.js +1 -0
  22. package/dist/assets/provider-scoped-model-input-DAX2HgmR.js +1 -0
  23. package/dist/assets/providers-config-page-BzAeaACA.js +1 -0
  24. package/dist/assets/{react-sZz5HSCm.js → react-C6cZgP44.js} +1 -1
  25. package/dist/assets/remote-B0G80uPr.js +1 -0
  26. package/dist/assets/rotate-cw-DnEB1Rju.js +1 -0
  27. package/dist/assets/runtime-config-page-C5xKaV3i.js +1 -0
  28. package/dist/assets/{save-MzjoiHri.js → save-DKA0x4zk.js} +1 -1
  29. package/dist/assets/{search-B29dE4GN.js → search-BNlcoj5c.js} +1 -1
  30. package/dist/assets/{search-config-page-Dl5sZzob.js → search-config-page-Cq9uJ-N8.js} +1 -1
  31. package/dist/assets/{secrets-config-page-C_l-IFCK.js → secrets-config-page-CxSbZLBo.js} +2 -2
  32. package/dist/assets/{select-BKmh05hd.js → select-CvTlDZTQ.js} +1 -1
  33. package/dist/assets/{tag-chip-BS_7Ueom.js → tag-chip-8ffF2y2C.js} +1 -1
  34. package/dist/assets/use-config-DmT0o1gv.js +1 -0
  35. package/dist/assets/x-BiSSpgqB.js +1 -0
  36. package/dist/index.html +15 -14
  37. package/package.json +9 -9
  38. package/src/app/components/app-presenter-provider.tsx +11 -2
  39. package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
  40. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
  41. package/src/app/components/layout/app-layout.tsx +18 -4
  42. package/src/app/components/layout/sidebar-items.tsx +194 -71
  43. package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
  44. package/src/app/components/layout/sidebar.tsx +340 -83
  45. package/src/app/managers/viewport-layout.manager.ts +10 -0
  46. package/src/app/stores/viewport-layout.store.ts +39 -3
  47. package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
  48. package/src/features/agents/components/agents-page.tsx +10 -6
  49. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
  50. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
  51. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
  52. package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
  53. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +14 -15
  54. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +18 -18
  55. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
  56. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +16 -14
  58. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
  59. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +80 -7
  60. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +82 -25
  61. package/src/features/chat/components/layout/chat-sidebar.tsx +59 -92
  62. package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
  63. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +289 -0
  64. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +427 -0
  65. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +204 -0
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
  67. package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +32 -17
  68. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
  69. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
  70. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
  71. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
  72. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
  73. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
  74. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
  75. package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
  76. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
  77. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
  78. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
  79. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
  80. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
  81. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
  82. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
  83. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
  84. package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
  85. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
  86. package/src/features/chat/features/message/components/chat-message-list.container.tsx +93 -12
  87. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
  88. package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
  89. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  90. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  91. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  92. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
  93. package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
  94. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
  95. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +2 -2
  96. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +2 -0
  97. package/src/features/chat/features/session-type/components/chat-session-type-menu.tsx +2 -2
  98. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +2 -2
  99. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
  100. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -2
  101. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
  102. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +2 -2
  103. package/src/features/chat/features/welcome/components/chat-welcome.tsx +2 -2
  104. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
  106. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
  107. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
  108. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
  109. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
  110. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
  111. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
  112. package/src/features/chat/index.ts +0 -1
  113. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
  114. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
  115. package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
  116. package/src/features/chat/managers/chat-thread.manager.ts +108 -34
  117. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
  118. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
  119. package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
  120. package/src/features/chat/presenters/chat.presenter.ts +2 -19
  121. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
  122. package/src/features/chat/stores/chat-thread.store.ts +42 -26
  123. package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
  124. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
  125. package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
  126. package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
  127. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
  128. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
  129. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
  130. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  131. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
  132. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  133. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
  134. package/src/shared/lib/ui-document-title/index.ts +50 -21
  135. package/dist/assets/channels-list-page-D6jtziu4.js +0 -8
  136. package/dist/assets/chat-page-BBnNJoKG.js +0 -105
  137. package/dist/assets/doc-browser-C8xOF_9F.js +0 -1
  138. package/dist/assets/doc-browser-DgQedeAn.js +0 -1
  139. package/dist/assets/doc-browser-context-B46tQGmO.js +0 -1
  140. package/dist/assets/index-7o1WdUbU.js +0 -103
  141. package/dist/assets/index-DQWmEWzk.css +0 -1
  142. package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +0 -1
  143. package/dist/assets/mcp-marketplace-page-Dzull_4-.js +0 -40
  144. package/dist/assets/model-config-page-Cv0O3BIj.js +0 -1
  145. package/dist/assets/plus-Bnt56oqG.js +0 -1
  146. package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +0 -1
  147. package/dist/assets/providers-config-page-CPDz0Lxc.js +0 -1
  148. package/dist/assets/remote-B0LMs8u7.js +0 -1
  149. package/dist/assets/rotate-cw-BfyG_KEF.js +0 -1
  150. package/dist/assets/runtime-config-page-q0fvv8ME.js +0 -1
  151. package/dist/assets/use-config-DZkBWIdL.js +0 -1
  152. package/dist/assets/x-CW2KO-Hz.js +0 -1
  153. package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
  154. package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
  155. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
  156. package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
  157. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
  158. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
  159. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
  160. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
  161. package/src/features/chat/managers/chat-input.manager.ts +0 -430
  162. package/src/features/chat/managers/chat-run.manager.ts +0 -110
  163. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
  164. package/src/features/chat/stores/chat-input.store.ts +0 -104
  165. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
@@ -11,41 +11,14 @@ import {
11
11
  } from "@/features/chat/components/layout/chat-page-shell";
12
12
  import { parseSessionKeyFromRoute } from "@/features/chat/features/session/utils/chat-session-route.utils";
13
13
  import { ChatPresenter } from "@/features/chat/presenters/chat.presenter";
14
- import {
15
- isNcpAgentStartupUnavailableErrorMessage,
16
- useNcpSessionConversation,
17
- } from "@/features/chat/features/ncp/hooks/use-ncp-session-conversation";
18
- import {
19
- useNcpChatSelectedSession,
20
- } from "@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state";
21
14
  import { useChatQueryStoreSync } from "@/features/chat/features/ncp/hooks/use-ncp-chat-query-store-sync";
22
15
  import {
23
16
  ChatPresenterProvider,
24
17
  usePresenter,
25
18
  } from "@/features/chat/components/providers/chat-presenter.provider";
26
19
  import { useConfirmDialog } from "@/shared/hooks/use-confirm-dialog";
27
- import { useSystemStatus } from "@/features/system-status";
28
20
  import { useAppPresenter } from "@/app/components/app-presenter-provider";
29
- import { isNcpChatRuntimeBlocked, resolveNcpChatSendErrorMessage } from "@/features/chat/features/runtime/utils/ncp-chat-runtime-availability.utils";
30
21
  import { useUiShowContentEvent } from "@/features/chat/features/ncp/hooks/use-ui-show-content-event";
31
- import { readNcpContextWindowValue } from "@/features/chat/features/session/utils/ncp-session-context-metadata.utils";
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
22
 
50
23
  function useNcpChatRouteSelection() {
51
24
  const { sessionId: routeSessionIdParam } = useParams<{ sessionId?: string }>();
@@ -59,31 +32,6 @@ function useNcpChatRouteSelection() {
59
32
  };
60
33
  }
61
34
 
62
- type NcpChatConversation = ReturnType<typeof useNcpSessionConversation>;
63
-
64
- function useChatRunRuntimeConnection(params: {
65
- agent: NcpChatConversation;
66
- sessionKey: string | undefined;
67
- }) {
68
- const presenter = usePresenter();
69
- const { agent, sessionKey } = params;
70
- useEffect(() => {
71
- presenter.chatRunManager.setActiveRuntime({
72
- sessionKey: sessionKey ?? null,
73
- sendEnvelope: (envelope) => agent.send(envelope),
74
- abortCurrentRun: () => agent.abort(),
75
- resumeCurrentSessionRun: () => agent.streamRun(),
76
- });
77
- return () => {
78
- presenter.chatRunManager.setActiveRuntime(null);
79
- };
80
- }, [
81
- agent,
82
- presenter,
83
- sessionKey,
84
- ]);
85
- }
86
-
87
35
  function useNcpChatUiBindings() {
88
36
  const presenter = usePresenter();
89
37
  const { confirm, ConfirmDialog } = useConfirmDialog();
@@ -101,42 +49,6 @@ function useNcpChatUiBindings() {
101
49
  return <ConfirmDialog />;
102
50
  }
103
51
 
104
- function useChatRunSnapshotSync(params: {
105
- agent: NcpChatConversation;
106
- isSending: boolean;
107
- isRunning: boolean;
108
- lastSendError: string | null;
109
- routeSessionKey: string | null;
110
- }) {
111
- const presenter = usePresenter();
112
- const { agent, isRunning, isSending, lastSendError, routeSessionKey } = params;
113
- useEffect(() => {
114
- presenter.chatRunManager.applyRunSnapshot({
115
- routeSessionKey,
116
- isHydrating: agent.isHydrating,
117
- isSending,
118
- isRunning,
119
- visibleMessages: agent.visibleMessages,
120
- contextWindow: readNcpContextWindowValue(agent.snapshot.contextWindow),
121
- sendErrorMessage: lastSendError,
122
- materializedSessionKey:
123
- agent.snapshot.activeRun?.sessionId ??
124
- agent.visibleMessages.find((message) => message.sessionId.trim())?.sessionId ??
125
- null,
126
- });
127
- }, [
128
- agent.isHydrating,
129
- agent.snapshot.activeRun?.sessionId,
130
- agent.snapshot.contextWindow,
131
- agent.visibleMessages,
132
- isRunning,
133
- isSending,
134
- lastSendError,
135
- presenter,
136
- routeSessionKey,
137
- ]);
138
- }
139
-
140
52
  export function NcpChatPage({ view }: ChatPageProps) {
141
53
  const appPresenter = useAppPresenter();
142
54
  const [presenter] = useState(() => new ChatPresenter(appPresenter));
@@ -149,52 +61,18 @@ export function NcpChatPage({ view }: ChatPageProps) {
149
61
 
150
62
  function NcpChatPageContent({ view }: ChatPageProps) {
151
63
  const presenter = usePresenter();
152
- const systemStatus = useSystemStatus();
153
- const isRuntimeBlocked = isNcpChatRuntimeBlocked(systemStatus);
154
64
  const confirmDialog = useNcpChatUiBindings();
155
65
  const routeSelection = useNcpChatRouteSelection();
156
66
  const { routeSessionKey, sessionKey } = routeSelection;
157
67
  useChatQueryStoreSync({
158
68
  sessionKey: sessionKey ?? null,
159
69
  });
160
- const selectedSession = useNcpChatSelectedSession(sessionKey ?? null);
161
- const agent = useNcpSessionConversation(sessionKey);
162
- const rawLastSendError =
163
- agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
164
- const filteredLastSendError =
165
- systemStatus.phase === "ready" &&
166
- isNcpAgentStartupUnavailableErrorMessage(rawLastSendError)
167
- ? null
168
- : rawLastSendError;
169
- const currentSessionRunning = agent.isRunning || selectedSession?.status === "running";
170
- const isSending = agent.isSending || currentSessionRunning;
171
- const lastSendError =
172
- isRuntimeBlocked
173
- ? null
174
- : systemStatus.phase === "ready"
175
- ? filteredLastSendError
176
- : resolveNcpChatSendErrorMessage({
177
- message: filteredLastSendError,
178
- status: systemStatus,
179
- });
180
- useChatRunRuntimeConnection({
181
- agent,
182
- sessionKey,
183
- });
184
70
  useChatSessionSync({
185
71
  view,
186
72
  routeSessionKey,
187
73
  syncRouteSessionSelection:
188
74
  presenter.chatSessionListManager.syncRouteSessionSelection,
189
75
  });
190
- useChatDraftIntentConsumer();
191
76
  useUiShowContentEvent();
192
- useChatRunSnapshotSync({
193
- agent,
194
- isRunning: currentSessionRunning,
195
- routeSessionKey,
196
- lastSendError,
197
- isSending,
198
- });
199
77
  return <ChatPageLayout view={view} confirmDialog={confirmDialog} />;
200
78
  }
@@ -1,39 +1,22 @@
1
1
  import { ChatSessionListManager } from '@/features/chat/managers/chat-session-list.manager';
2
- import { ChatRunManager } from '@/features/chat/managers/chat-run.manager';
3
2
  import { ChatUiManager } from '@/features/chat/managers/chat-ui.manager';
4
- import { ChatInputManager } from '@/features/chat/managers/chat-input.manager';
5
3
  import { ChatQueryManager } from '@/features/chat/managers/chat-query.manager';
6
4
  import { ChatThreadManager } from '@/features/chat/managers/chat-thread.manager';
7
5
  import type { AppPresenter } from '@/app/presenters/app.presenter';
8
6
 
9
7
  export class ChatPresenter {
10
8
  readonly chatUiManager: ChatUiManager;
11
- readonly chatRunManager: ChatRunManager;
12
9
  readonly chatSessionListManager: ChatSessionListManager;
13
- readonly chatInputManager: ChatInputManager;
14
10
  readonly chatQueryManager: ChatQueryManager;
15
11
  readonly chatThreadManager: ChatThreadManager;
16
12
 
17
13
  constructor(appPresenter: AppPresenter) {
18
14
  this.chatUiManager = new ChatUiManager(appPresenter.docBrowserManager);
19
- this.chatRunManager = new ChatRunManager(this.chatUiManager);
20
- this.chatSessionListManager = new ChatSessionListManager(this.chatUiManager, this.chatRunManager);
15
+ this.chatSessionListManager = new ChatSessionListManager(this.chatUiManager);
21
16
  this.chatQueryManager = new ChatQueryManager();
22
- this.chatInputManager = new ChatInputManager(
23
- this.chatRunManager,
24
- this.chatSessionListManager
25
- );
26
17
  this.chatThreadManager = new ChatThreadManager(
27
18
  this.chatUiManager,
28
- this.chatSessionListManager,
29
- this.chatRunManager
19
+ this.chatSessionListManager
30
20
  );
31
21
  }
32
-
33
- startAgentCreationDraft = (prompt: string) => {
34
- this.chatSessionListManager.createSession();
35
- this.chatSessionListManager.setSelectedAgentId('main');
36
- this.chatInputManager.setDraft(prompt);
37
- this.chatInputManager.requestComposerFocusAtEnd();
38
- };
39
22
  }
@@ -22,6 +22,7 @@ function resetChatThreadStore() {
22
22
  activeWorkspacePanelKind: null,
23
23
  childSessionTabs: [],
24
24
  activeChildSessionKey: null,
25
+ activeSideChatDraft: null,
25
26
  workspaceFileTabs: [],
26
27
  activeWorkspaceFileKey: null,
27
28
  workspaceNavigationHistory: [],
@@ -85,6 +86,36 @@ describe('chat thread workspace panel persistence', () => {
85
86
  expect(persisted.state.snapshot.workspaceFileTabs[0].fullLines).toBeUndefined();
86
87
  });
87
88
 
89
+ it('does not persist an unmaterialized side chat draft', () => {
90
+ useChatThreadStore.getState().setSnapshot({
91
+ workspacePanelParentKey: 'session-1',
92
+ activeWorkspacePanelKind: 'side-chat-draft',
93
+ activeSideChatDraft: {
94
+ draftKey: 'draft-1',
95
+ parentSessionKey: 'session-1',
96
+ },
97
+ workspaceNavigationHistory: [
98
+ { kind: 'child-session', key: 'child-session-1' },
99
+ { kind: 'side-chat-draft', key: 'draft-1' },
100
+ ],
101
+ workspaceNavigationHistoryIndex: 1,
102
+ });
103
+
104
+ const persisted = JSON.parse(
105
+ window.localStorage.getItem(chatThreadWorkspaceStorageKey) ?? '{}',
106
+ );
107
+
108
+ expect(persisted.state.snapshot).toMatchObject({
109
+ workspacePanelParentKey: 'session-1',
110
+ activeWorkspacePanelKind: null,
111
+ workspaceNavigationHistory: [
112
+ { kind: 'child-session', key: 'child-session-1' },
113
+ ],
114
+ workspaceNavigationHistoryIndex: 0,
115
+ });
116
+ expect(persisted.state.snapshot.activeSideChatDraft).toBeUndefined();
117
+ });
118
+
88
119
  it('hydrates the workspace panel state and repairs stale active file keys', async () => {
89
120
  window.localStorage.setItem(
90
121
  chatThreadWorkspaceStorageKey,
@@ -14,6 +14,11 @@ export type ChatChildSessionTab = {
14
14
  agentId?: string | null;
15
15
  };
16
16
 
17
+ export type ChatWorkspaceSideChatDraft = {
18
+ draftKey: string;
19
+ parentSessionKey: string;
20
+ };
21
+
17
22
  export type ChatWorkspaceFileTab = {
18
23
  key: string;
19
24
  parentSessionKey: string | null;
@@ -32,17 +37,10 @@ export type ChatWorkspaceFileTab = {
32
37
  };
33
38
 
34
39
  export type ChatWorkspaceNavigationEntry =
35
- | {
36
- kind: 'child-session';
37
- key: string;
38
- }
39
- | {
40
- kind: 'file';
41
- key: string;
42
- }
43
- | {
44
- kind: 'cron';
45
- };
40
+ | { kind: 'child-session'; key: string }
41
+ | { kind: 'side-chat-draft'; key: string }
42
+ | { kind: 'file'; key: string }
43
+ | { kind: 'cron' };
46
44
 
47
45
  export type ChatThreadSnapshot = {
48
46
  sessionTypeLabel?: string | null;
@@ -63,9 +61,10 @@ export type ChatThreadSnapshot = {
63
61
  parentSessionKey?: string | null;
64
62
  parentSessionLabel?: string | null;
65
63
  workspacePanelParentKey?: string | null;
66
- activeWorkspacePanelKind?: "child-session" | "file" | "cron" | null;
64
+ activeWorkspacePanelKind?: "child-session" | "side-chat-draft" | "file" | "cron" | null;
67
65
  childSessionTabs: ChatChildSessionTab[];
68
66
  activeChildSessionKey?: string | null;
67
+ activeSideChatDraft?: ChatWorkspaceSideChatDraft | null;
69
68
  workspaceFileTabs: ChatWorkspaceFileTab[];
70
69
  activeWorkspaceFileKey?: string | null;
71
70
  workspaceNavigationHistory: ChatWorkspaceNavigationEntry[];
@@ -105,6 +104,8 @@ type PersistedChatWorkspaceSnapshot = Pick<
105
104
  | 'workspaceNavigationHistoryIndex'
106
105
  >;
107
106
 
107
+ type PersistedWorkspacePanelKind = 'child-session' | 'file' | 'cron';
108
+
108
109
  const initialSnapshot: ChatThreadSnapshot = {
109
110
  sessionTypeLabel: null,
110
111
  sessionTypeIcon: null,
@@ -127,6 +128,7 @@ const initialSnapshot: ChatThreadSnapshot = {
127
128
  activeWorkspacePanelKind: null,
128
129
  childSessionTabs: [],
129
130
  activeChildSessionKey: null,
131
+ activeSideChatDraft: null,
130
132
  workspaceFileTabs: [],
131
133
  activeWorkspaceFileKey: null,
132
134
  workspaceNavigationHistory: [],
@@ -158,7 +160,7 @@ function normalizeHistoryIndex(value: unknown, maxIndex: number): number {
158
160
 
159
161
  function isWorkspacePanelKind(
160
162
  value: unknown,
161
- ): value is NonNullable<ChatThreadSnapshot['activeWorkspacePanelKind']> {
163
+ ): value is PersistedWorkspacePanelKind {
162
164
  return value === 'child-session' || value === 'file' || value === 'cron';
163
165
  }
164
166
 
@@ -344,19 +346,33 @@ export const useChatThreadStore = create<ChatThreadStore>()(
344
346
  name: CHAT_THREAD_WORKSPACE_STORAGE_KEY,
345
347
  version: CHAT_THREAD_WORKSPACE_STORAGE_VERSION,
346
348
  storage: createJSONStorage(() => window.localStorage),
347
- partialize: (state): PersistedChatThreadStore => ({
348
- snapshot: {
349
- workspacePanelParentKey: state.snapshot.workspacePanelParentKey,
350
- activeWorkspacePanelKind: state.snapshot.activeWorkspacePanelKind,
351
- activeChildSessionKey: state.snapshot.activeChildSessionKey,
352
- workspaceFileTabs: state.snapshot.workspaceFileTabs
353
- .slice(-CHAT_THREAD_MAX_PERSISTED_WORKSPACE_FILE_TABS)
354
- .map(toPersistedWorkspaceFileTab),
355
- activeWorkspaceFileKey: state.snapshot.activeWorkspaceFileKey,
356
- workspaceNavigationHistory: state.snapshot.workspaceNavigationHistory,
357
- workspaceNavigationHistoryIndex: state.snapshot.workspaceNavigationHistoryIndex,
358
- }
359
- }),
349
+ partialize: (state): PersistedChatThreadStore => {
350
+ const workspaceNavigationHistory = state.snapshot.workspaceNavigationHistory
351
+ .filter((entry) => entry.kind !== 'side-chat-draft');
352
+ const workspaceNavigationHistoryIndex =
353
+ workspaceNavigationHistory.length > 0
354
+ ? Math.min(
355
+ state.snapshot.workspaceNavigationHistoryIndex,
356
+ workspaceNavigationHistory.length - 1,
357
+ )
358
+ : 0;
359
+ return {
360
+ snapshot: {
361
+ workspacePanelParentKey: state.snapshot.workspacePanelParentKey,
362
+ activeWorkspacePanelKind:
363
+ state.snapshot.activeWorkspacePanelKind === 'side-chat-draft'
364
+ ? null
365
+ : state.snapshot.activeWorkspacePanelKind,
366
+ activeChildSessionKey: state.snapshot.activeChildSessionKey,
367
+ workspaceFileTabs: state.snapshot.workspaceFileTabs
368
+ .slice(-CHAT_THREAD_MAX_PERSISTED_WORKSPACE_FILE_TABS)
369
+ .map(toPersistedWorkspaceFileTab),
370
+ activeWorkspaceFileKey: state.snapshot.activeWorkspaceFileKey,
371
+ workspaceNavigationHistory,
372
+ workspaceNavigationHistoryIndex,
373
+ }
374
+ };
375
+ },
360
376
  merge: (persistedState, currentState) => {
361
377
  const persistedSnapshot = isRecord(persistedState)
362
378
  ? persistedState.snapshot
@@ -4,10 +4,11 @@ import type { PanelAppPreferencesUpdateView } from '@/shared/lib/api';
4
4
 
5
5
  const PANEL_APPS_QUERY_KEY = ['panel-apps'] as const;
6
6
 
7
- export function usePanelApps() {
7
+ export function usePanelApps(options: { enabled?: boolean } = {}) {
8
8
  return useQuery({
9
9
  queryKey: PANEL_APPS_QUERY_KEY,
10
10
  queryFn: () => nextclawClient.panelApps.listPanelApps(),
11
+ enabled: options.enabled ?? true,
11
12
  staleTime: 0,
12
13
  });
13
14
  }
@@ -1,8 +1,9 @@
1
1
  import { act, render, screen } from "@testing-library/react";
2
2
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
3
  import { MemoryRouter } from "react-router-dom";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
  import { DesktopAppShell } from "@/platforms/desktop/components/desktop-app-shell";
6
+ import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
6
7
 
7
8
  vi.mock("@/app/components/layout/sidebar", () => ({
8
9
  Sidebar: () => <aside data-testid="settings-sidebar">Settings Sidebar</aside>,
@@ -16,7 +17,10 @@ type WindowStateListener = (snapshot: { isMaximized: boolean }) => void;
16
17
 
17
18
  let windowStateListener: WindowStateListener | null = null;
18
19
 
19
- function setDesktopPlatform(platform: string | null, isMaximized = false): void {
20
+ function setDesktopPlatform(
21
+ platform: string | null,
22
+ isMaximized = false,
23
+ ): void {
20
24
  windowStateListener = null;
21
25
  window.nextclawDesktop = platform
22
26
  ? ({
@@ -59,6 +63,11 @@ function renderDesktopShell(platform: string | null, isMobileLayout = false) {
59
63
  }
60
64
 
61
65
  describe("DesktopAppShell", () => {
66
+ beforeEach(() => {
67
+ window.localStorage.clear();
68
+ viewportLayoutManager.resetForTests();
69
+ });
70
+
62
71
  afterEach(() => {
63
72
  setDesktopPlatform(null);
64
73
  });
@@ -68,29 +77,58 @@ describe("DesktopAppShell", () => {
68
77
 
69
78
  const chrome = screen.getByTestId("desktop-window-chrome");
70
79
  const sidebarChrome = screen.getByTestId("desktop-window-chrome-sidebar");
71
- const resizeStrip = screen.getByTestId("desktop-window-chrome-resize-strip");
80
+ const resizeStrip = screen.getByTestId(
81
+ "desktop-window-chrome-resize-strip",
82
+ );
72
83
  const controls = screen.getByTestId("desktop-window-controls");
73
84
 
74
85
  expect(chrome).toBeTruthy();
75
- expect(chrome.parentElement?.style.getPropertyValue("--desktop-titlebar-height")).toBe("40px");
86
+ expect(
87
+ chrome.parentElement?.style.getPropertyValue("--desktop-titlebar-height"),
88
+ ).toBe("40px");
76
89
  expect(chrome.className).toContain("bg-secondary");
77
90
  expect(chrome.className).not.toContain(" shrink-0 border-b ");
78
- expect(chrome.className).toContain("after:left-[var(--desktop-sidebar-width)]");
91
+ expect(chrome.className).toContain(
92
+ "after:left-[var(--desktop-sidebar-width)]",
93
+ );
79
94
  expect(chrome.className).toContain("after:border-b");
80
95
  expect(chrome.className).toContain("desktop-window-drag");
81
- expect(sidebarChrome.className).toContain("w-[var(--desktop-sidebar-width)]");
96
+ expect(sidebarChrome.className).toContain(
97
+ "w-[var(--desktop-sidebar-width)]",
98
+ );
82
99
  expect(sidebarChrome.className).not.toContain("border-b");
83
100
  expect(sidebarChrome.className).toContain("desktop-window-drag");
84
101
  expect(resizeStrip.className).toContain("desktop-window-no-drag");
85
102
  expect(resizeStrip.className).toContain("top-0");
86
103
  expect(resizeStrip.className).toContain("h-1");
87
104
  expect(controls.className).toContain("desktop-window-no-drag");
88
- expect(screen.getByLabelText("Minimize").className).toContain("desktop-window-no-drag");
89
- expect(screen.getByLabelText("Maximize").className).toContain("desktop-window-no-drag");
90
- expect(screen.getByLabelText("Close").className).toContain("desktop-window-no-drag");
105
+ expect(screen.getByLabelText("Minimize").className).toContain(
106
+ "desktop-window-no-drag",
107
+ );
108
+ expect(screen.getByLabelText("Maximize").className).toContain(
109
+ "desktop-window-no-drag",
110
+ );
111
+ expect(screen.getByLabelText("Close").className).toContain(
112
+ "desktop-window-no-drag",
113
+ );
91
114
  expect(screen.getByTestId("app-content")).toBeTruthy();
92
115
  });
93
116
 
117
+ it("uses the collapsed sidebar width in Windows chrome from the shared layout store", () => {
118
+ viewportLayoutManager.setSidebarCollapsed(true);
119
+
120
+ renderDesktopShell("win32");
121
+
122
+ const chromeRoot = screen.getByTestId(
123
+ "desktop-window-chrome",
124
+ ).parentElement;
125
+
126
+ expect(chromeRoot?.style.getPropertyValue("--desktop-sidebar-width")).toBe(
127
+ "56px",
128
+ );
129
+ expect(screen.getByAltText("NextClaw")).toBeTruthy();
130
+ });
131
+
94
132
  it("switches the Windows maximize button to restore while maximized", async () => {
95
133
  renderDesktopShell("win32");
96
134
 
@@ -100,7 +138,9 @@ describe("DesktopAppShell", () => {
100
138
  windowStateListener?.({ isMaximized: true });
101
139
  });
102
140
 
103
- expect(screen.getByLabelText("Restore").className).toContain("desktop-window-no-drag");
141
+ expect(screen.getByLabelText("Restore").className).toContain(
142
+ "desktop-window-no-drag",
143
+ );
104
144
  expect(screen.queryByLabelText("Maximize")).toBeNull();
105
145
  });
106
146
 
@@ -11,9 +11,12 @@ import { MobileBottomNav } from "@/platforms/mobile";
11
11
  import type { DocBrowserCustomTabRenderers } from "@/shared/components/doc-browser/doc-browser-renderer.types";
12
12
  import type { DocBrowserDockControls } from "@/shared/components/doc-browser/doc-browser-context";
13
13
  import { cn } from "@/shared/lib/utils";
14
+ import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
15
+ import { SIDEBAR_RAIL_WIDTH_PX } from "@/app/components/layout/sidebar-rail.styles";
14
16
 
15
17
  const DocBrowser = lazy(async () => ({
16
- default: (await import("@/shared/components/doc-browser/doc-browser")).DocBrowser,
18
+ default: (await import("@/shared/components/doc-browser/doc-browser"))
19
+ .DocBrowser,
17
20
  }));
18
21
 
19
22
  type DesktopAppShellProps = {
@@ -38,8 +41,17 @@ export function DesktopAppShell({
38
41
  children,
39
42
  }: DesktopAppShellProps) {
40
43
  const isMainRoute = isMainWorkspaceRoute(pathname);
41
- const showMobileBottomNav = isMobileLayout && !isChatSessionDetailRoute(pathname);
44
+ const isSidebarCollapsed = useViewportLayoutStore(
45
+ (state) => state.isSidebarCollapsed,
46
+ );
47
+ const showMobileBottomNav =
48
+ isMobileLayout && !isChatSessionDetailRoute(pathname);
42
49
  const shouldUseWindowsChrome = isWindowsDesktopHost();
50
+ const desktopSidebarWidth = isSidebarCollapsed
51
+ ? `${SIDEBAR_RAIL_WIDTH_PX}px`
52
+ : isMainRoute
53
+ ? "280px"
54
+ : "240px";
43
55
 
44
56
  return (
45
57
  <div
@@ -47,13 +59,19 @@ export function DesktopAppShell({
47
59
  "h-screen flex flex-col overflow-hidden bg-background font-sans text-foreground",
48
60
  shouldUseWindowsChrome ? "rounded-[10px]" : null,
49
61
  )}
50
- style={shouldUseWindowsChrome ? ({
51
- "--desktop-titlebar-height": "40px",
52
- "--desktop-caption-safe-right": "140px",
53
- "--desktop-sidebar-width": isMainRoute ? "280px" : "240px",
54
- } as CSSProperties) : undefined}
62
+ style={
63
+ shouldUseWindowsChrome
64
+ ? ({
65
+ "--desktop-titlebar-height": "40px",
66
+ "--desktop-caption-safe-right": "140px",
67
+ "--desktop-sidebar-width": desktopSidebarWidth,
68
+ } as CSSProperties)
69
+ : undefined
70
+ }
55
71
  >
56
- {shouldUseWindowsChrome ? <DesktopWindowChrome /> : null}
72
+ {shouldUseWindowsChrome ? (
73
+ <DesktopWindowChrome sidebarCollapsed={isSidebarCollapsed} />
74
+ ) : null}
57
75
  <div className="flex min-h-0 flex-1 overflow-hidden">
58
76
  {!isMainRoute && <Sidebar mode="settings" />}
59
77
  <div className="flex-1 flex min-w-0 overflow-hidden relative">
@@ -70,7 +88,10 @@ export function DesktopAppShell({
70
88
  </div>
71
89
  {isDocBrowserOpen && docBrowserMode === "docked" ? (
72
90
  <Suspense fallback={null}>
73
- <DocBrowser customTabRenderers={docBrowserRenderers} dockControls={docBrowserDockControls} />
91
+ <DocBrowser
92
+ customTabRenderers={docBrowserRenderers}
93
+ dockControls={docBrowserDockControls}
94
+ />
74
95
  </Suspense>
75
96
  ) : null}
76
97
  {sideDock}
@@ -79,7 +100,10 @@ export function DesktopAppShell({
79
100
  {showMobileBottomNav ? <MobileBottomNav /> : null}
80
101
  {isDocBrowserOpen && docBrowserMode === "floating" ? (
81
102
  <Suspense fallback={null}>
82
- <DocBrowser customTabRenderers={docBrowserRenderers} dockControls={docBrowserDockControls} />
103
+ <DocBrowser
104
+ customTabRenderers={docBrowserRenderers}
105
+ dockControls={docBrowserDockControls}
106
+ />
83
107
  </Suspense>
84
108
  ) : null}
85
109
  </div>
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { Copy, Minus, Square, X } from "lucide-react";
3
3
  import { BrandHeader } from "@/shared/components/common/brand-header";
4
+ import { cn } from "@/shared/lib/utils";
4
5
  import type { DesktopWindowStateSnapshot } from "@/platforms/desktop/types/desktop-update.types";
5
6
 
6
7
  type DesktopWindowControlAction = "minimize" | "toggle-maximize" | "close";
@@ -17,7 +18,11 @@ const windowControls: WindowControlDefinition[] = [
17
18
  { action: "close", label: "Close", icon: X, variant: "danger" },
18
19
  ];
19
20
 
20
- export function DesktopWindowChrome() {
21
+ export function DesktopWindowChrome({
22
+ sidebarCollapsed = false,
23
+ }: {
24
+ sidebarCollapsed?: boolean;
25
+ }) {
21
26
  const isMaximized = useDesktopWindowMaximizedState();
22
27
  const maximizeControl: WindowControlDefinition = isMaximized
23
28
  ? { action: "toggle-maximize", label: "Restore", icon: Copy }
@@ -33,21 +38,37 @@ export function DesktopWindowChrome() {
33
38
  data-testid="desktop-window-chrome-resize-strip"
34
39
  />
35
40
  <div
36
- className="desktop-window-drag relative z-10 flex h-full w-[var(--desktop-sidebar-width)] shrink-0 items-center bg-secondary pl-4 pr-3 text-secondary-foreground"
41
+ className={cn(
42
+ "desktop-window-drag relative z-10 flex h-full w-[var(--desktop-sidebar-width)] shrink-0 items-center bg-secondary text-secondary-foreground",
43
+ sidebarCollapsed ? "justify-center px-0" : "pl-4 pr-3",
44
+ )}
37
45
  data-testid="desktop-window-chrome-sidebar"
38
46
  >
39
- <BrandHeader
40
- className="flex min-w-0 shrink-0 items-center gap-2.5"
41
- density="chrome"
42
- />
47
+ {sidebarCollapsed ? (
48
+ <img
49
+ src="/logo.svg"
50
+ alt="NextClaw"
51
+ className="h-6 w-6 shrink-0 object-contain"
52
+ />
53
+ ) : (
54
+ <BrandHeader
55
+ className="flex min-w-0 shrink-0 items-center gap-2.5"
56
+ density="chrome"
57
+ />
58
+ )}
43
59
  </div>
44
60
  <div
45
61
  className="desktop-window-no-drag absolute right-0 top-0 z-20 flex h-full w-[var(--desktop-caption-safe-right)] items-start justify-end"
46
62
  data-testid="desktop-window-controls"
47
63
  >
48
- {[windowControls[0], maximizeControl, windowControls[1]].map((control) => (
49
- <DesktopWindowControlButton key={control.action} control={control} />
50
- ))}
64
+ {[windowControls[0], maximizeControl, windowControls[1]].map(
65
+ (control) => (
66
+ <DesktopWindowControlButton
67
+ key={control.action}
68
+ control={control}
69
+ />
70
+ ),
71
+ )}
51
72
  </div>
52
73
  </header>
53
74
  );
@@ -86,14 +107,18 @@ function useDesktopWindowMaximizedState(): boolean {
86
107
  let isSubscribed = true;
87
108
  const desktopApi = window.nextclawDesktop;
88
109
 
89
- void desktopApi?.getWindowState?.().then((snapshot: DesktopWindowStateSnapshot) => {
90
- if (isSubscribed) {
110
+ void desktopApi
111
+ ?.getWindowState?.()
112
+ .then((snapshot: DesktopWindowStateSnapshot) => {
113
+ if (isSubscribed) {
114
+ setIsMaximized(snapshot.isMaximized);
115
+ }
116
+ });
117
+ const unsubscribe = desktopApi?.onWindowStateChanged?.(
118
+ (snapshot: DesktopWindowStateSnapshot) => {
91
119
  setIsMaximized(snapshot.isMaximized);
92
- }
93
- });
94
- const unsubscribe = desktopApi?.onWindowStateChanged?.((snapshot: DesktopWindowStateSnapshot) => {
95
- setIsMaximized(snapshot.isMaximized);
96
- });
120
+ },
121
+ );
97
122
 
98
123
  return () => {
99
124
  isSubscribed = false;