@nextclaw/ui 0.13.11 → 0.13.13

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 (154) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/assets/{api-DmxQnm30.js → api-D0hARyyl.js} +1 -1
  3. package/dist/assets/app-presenter-provider-C7EsUuEJ.js +3 -0
  4. package/dist/assets/arrow-left-CzSkR5L6.js +1 -0
  5. package/dist/assets/book-open-DmeQPqkd.js +1 -0
  6. package/dist/assets/{channels-list-page-bkKDqaO5.js → channels-list-page-B0yiiH01.js} +2 -2
  7. package/dist/assets/chat-page-ChPjx_X9.js +105 -0
  8. package/dist/assets/{config-split-page-CdJEkWxv.js → config-split-page-BzfZSyAr.js} +1 -1
  9. package/dist/assets/{confirm-dialog-BWRNsRgb.js → confirm-dialog-CTcaBp-8.js} +1 -1
  10. package/dist/assets/desktop-update-config-CExpdkqD.js +1 -0
  11. package/dist/assets/{dist-CyWdaJvr.js → dist-C59BKqbX.js} +1 -1
  12. package/dist/assets/{dist-DzM2ZEVN.js → dist-Di6EHp2x.js} +1 -1
  13. package/dist/assets/{doc-browser-DewXa6oX.js → doc-browser-EZbnEcdk.js} +1 -1
  14. package/dist/assets/doc-browser-XIwe1Tg-.js +1 -0
  15. package/dist/assets/{doc-browser-context-CpL2m4Sp.js → doc-browser-context-BzEA1lm1.js} +1 -1
  16. package/dist/assets/{ellipsis-BDgTYNDX.js → ellipsis-hXUNwi63.js} +1 -1
  17. package/dist/assets/{es2015-WqekV0OK.js → es2015-CJlVSTh-.js} +1 -1
  18. package/dist/assets/{external-link-BLjo8z2a.js → external-link-J0fCK-vu.js} +1 -1
  19. package/dist/assets/host-capabilities-DjiRQ0Q9.js +1 -0
  20. package/dist/assets/index-DfZTO6Pi.js +101 -0
  21. package/dist/assets/index-mIb_YMrc.css +1 -0
  22. package/dist/assets/{key-round-Du8x3sWa.js → key-round-BzYZ0Nno.js} +1 -1
  23. package/dist/assets/loader-circle-BbdIlBbQ.js +1 -0
  24. package/dist/assets/mcp-marketplace-page-L22aCzHX.js +40 -0
  25. package/dist/assets/mcp-marketplace-page-f80Jmv37.js +1 -0
  26. package/dist/assets/model-config-DkD7vSQT.js +1 -0
  27. package/dist/assets/{notice-card-3a7FmAz-.js → notice-card-CD9Ok62G.js} +1 -1
  28. package/dist/assets/play-DHuSeKs8.js +1 -0
  29. package/dist/assets/plus-DrV685KC.js +1 -0
  30. package/dist/assets/{popover-BlJLguiZ.js → popover-CoBu3mbV.js} +1 -1
  31. package/dist/assets/{provider-scoped-model-input-BgPTQa8H.js → provider-scoped-model-input-DZlmTCJU.js} +1 -1
  32. package/dist/assets/providers-list-CVkWiqIy.js +1 -0
  33. package/dist/assets/{react-BVjl1ZaK.js → react-C7HsxuKH.js} +1 -1
  34. package/dist/assets/{refresh-cw-DFR87zei.js → refresh-cw-CVo4nWpq.js} +1 -1
  35. package/dist/assets/remote-Bznw9iji.js +1 -0
  36. package/dist/assets/rotate-cw-CXDpRO6D.js +1 -0
  37. package/dist/assets/{runtime-config-page-DYH2ejUI.js → runtime-config-page-5IycbIxs.js} +1 -1
  38. package/dist/assets/{save-BDmIlHvN.js → save-Z1QTovIt.js} +1 -1
  39. package/dist/assets/search-8nY6PGm0.js +1 -0
  40. package/dist/assets/{search-config-BRsrQ4zK.js → search-config-DEH80WkC.js} +1 -1
  41. package/dist/assets/{secrets-config-eYdx5_AW.js → secrets-config-Gzo5nDJG.js} +1 -1
  42. package/dist/assets/{status-dot-SVPjIyG5.js → status-dot-C1-FKgvv.js} +1 -1
  43. package/dist/assets/{tabs-Czd2mj6z.js → tabs-BqkKwx1R.js} +1 -1
  44. package/dist/assets/{tabs-custom-Ix4nkp2h.js → tabs-custom-DOkaJZmt.js} +1 -1
  45. package/dist/assets/{tag-chip-Cdwb3JSH.js → tag-chip-3kpiMTST.js} +1 -1
  46. package/dist/assets/{tooltip-CKGxLGnG.js → tooltip-Bw-mXQAM.js} +1 -1
  47. package/dist/assets/{trash-2-yLA8BEcE.js → trash-2-CmIwAJIT.js} +1 -1
  48. package/dist/assets/x-CSRTOZnM.js +1 -0
  49. package/dist/index.html +27 -25
  50. package/package.json +8 -8
  51. package/src/features/account/components/account-panel.tsx +2 -2
  52. package/src/features/account/managers/account.manager.ts +6 -5
  53. package/src/features/chat/components/chat-session-workspace-file-preview.test.tsx +39 -0
  54. package/src/features/chat/components/chat-session-workspace-file-preview.tsx +11 -4
  55. package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +45 -13
  56. package/src/features/chat/components/chat-session-workspace-panel.tsx +20 -8
  57. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +73 -0
  58. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +4 -0
  59. package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
  60. package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +2 -0
  61. package/src/features/chat/managers/chat-session-list.manager.test.ts +4 -0
  62. package/src/features/chat/managers/chat-session-list.manager.ts +2 -0
  63. package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +153 -25
  64. package/src/features/chat/managers/ncp-chat-thread.manager.ts +153 -7
  65. package/src/features/chat/pages/ncp-chat-page.tsx +4 -0
  66. package/src/features/chat/stores/chat-thread.store.test.ts +148 -0
  67. package/src/features/chat/stores/chat-thread.store.ts +289 -9
  68. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +21 -0
  69. package/src/features/chat/utils/ncp-session-adapter.utils.ts +6 -0
  70. package/src/features/marketplace/components/marketplace-catalog-grid.tsx +19 -1
  71. package/src/features/marketplace/components/marketplace-external-skill-source-action.tsx +14 -0
  72. package/src/features/marketplace/components/marketplace-page-parts.tsx +10 -2
  73. package/src/features/marketplace/components/marketplace-page.test.tsx +37 -1
  74. package/src/features/marketplace/components/marketplace-page.tsx +38 -16
  75. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +1 -0
  76. package/src/features/marketplace/hooks/use-marketplace.test.tsx +109 -0
  77. package/src/features/marketplace/hooks/use-marketplace.ts +3 -5
  78. package/src/features/side-dock/components/side-dock.test.tsx +25 -0
  79. package/src/features/side-dock/components/side-dock.tsx +13 -0
  80. package/src/features/system-status/components/desktop-update-config.tsx +2 -1
  81. package/src/platforms/desktop/types/desktop-update.types.ts +9 -0
  82. package/src/shared/components/config/provider-form.tsx +2 -2
  83. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +39 -32
  84. package/src/shared/components/ui/action-link.tsx +23 -1
  85. package/src/shared/lib/api/ncp-session.types.ts +1 -0
  86. package/src/shared/lib/host-capabilities/host-capability.manager.test.ts +50 -0
  87. package/src/shared/lib/host-capabilities/host-capability.manager.ts +62 -0
  88. package/src/shared/lib/host-capabilities/index.ts +1 -0
  89. package/src/shared/lib/i18n/README.md +16 -3
  90. package/src/shared/lib/i18n/index.ts +93 -426
  91. package/src/shared/lib/i18n/locales/en-US/agents.json +49 -0
  92. package/src/shared/lib/i18n/locales/en-US/channel-auth.json +51 -0
  93. package/src/shared/lib/i18n/locales/en-US/channels.json +50 -0
  94. package/src/shared/lib/i18n/locales/en-US/chat.json +181 -0
  95. package/src/shared/lib/i18n/locales/en-US/core.json +310 -0
  96. package/src/shared/lib/i18n/locales/en-US/cron.json +32 -0
  97. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +90 -0
  98. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +87 -0
  99. package/src/shared/lib/i18n/locales/en-US/marketplace.json +72 -0
  100. package/src/shared/lib/i18n/locales/en-US/path-picker.json +12 -0
  101. package/src/shared/lib/i18n/locales/en-US/pwa.json +26 -0
  102. package/src/shared/lib/i18n/locales/en-US/remote.json +168 -0
  103. package/src/shared/lib/i18n/locales/en-US/runtime-control.json +105 -0
  104. package/src/shared/lib/i18n/locales/en-US/search.json +28 -0
  105. package/src/shared/lib/i18n/locales/zh-CN/agents.json +49 -0
  106. package/src/shared/lib/i18n/locales/zh-CN/channel-auth.json +51 -0
  107. package/src/shared/lib/i18n/locales/zh-CN/channels.json +50 -0
  108. package/src/shared/lib/i18n/locales/zh-CN/chat.json +181 -0
  109. package/src/shared/lib/i18n/locales/zh-CN/core.json +310 -0
  110. package/src/shared/lib/i18n/locales/zh-CN/cron.json +32 -0
  111. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +90 -0
  112. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +87 -0
  113. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +72 -0
  114. package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +12 -0
  115. package/src/shared/lib/i18n/locales/zh-CN/pwa.json +26 -0
  116. package/src/shared/lib/i18n/locales/zh-CN/remote.json +168 -0
  117. package/src/shared/lib/i18n/locales/zh-CN/runtime-control.json +105 -0
  118. package/src/shared/lib/i18n/locales/zh-CN/search.json +28 -0
  119. package/src/shared/lib/navigation-history/index.ts +1 -0
  120. package/src/shared/lib/navigation-history/navigation-history.utils.test.ts +60 -0
  121. package/src/shared/lib/navigation-history/navigation-history.utils.ts +105 -0
  122. package/dist/assets/app-presenter-provider-CDSjiqfE.js +0 -3
  123. package/dist/assets/arrow-left-Dd45Mvr1.js +0 -1
  124. package/dist/assets/chat-page-YKdeGoVx.js +0 -105
  125. package/dist/assets/desktop-update-config-CLXdVz9r.js +0 -1
  126. package/dist/assets/doc-browser-D5FJcAyz.js +0 -1
  127. package/dist/assets/index-DKflQCOL.js +0 -101
  128. package/dist/assets/index-Q891JiF_.css +0 -1
  129. package/dist/assets/loader-circle-Dz4FssYC.js +0 -1
  130. package/dist/assets/mcp-marketplace-page-BaBuHB2L.js +0 -40
  131. package/dist/assets/mcp-marketplace-page-ooEvG_bB.js +0 -1
  132. package/dist/assets/model-config-B-qg70S4.js +0 -1
  133. package/dist/assets/play-BziM-Y34.js +0 -1
  134. package/dist/assets/plus-DrpyArC8.js +0 -1
  135. package/dist/assets/providers-list-cBeeslbc.js +0 -1
  136. package/dist/assets/remote-DjhVMpVd.js +0 -1
  137. package/dist/assets/rotate-cw-DMBnsYKm.js +0 -1
  138. package/dist/assets/search-CjHGYvTI.js +0 -1
  139. package/dist/assets/x-dpDISboO.js +0 -1
  140. package/src/shared/lib/i18n/agents.ts +0 -85
  141. package/src/shared/lib/i18n/channel-auth.constants.ts +0 -87
  142. package/src/shared/lib/i18n/channels.ts +0 -53
  143. package/src/shared/lib/i18n/chat-labels.utils.ts +0 -206
  144. package/src/shared/lib/i18n/cron-labels.utils.ts +0 -32
  145. package/src/shared/lib/i18n/desktop-update-labels.utils.ts +0 -165
  146. package/src/shared/lib/i18n/marketplace-labels.utils.ts +0 -78
  147. package/src/shared/lib/i18n/pwa.ts +0 -62
  148. package/src/shared/lib/i18n/remote.ts +0 -273
  149. package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +0 -120
  150. package/src/shared/lib/i18n/runtime/i18n.path-picker.ts +0 -12
  151. package/src/shared/lib/i18n/runtime-control-labels.utils.ts +0 -180
  152. package/src/shared/lib/i18n/search.ts +0 -37
  153. /package/dist/assets/{config-hints-Ceiol9x4.js → config-hints-fGnUjDe9.js} +0 -0
  154. /package/dist/assets/{provider-models-C_yOh6DE.js → provider-models-C9VRroK3.js} +0 -0
@@ -11,11 +11,30 @@ import type { ChatUiManager } from '@/features/chat/managers/chat-ui.manager';
11
11
  import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
12
12
  import type {
13
13
  ChatThreadSnapshot,
14
+ ChatWorkspaceNavigationEntry,
14
15
  ChatWorkspaceFileTab,
15
16
  } from '@/features/chat/stores/chat-thread.store';
16
17
  import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
17
18
  import { viewportLayoutManager } from '@/app/managers/viewport-layout.manager';
18
19
  import { t } from '@/shared/lib/i18n';
20
+ import {
21
+ filterNavigationHistoryEntries,
22
+ pushNavigationHistoryEntry,
23
+ stepNavigationHistory,
24
+ } from '@/shared/lib/navigation-history';
25
+
26
+ function areWorkspaceNavigationEntriesEqual(
27
+ current: ChatWorkspaceNavigationEntry,
28
+ next: ChatWorkspaceNavigationEntry,
29
+ ): boolean {
30
+ if (current.kind !== next.kind) {
31
+ return false;
32
+ }
33
+ if (current.kind === 'cron') {
34
+ return true;
35
+ }
36
+ return next.kind !== 'cron' && current.key === next.key;
37
+ }
19
38
 
20
39
  export class NcpChatThreadManager {
21
40
  constructor(
@@ -67,6 +86,8 @@ export class NcpChatThreadManager {
67
86
  activeChildSessionKey: null,
68
87
  workspaceFileTabs: [],
69
88
  activeWorkspaceFileKey: null,
89
+ workspaceNavigationHistory: [],
90
+ workspaceNavigationHistoryIndex: 0,
70
91
  });
71
92
  };
72
93
 
@@ -134,6 +155,57 @@ export class NcpChatThreadManager {
134
155
  }
135
156
  };
136
157
 
158
+ private createWorkspaceSelectionPatch = (
159
+ entry: ChatWorkspaceNavigationEntry,
160
+ snapshot: ChatThreadSnapshot,
161
+ ): Partial<ChatThreadSnapshot> | null => {
162
+ if (entry.kind === 'cron') {
163
+ return {
164
+ activeWorkspacePanelKind: 'cron',
165
+ activeChildSessionKey: null,
166
+ activeWorkspaceFileKey: null,
167
+ };
168
+ }
169
+ if (entry.kind === 'file') {
170
+ if (!snapshot.workspaceFileTabs.some((tab) => tab.key === entry.key)) {
171
+ return null;
172
+ }
173
+ return {
174
+ activeWorkspacePanelKind: 'file',
175
+ activeChildSessionKey: null,
176
+ activeWorkspaceFileKey: entry.key,
177
+ };
178
+ }
179
+ if (!entry.key) {
180
+ return null;
181
+ }
182
+ return {
183
+ activeWorkspacePanelKind: 'child-session',
184
+ activeChildSessionKey: entry.key,
185
+ activeWorkspaceFileKey: null,
186
+ };
187
+ };
188
+
189
+ private setWorkspaceSelection = (
190
+ patch: Partial<ChatThreadSnapshot>,
191
+ entry: ChatWorkspaceNavigationEntry,
192
+ ) => {
193
+ const { snapshot } = useChatThreadStore.getState();
194
+ const history = pushNavigationHistoryEntry(
195
+ {
196
+ entries: snapshot.workspaceNavigationHistory,
197
+ index: snapshot.workspaceNavigationHistoryIndex,
198
+ },
199
+ entry,
200
+ areWorkspaceNavigationEntriesEqual,
201
+ );
202
+ useChatThreadStore.getState().setSnapshot({
203
+ ...patch,
204
+ workspaceNavigationHistory: [...history.entries],
205
+ workspaceNavigationHistoryIndex: history.index,
206
+ });
207
+ };
208
+
137
209
  deleteSession = () => {
138
210
  void this.deleteCurrentSession();
139
211
  };
@@ -155,12 +227,20 @@ export class NcpChatThreadManager {
155
227
  return;
156
228
  }
157
229
  const activeChildSessionKey = params.activeChildSessionKey?.trim() || null;
158
- useChatThreadStore.getState().setSnapshot({
230
+ const patch: Partial<ChatThreadSnapshot> = {
159
231
  workspacePanelParentKey: parentSessionKey,
160
232
  activeWorkspacePanelKind: 'child-session',
161
233
  activeChildSessionKey,
162
234
  activeWorkspaceFileKey: null,
163
- });
235
+ };
236
+ if (activeChildSessionKey) {
237
+ this.setWorkspaceSelection(patch, {
238
+ kind: 'child-session',
239
+ key: activeChildSessionKey,
240
+ });
241
+ } else {
242
+ useChatThreadStore.getState().setSnapshot(patch);
243
+ }
164
244
  this.ensureWorkspaceParentRoute(parentSessionKey);
165
245
  };
166
246
 
@@ -170,12 +250,15 @@ export class NcpChatThreadManager {
170
250
  if (!nextTab) {
171
251
  return;
172
252
  }
173
- useChatThreadStore.getState().setSnapshot({
253
+ this.setWorkspaceSelection({
174
254
  workspacePanelParentKey: parentSessionKey,
175
255
  activeWorkspacePanelKind: 'file',
176
256
  workspaceFileTabs: this.upsertWorkspaceFileTab(nextTab),
177
257
  activeWorkspaceFileKey: nextTab.key,
178
258
  activeChildSessionKey: null,
259
+ }, {
260
+ kind: 'file',
261
+ key: nextTab.key,
179
262
  });
180
263
  this.ensureWorkspaceParentRoute(parentSessionKey);
181
264
  };
@@ -202,6 +285,8 @@ export class NcpChatThreadManager {
202
285
  activeWorkspacePanelKind: null,
203
286
  activeChildSessionKey: null,
204
287
  activeWorkspaceFileKey: null,
288
+ workspaceNavigationHistory: [],
289
+ workspaceNavigationHistoryIndex: 0,
205
290
  });
206
291
  this.uiManager.goToSession(action.sessionId);
207
292
  };
@@ -215,10 +300,13 @@ export class NcpChatThreadManager {
215
300
  if (!childSessionTabs.some((tab) => tab.sessionKey === normalizedSessionKey)) {
216
301
  return;
217
302
  }
218
- useChatThreadStore.getState().setSnapshot({
303
+ this.setWorkspaceSelection({
219
304
  activeChildSessionKey: normalizedSessionKey,
220
305
  activeWorkspaceFileKey: null,
221
306
  activeWorkspacePanelKind: 'child-session',
307
+ }, {
308
+ kind: 'child-session',
309
+ key: normalizedSessionKey,
222
310
  });
223
311
  };
224
312
 
@@ -231,10 +319,13 @@ export class NcpChatThreadManager {
231
319
  if (!workspaceFileTabs.some((tab) => tab.key === normalizedFileKey)) {
232
320
  return;
233
321
  }
234
- useChatThreadStore.getState().setSnapshot({
322
+ this.setWorkspaceSelection({
235
323
  activeWorkspaceFileKey: normalizedFileKey,
236
324
  activeChildSessionKey: null,
237
325
  activeWorkspacePanelKind: 'file',
326
+ }, {
327
+ kind: 'file',
328
+ key: normalizedFileKey,
238
329
  });
239
330
  };
240
331
 
@@ -248,11 +339,31 @@ export class NcpChatThreadManager {
248
339
  const nextTabs = workspaceFileTabs.filter(
249
340
  (tab) => tab.key !== normalizedFileKey,
250
341
  );
342
+ const history = filterNavigationHistoryEntries(
343
+ {
344
+ entries: snapshot.workspaceNavigationHistory,
345
+ index: snapshot.workspaceNavigationHistoryIndex,
346
+ },
347
+ (entry) => entry.kind !== 'file' || entry.key !== normalizedFileKey,
348
+ );
251
349
  const nextPatch: Partial<ChatThreadSnapshot> = {
252
350
  workspaceFileTabs: nextTabs,
351
+ workspaceNavigationHistory: [...history.entries],
352
+ workspaceNavigationHistoryIndex: history.index,
253
353
  };
254
354
  if (activeWorkspaceFileKey === normalizedFileKey) {
255
- nextPatch.activeWorkspaceFileKey = null;
355
+ const nextSnapshot = {
356
+ ...snapshot,
357
+ workspaceFileTabs: nextTabs,
358
+ };
359
+ const restoreEntry = history.entries[history.index];
360
+ const restorePatch = restoreEntry
361
+ ? this.createWorkspaceSelectionPatch(restoreEntry, nextSnapshot)
362
+ : null;
363
+ Object.assign(nextPatch, restorePatch ?? {
364
+ activeWorkspacePanelKind: null,
365
+ activeWorkspaceFileKey: null,
366
+ });
256
367
  }
257
368
  useChatThreadStore.getState().setSnapshot(nextPatch);
258
369
  };
@@ -263,6 +374,8 @@ export class NcpChatThreadManager {
263
374
  activeWorkspacePanelKind: null,
264
375
  activeChildSessionKey: null,
265
376
  activeWorkspaceFileKey: null,
377
+ workspaceNavigationHistory: [],
378
+ workspaceNavigationHistoryIndex: 0,
266
379
  });
267
380
  };
268
381
 
@@ -271,15 +384,48 @@ export class NcpChatThreadManager {
271
384
  if (!parentSessionKey) {
272
385
  return;
273
386
  }
274
- useChatThreadStore.getState().setSnapshot({
387
+ this.setWorkspaceSelection({
275
388
  workspacePanelParentKey: parentSessionKey,
276
389
  activeWorkspacePanelKind: 'cron',
277
390
  activeChildSessionKey: null,
278
391
  activeWorkspaceFileKey: null,
392
+ }, {
393
+ kind: 'cron',
279
394
  });
280
395
  this.ensureWorkspaceParentRoute(parentSessionKey);
281
396
  };
282
397
 
398
+ goBackWorkspacePanel = () => {
399
+ this.restoreWorkspaceNavigationStep('back');
400
+ };
401
+
402
+ goForwardWorkspacePanel = () => {
403
+ this.restoreWorkspaceNavigationStep('forward');
404
+ };
405
+
406
+ private restoreWorkspaceNavigationStep = (direction: 'back' | 'forward') => {
407
+ const { snapshot } = useChatThreadStore.getState();
408
+ const step = stepNavigationHistory(
409
+ {
410
+ entries: snapshot.workspaceNavigationHistory,
411
+ index: snapshot.workspaceNavigationHistoryIndex,
412
+ },
413
+ direction,
414
+ );
415
+ if (!step) {
416
+ return;
417
+ }
418
+ const selectionPatch = this.createWorkspaceSelectionPatch(step.entry, snapshot);
419
+ if (!selectionPatch) {
420
+ return;
421
+ }
422
+ useChatThreadStore.getState().setSnapshot({
423
+ ...selectionPatch,
424
+ workspaceNavigationHistory: [...step.history.entries],
425
+ workspaceNavigationHistoryIndex: step.history.index,
426
+ });
427
+ };
428
+
283
429
  closeChildSessionDetail = () => {
284
430
  this.closeWorkspacePanel();
285
431
  };
@@ -195,6 +195,8 @@ function useNcpChatPageState(presenter: NcpChatPresenter) {
195
195
  const effectiveSessionProjectRoot = hasSessionProjectRootOverride
196
196
  ? pendingProjectRoot
197
197
  : (selectedSession?.projectRoot ?? null);
198
+ const effectiveSessionWorkingDir =
199
+ selectedSession?.workingDir ?? effectiveSessionProjectRoot;
198
200
  const effectiveSessionProjectName = hasSessionProjectRootOverride
199
201
  ? getSessionProjectName(effectiveSessionProjectRoot)
200
202
  : (selectedSession?.projectName ??
@@ -224,6 +226,7 @@ function useNcpChatPageState(presenter: NcpChatPresenter) {
224
226
  ...baseState,
225
227
  availableAgents,
226
228
  effectiveSessionProjectRoot,
229
+ effectiveSessionWorkingDir,
227
230
  effectiveSessionProjectName,
228
231
  isSending: agent.isSending || currentSessionRunning,
229
232
  isAwaitingAssistantOutput: currentSessionRunning,
@@ -438,6 +441,7 @@ export function NcpChatPage({ view }: ChatPageProps) {
438
441
  availableAgents: state.availableAgents,
439
442
  currentSessionDisplayName: state.currentSessionDisplayName,
440
443
  effectiveSessionProjectRoot: state.effectiveSessionProjectRoot,
444
+ effectiveSessionWorkingDir: state.effectiveSessionWorkingDir,
441
445
  effectiveSessionProjectName: state.effectiveSessionProjectName,
442
446
  selectedSession: state.selectedSession,
443
447
  threadRef: state.threadRef,
@@ -0,0 +1,148 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
3
+
4
+ const chatThreadWorkspaceStorageKey = 'nextclaw.chat.workspace-panel.state';
5
+
6
+ function createLocalStoragePersistStorage() {
7
+ return {
8
+ getItem: (name: string) => JSON.parse(window.localStorage.getItem(name) ?? 'null'),
9
+ setItem: (name: string, value: unknown) => window.localStorage.setItem(name, JSON.stringify(value)),
10
+ removeItem: (name: string) => window.localStorage.removeItem(name)
11
+ };
12
+ }
13
+
14
+ function resetChatThreadStore() {
15
+ useChatThreadStore.setState({
16
+ snapshot: {
17
+ ...useChatThreadStore.getState().snapshot,
18
+ sessionKey: null,
19
+ messages: [],
20
+ isSending: false,
21
+ workspacePanelParentKey: null,
22
+ activeWorkspacePanelKind: null,
23
+ childSessionTabs: [],
24
+ activeChildSessionKey: null,
25
+ workspaceFileTabs: [],
26
+ activeWorkspaceFileKey: null,
27
+ workspaceNavigationHistory: [],
28
+ workspaceNavigationHistoryIndex: 0,
29
+ },
30
+ });
31
+ }
32
+
33
+ describe('chat thread workspace panel persistence', () => {
34
+ beforeEach(() => {
35
+ window.localStorage.clear();
36
+ useChatThreadStore.persist.setOptions({
37
+ storage: createLocalStoragePersistStorage(),
38
+ });
39
+ resetChatThreadStore();
40
+ window.localStorage.clear();
41
+ });
42
+
43
+ it('persists only the workspace panel continuity state', () => {
44
+ useChatThreadStore.getState().setSnapshot({
45
+ sessionKey: 'session-1',
46
+ isSending: true,
47
+ workspacePanelParentKey: 'session-1',
48
+ activeWorkspacePanelKind: 'file',
49
+ workspaceFileTabs: [
50
+ {
51
+ key: 'session-1::preview::README.md',
52
+ parentSessionKey: 'session-1',
53
+ path: 'README.md',
54
+ label: 'README.md',
55
+ viewMode: 'preview',
56
+ rawText: '# Hello',
57
+ fullLines: [],
58
+ },
59
+ ],
60
+ activeWorkspaceFileKey: 'session-1::preview::README.md',
61
+ workspaceNavigationHistory: [
62
+ { kind: 'child-session', key: 'child-session-1' },
63
+ { kind: 'file', key: 'session-1::preview::README.md' },
64
+ ],
65
+ workspaceNavigationHistoryIndex: 1,
66
+ });
67
+
68
+ const persisted = JSON.parse(
69
+ window.localStorage.getItem(chatThreadWorkspaceStorageKey) ?? '{}',
70
+ );
71
+
72
+ expect(persisted.state.snapshot).toMatchObject({
73
+ workspacePanelParentKey: 'session-1',
74
+ activeWorkspacePanelKind: 'file',
75
+ activeWorkspaceFileKey: 'session-1::preview::README.md',
76
+ workspaceNavigationHistory: [
77
+ { kind: 'child-session', key: 'child-session-1' },
78
+ { kind: 'file', key: 'session-1::preview::README.md' },
79
+ ],
80
+ workspaceNavigationHistoryIndex: 1,
81
+ });
82
+ expect(persisted.state.snapshot.sessionKey).toBeUndefined();
83
+ expect(persisted.state.snapshot.isSending).toBeUndefined();
84
+ expect(persisted.state.snapshot.workspaceFileTabs[0].rawText).toBe('# Hello');
85
+ expect(persisted.state.snapshot.workspaceFileTabs[0].fullLines).toBeUndefined();
86
+ });
87
+
88
+ it('hydrates the workspace panel state and repairs stale active file keys', async () => {
89
+ window.localStorage.setItem(
90
+ chatThreadWorkspaceStorageKey,
91
+ JSON.stringify({
92
+ state: {
93
+ snapshot: {
94
+ workspacePanelParentKey: 'session-1',
95
+ activeWorkspacePanelKind: 'file',
96
+ activeWorkspaceFileKey: 'missing-file',
97
+ activeChildSessionKey: 'child-session-1',
98
+ workspaceNavigationHistory: [
99
+ { kind: 'file', key: 'missing-file' },
100
+ { kind: 'file', key: 'session-1::preview::README.md' },
101
+ ],
102
+ workspaceNavigationHistoryIndex: 1,
103
+ workspaceFileTabs: [
104
+ {
105
+ key: 'session-1::preview::README.md',
106
+ parentSessionKey: 'session-1',
107
+ path: 'README.md',
108
+ label: 'README.md',
109
+ viewMode: 'preview',
110
+ rawText: '# Hello',
111
+ },
112
+ {
113
+ key: 'invalid',
114
+ parentSessionKey: 'session-1',
115
+ path: '',
116
+ viewMode: 'preview',
117
+ },
118
+ ],
119
+ },
120
+ },
121
+ version: 2,
122
+ }),
123
+ );
124
+
125
+ await useChatThreadStore.persist.rehydrate();
126
+
127
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
128
+ workspacePanelParentKey: 'session-1',
129
+ activeWorkspacePanelKind: 'file',
130
+ activeWorkspaceFileKey: 'session-1::preview::README.md',
131
+ activeChildSessionKey: 'child-session-1',
132
+ workspaceNavigationHistory: [
133
+ { kind: 'file', key: 'session-1::preview::README.md' },
134
+ ],
135
+ workspaceNavigationHistoryIndex: 0,
136
+ workspaceFileTabs: [
137
+ {
138
+ key: 'session-1::preview::README.md',
139
+ parentSessionKey: 'session-1',
140
+ path: 'README.md',
141
+ label: 'README.md',
142
+ viewMode: 'preview',
143
+ rawText: '# Hello',
144
+ },
145
+ ],
146
+ });
147
+ });
148
+ });