@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
@@ -1,4 +1,4 @@
1
- import { AlarmClock, ArrowLeft, FileCode2, MessageSquareText, X } from "lucide-react";
1
+ import { AlarmClock, ArrowLeft, ArrowRight, FileCode2, MessageSquareText, X } from "lucide-react";
2
2
  import type { ResolvedChildSessionTab } from "@/features/chat/hooks/use-ncp-child-session-tabs-view";
3
3
  import type { ChatWorkspaceFileTab } from "@/features/chat/stores/chat-thread.store";
4
4
  import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
@@ -184,12 +184,18 @@ function WorkspaceTabItem({ tab }: { tab: WorkspaceTabViewModel }) {
184
184
  }
185
185
 
186
186
  export function WorkspaceTabsBar({
187
- onBack,
187
+ canGoBack,
188
+ canGoForward,
188
189
  onClose,
190
+ onGoBack,
191
+ onGoForward,
189
192
  tabs,
190
193
  }: {
191
- onBack?: () => void;
194
+ canGoBack: boolean;
195
+ canGoForward: boolean;
192
196
  onClose: () => void;
197
+ onGoBack: () => void;
198
+ onGoForward: () => void;
193
199
  tabs: readonly WorkspaceTabViewModel[];
194
200
  }) {
195
201
  return (
@@ -208,16 +214,42 @@ export function WorkspaceTabsBar({
208
214
  ))}
209
215
  </div>
210
216
  </div>
211
- {onBack ? (
212
- <button
213
- type="button"
214
- onClick={onBack}
215
- className="mx-1 flex h-7 w-7 shrink-0 self-center items-center justify-center rounded-md text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
216
- aria-label={t("chatBackToParent")}
217
- >
218
- <ArrowLeft className="h-4 w-4" />
219
- </button>
220
- ) : null}
217
+ <Tooltip>
218
+ <TooltipTrigger asChild>
219
+ <span className="ml-1 inline-flex self-center">
220
+ <button
221
+ type="button"
222
+ onClick={onGoBack}
223
+ disabled={!canGoBack}
224
+ className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:cursor-not-allowed disabled:text-gray-300 disabled:hover:bg-transparent"
225
+ aria-label={t("chatWorkspaceBack")}
226
+ >
227
+ <ArrowLeft className="h-4 w-4" />
228
+ </button>
229
+ </span>
230
+ </TooltipTrigger>
231
+ <TooltipContent side="bottom" className="text-xs">
232
+ {t("chatWorkspaceBack")}
233
+ </TooltipContent>
234
+ </Tooltip>
235
+ <Tooltip>
236
+ <TooltipTrigger asChild>
237
+ <span className="inline-flex self-center">
238
+ <button
239
+ type="button"
240
+ onClick={onGoForward}
241
+ disabled={!canGoForward}
242
+ className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:cursor-not-allowed disabled:text-gray-300 disabled:hover:bg-transparent"
243
+ aria-label={t("chatWorkspaceForward")}
244
+ >
245
+ <ArrowRight className="h-4 w-4" />
246
+ </button>
247
+ </span>
248
+ </TooltipTrigger>
249
+ <TooltipContent side="bottom" className="text-xs">
250
+ {t("chatWorkspaceForward")}
251
+ </TooltipContent>
252
+ </Tooltip>
221
253
  <button
222
254
  type="button"
223
255
  onClick={onClose}
@@ -18,6 +18,7 @@ import {
18
18
  } from "@/features/chat/stores/chat-session-list.store";
19
19
  import type {
20
20
  ChatChildSessionTab,
21
+ ChatWorkspaceNavigationEntry,
21
22
  ChatWorkspaceFileTab,
22
23
  } from "@/features/chat/stores/chat-thread.store";
23
24
  import {
@@ -33,6 +34,10 @@ import { SessionCronJobContent } from "@/features/chat/components/workspace/sess
33
34
  import { ResizableRightPanel } from "@/shared/components/resizable-right-panel/resizable-right-panel";
34
35
  import { t } from "@/shared/lib/i18n";
35
36
  import { cn } from "@/shared/lib/utils";
37
+ import {
38
+ canGoBackInNavigationHistory,
39
+ canGoForwardInNavigationHistory,
40
+ } from "@/shared/lib/navigation-history";
36
41
 
37
42
  type ChatSessionWorkspacePanelProps = {
38
43
  sessionKey: string | null;
@@ -40,9 +45,12 @@ type ChatSessionWorkspacePanelProps = {
40
45
  activeChildSessionKey: string | null;
41
46
  workspaceFileTabs: readonly ChatWorkspaceFileTab[];
42
47
  activeWorkspaceFileKey: string | null;
48
+ workspaceNavigationHistory?: readonly ChatWorkspaceNavigationEntry[];
49
+ workspaceNavigationHistoryIndex?: number;
43
50
  activePanelKind?: "child-session" | "file" | "cron" | null;
44
51
  sessionCronJobs?: readonly CronJobView[];
45
52
  sessionProjectRoot: string | null;
53
+ sessionWorkingDir: string | null;
46
54
  displayMode?: "docked" | "overlay";
47
55
  };
48
56
 
@@ -246,9 +254,12 @@ export function ChatSessionWorkspacePanel({
246
254
  activeChildSessionKey,
247
255
  workspaceFileTabs,
248
256
  activeWorkspaceFileKey,
257
+ workspaceNavigationHistory = [],
258
+ workspaceNavigationHistoryIndex = 0,
249
259
  activePanelKind,
250
260
  sessionCronJobs = [],
251
261
  sessionProjectRoot,
262
+ sessionWorkingDir,
252
263
  displayMode = "docked",
253
264
  }: ChatSessionWorkspacePanelProps) {
254
265
  const presenter = usePresenter();
@@ -264,9 +275,10 @@ export function ChatSessionWorkspacePanel({
264
275
  workspaceFileTabs,
265
276
  sessionCronJobCount: sessionCronJobs.length,
266
277
  });
267
- const hasParentSession = resolvedChildTabs.some((tab) =>
268
- Boolean(tab.parentSessionKey),
269
- );
278
+ const workspaceHistory = {
279
+ entries: workspaceNavigationHistory,
280
+ index: workspaceNavigationHistoryIndex,
281
+ };
270
282
 
271
283
  useEffect(() => {
272
284
  if (
@@ -331,11 +343,10 @@ export function ChatSessionWorkspacePanel({
331
343
  >
332
344
  <WorkspaceTabsBar
333
345
  tabs={workspaceTabs}
334
- onBack={
335
- hasParentSession
336
- ? presenter.chatThreadManager.goToParentSession
337
- : undefined
338
- }
346
+ canGoBack={canGoBackInNavigationHistory(workspaceHistory)}
347
+ canGoForward={canGoForwardInNavigationHistory(workspaceHistory)}
348
+ onGoBack={presenter.chatThreadManager.goBackWorkspacePanel}
349
+ onGoForward={presenter.chatThreadManager.goForwardWorkspacePanel}
339
350
  onClose={presenter.chatThreadManager.closeWorkspacePanel}
340
351
  />
341
352
 
@@ -357,6 +368,7 @@ export function ChatSessionWorkspacePanel({
357
368
  <ChatSessionWorkspaceFilePreview
358
369
  file={activeSelection.file}
359
370
  sessionProjectRoot={sessionProjectRoot}
371
+ sessionWorkingDir={sessionWorkingDir}
360
372
  onFileOpen={presenter.chatThreadManager.openFilePreview}
361
373
  />
362
374
  ) : (
@@ -16,6 +16,8 @@ const mocks = vi.hoisted(() => ({
16
16
  goToChatRoot: vi.fn(),
17
17
  goToSession: vi.fn(),
18
18
  openSessionCronPanel: vi.fn(),
19
+ goBackWorkspacePanel: vi.fn(),
20
+ goForwardWorkspacePanel: vi.fn(),
19
21
  deleteCronJob: vi.fn(),
20
22
  cronJobs: [] as CronJobView[],
21
23
  setSelectedAgentId: vi.fn(),
@@ -119,6 +121,8 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
119
121
  selectWorkspaceFile: vi.fn(),
120
122
  closeWorkspaceFile: vi.fn(),
121
123
  closeWorkspacePanel: vi.fn(),
124
+ goBackWorkspacePanel: mocks.goBackWorkspacePanel,
125
+ goForwardWorkspacePanel: mocks.goForwardWorkspacePanel,
122
126
  goToParentSession: vi.fn(),
123
127
  },
124
128
  chatSessionListManager: {
@@ -200,8 +204,25 @@ function resetChatConversationPanelTestState() {
200
204
  mocks.goToChatRoot.mockReset();
201
205
  mocks.goToSession.mockReset();
202
206
  mocks.openSessionCronPanel.mockReset();
207
+ mocks.goBackWorkspacePanel.mockReset();
208
+ mocks.goForwardWorkspacePanel.mockReset();
203
209
  mocks.deleteCronJob.mockReset();
204
210
  mocks.cronJobs = [];
211
+ mocks.resolvedChildTabs = [
212
+ {
213
+ sessionKey: "child-session-1",
214
+ parentSessionKey: "parent-session-1",
215
+ title: "北京天气",
216
+ agentId: "weather",
217
+ updatedAt: "2026-04-10T09:00:00.000Z",
218
+ lastMessageAt: "2026-04-10T09:00:00.000Z",
219
+ readAt: "2026-04-10T09:00:00.000Z",
220
+ sessionTypeLabel: "Codex",
221
+ preferredModel: "openai/gpt-5.3-codex",
222
+ projectName: "project-alpha",
223
+ projectRoot: "/Users/demo/project-alpha",
224
+ },
225
+ ];
205
226
  mocks.setSelectedAgentId.mockReset();
206
227
  mocks.setPendingSessionType.mockReset();
207
228
  mocks.stickyBottomScroll.mockClear();
@@ -247,6 +268,8 @@ function resetChatConversationPanelTestState() {
247
268
  activeChildSessionKey: null,
248
269
  workspaceFileTabs: [],
249
270
  activeWorkspaceFileKey: null,
271
+ workspaceNavigationHistory: [],
272
+ workspaceNavigationHistoryIndex: 0,
250
273
  },
251
274
  });
252
275
  useChatSessionListStore.setState({
@@ -406,6 +429,11 @@ describe("ChatConversationPanel", () => {
406
429
  messageCount: "1",
407
430
  sending: "true",
408
431
  });
432
+ expect(
433
+ screen
434
+ .getByTestId("chat-message-list")
435
+ .closest('[data-chat-scroll-container="true"]'),
436
+ ).toBeTruthy();
409
437
  expect(
410
438
  screen.queryByText("No messages yet. Send one to start."),
411
439
  ).toBeNull();
@@ -520,6 +548,8 @@ describe("ChatConversationPanel", () => {
520
548
  });
521
549
 
522
550
  describe("ChatSessionWorkspacePanel", () => {
551
+ beforeEach(resetChatConversationPanelTestState);
552
+
523
553
  it("renders child session tabs and active child metadata in the workspace sidebar", () => {
524
554
  mocks.resolvedChildTabs = [
525
555
  {
@@ -552,6 +582,7 @@ describe("ChatSessionWorkspacePanel", () => {
552
582
  workspaceFileTabs={[]}
553
583
  activeWorkspaceFileKey={null}
554
584
  sessionProjectRoot="/Users/demo/project-alpha"
585
+ sessionWorkingDir="/Users/demo/project-alpha"
555
586
  />,
556
587
  );
557
588
 
@@ -622,6 +653,7 @@ describe("ChatSessionWorkspacePanel", () => {
622
653
  workspaceFileTabs={[]}
623
654
  activeWorkspaceFileKey={null}
624
655
  sessionProjectRoot="/Users/demo/project-alpha"
656
+ sessionWorkingDir="/Users/demo/project-alpha"
625
657
  />,
626
658
  );
627
659
 
@@ -645,6 +677,7 @@ describe("ChatSessionWorkspacePanel", () => {
645
677
  ]}
646
678
  activeWorkspaceFileKey="parent-session-1::preview::README.md"
647
679
  sessionProjectRoot="/Users/demo/project-alpha"
680
+ sessionWorkingDir="/Users/demo/project-alpha"
648
681
  />,
649
682
  );
650
683
 
@@ -658,6 +691,45 @@ describe("ChatSessionWorkspacePanel", () => {
658
691
  ).toContain("workspace-horizontal-scrollbar");
659
692
  });
660
693
 
694
+ it("uses workspace-local backward and forward history actions in the tab bar", async () => {
695
+ const user = userEvent.setup();
696
+
697
+ render(
698
+ <ChatSessionWorkspacePanel
699
+ sessionKey="parent-session-1"
700
+ childSessionTabs={[]}
701
+ activeChildSessionKey={null}
702
+ workspaceFileTabs={[
703
+ {
704
+ key: "parent-session-1::preview::README.md",
705
+ parentSessionKey: "parent-session-1",
706
+ path: "README.md",
707
+ label: "README.md",
708
+ viewMode: "preview",
709
+ },
710
+ ]}
711
+ activeWorkspaceFileKey="parent-session-1::preview::README.md"
712
+ workspaceNavigationHistory={[
713
+ { kind: "child-session", key: "child-session-1" },
714
+ { kind: "file", key: "parent-session-1::preview::README.md" },
715
+ { kind: "cron" },
716
+ ]}
717
+ workspaceNavigationHistoryIndex={1}
718
+ sessionProjectRoot="/Users/demo/project-alpha"
719
+ sessionWorkingDir="/Users/demo/project-alpha"
720
+ />,
721
+ );
722
+
723
+ await user.click(screen.getByRole("button", { name: "Back in workspace" }));
724
+ await user.click(
725
+ screen.getByRole("button", { name: "Forward in workspace" }),
726
+ );
727
+
728
+ expect(mocks.goBackWorkspacePanel).toHaveBeenCalledTimes(1);
729
+ expect(mocks.goForwardWorkspacePanel).toHaveBeenCalledTimes(1);
730
+ expect(screen.queryByRole("button", { name: "Back to parent" })).toBeNull();
731
+ });
732
+
661
733
  it("renders session cron jobs in the workspace sidebar and deletes with a neutral confirmation", async () => {
662
734
  const user = userEvent.setup();
663
735
  const job: CronJobView = {
@@ -691,6 +763,7 @@ describe("ChatSessionWorkspacePanel", () => {
691
763
  activePanelKind="cron"
692
764
  sessionCronJobs={[job]}
693
765
  sessionProjectRoot="/Users/demo/project-alpha"
766
+ sessionWorkingDir="/Users/demo/project-alpha"
694
767
  />,
695
768
  );
696
769
 
@@ -194,6 +194,7 @@ function ChatConversationContent() {
194
194
  <div
195
195
  ref={threadRef}
196
196
  onScroll={onScroll}
197
+ data-chat-scroll-container="true"
197
198
  className="flex-1 min-h-0 overflow-y-auto custom-scrollbar"
198
199
  >
199
200
  {showWelcome ? (
@@ -363,9 +364,12 @@ function ChatSessionWorkspacePanelContainer({
363
364
  activeChildSessionKey={snapshot.activeChildSessionKey ?? null}
364
365
  workspaceFileTabs={workspaceFileTabs}
365
366
  activeWorkspaceFileKey={snapshot.activeWorkspaceFileKey ?? null}
367
+ workspaceNavigationHistory={snapshot.workspaceNavigationHistory}
368
+ workspaceNavigationHistoryIndex={snapshot.workspaceNavigationHistoryIndex}
366
369
  activePanelKind={snapshot.activeWorkspacePanelKind ?? null}
367
370
  sessionCronJobs={sessionCronJobs}
368
371
  sessionProjectRoot={snapshot.sessionProjectRoot ?? null}
372
+ sessionWorkingDir={snapshot.sessionWorkingDir ?? snapshot.sessionProjectRoot ?? null}
369
373
  displayMode={layoutMode === "mobile" ? "overlay" : "docked"}
370
374
  />
371
375
  );
@@ -42,6 +42,8 @@ export type ChatThreadManagerLike = {
42
42
  selectWorkspaceFile: (fileKey: string) => void;
43
43
  closeWorkspaceFile: (fileKey: string) => void;
44
44
  closeWorkspacePanel: () => void;
45
+ goBackWorkspacePanel: () => void;
46
+ goForwardWorkspacePanel: () => void;
45
47
  goToParentSession: () => void;
46
48
  };
47
49
 
@@ -119,6 +119,7 @@ export function useNcpChatSnapshotSync(params: {
119
119
  availableAgents: AgentProfileView[];
120
120
  currentSessionDisplayName?: string;
121
121
  effectiveSessionProjectRoot: string | null;
122
+ effectiveSessionWorkingDir: string | null;
122
123
  effectiveSessionProjectName: string | null;
123
124
  selectedSession: SessionEntryView | null;
124
125
  threadRef: MutableRefObject<HTMLDivElement | null>;
@@ -159,6 +160,7 @@ export function useNcpChatSnapshotSync(params: {
159
160
  availableAgents: params.availableAgents,
160
161
  sessionDisplayName: params.currentSessionDisplayName,
161
162
  sessionProjectRoot: params.effectiveSessionProjectRoot,
163
+ sessionWorkingDir: params.effectiveSessionWorkingDir,
162
164
  sessionProjectName: params.effectiveSessionProjectName,
163
165
  canDeleteSession: Boolean(params.selectedSession),
164
166
  threadRef: params.threadRef,
@@ -57,6 +57,10 @@ describe('ChatSessionListManager', () => {
57
57
  activeWorkspacePanelKind: 'child-session',
58
58
  activeChildSessionKey: 'child-session-1',
59
59
  activeWorkspaceFileKey: null,
60
+ workspaceNavigationHistory: [
61
+ { kind: 'child-session', key: 'child-session-1' },
62
+ ],
63
+ workspaceNavigationHistoryIndex: 0,
60
64
  },
61
65
  });
62
66
  });
@@ -30,6 +30,8 @@ export class ChatSessionListManager {
30
30
  childSessionTabs: [],
31
31
  activeChildSessionKey: null,
32
32
  activeWorkspaceFileKey: null,
33
+ workspaceNavigationHistory: [],
34
+ workspaceNavigationHistoryIndex: 0,
33
35
  });
34
36
  };
35
37
 
@@ -19,33 +19,37 @@ vi.mock('@/shared/lib/api', async (importOriginal) => {
19
19
  };
20
20
  });
21
21
 
22
- describe('NcpChatThreadManager', () => {
23
- beforeEach(() => {
24
- useChatSessionListStore.setState({
25
- optimisticReadAtBySessionKey: {},
26
- snapshot: {
27
- ...useChatSessionListStore.getState().snapshot,
28
- selectedSessionKey: 'parent-session-1',
29
- },
30
- });
31
- useChatThreadStore.setState({
32
- snapshot: {
33
- ...useChatThreadStore.getState().snapshot,
34
- sessionKey: 'parent-session-1',
35
- workspacePanelParentKey: null,
36
- childSessionTabs: [
37
- {
38
- sessionKey: 'child-session-1',
39
- parentSessionKey: 'parent-session-1',
40
- label: 'Child Session 1',
41
- agentId: 'reviewer',
42
- },
43
- ],
44
- activeChildSessionKey: null,
45
- },
46
- });
22
+ beforeEach(() => {
23
+ useChatSessionListStore.setState({
24
+ optimisticReadAtBySessionKey: {},
25
+ snapshot: {
26
+ ...useChatSessionListStore.getState().snapshot,
27
+ selectedSessionKey: 'parent-session-1',
28
+ },
47
29
  });
30
+ useChatThreadStore.setState({
31
+ snapshot: {
32
+ ...useChatThreadStore.getState().snapshot,
33
+ sessionKey: 'parent-session-1',
34
+ workspacePanelParentKey: null,
35
+ childSessionTabs: [
36
+ {
37
+ sessionKey: 'child-session-1',
38
+ parentSessionKey: 'parent-session-1',
39
+ label: 'Child Session 1',
40
+ agentId: 'reviewer',
41
+ },
42
+ ],
43
+ activeChildSessionKey: null,
44
+ workspaceFileTabs: [],
45
+ activeWorkspaceFileKey: null,
46
+ workspaceNavigationHistory: [],
47
+ workspaceNavigationHistoryIndex: 0,
48
+ },
49
+ });
50
+ });
48
51
 
52
+ describe('NcpChatThreadManager', () => {
49
53
  it('opens the child-session panel for the requested parent session and keeps focus on the chosen child', () => {
50
54
  const uiManager = {
51
55
  goToSession: vi.fn(),
@@ -190,7 +194,129 @@ describe('NcpChatThreadManager', () => {
190
194
  ]),
191
195
  );
192
196
  });
197
+ });
198
+
199
+ describe('NcpChatThreadManager workspace navigation', () => {
200
+ it('navigates backward and forward through workspace selections', () => {
201
+ const uiManager = {
202
+ goToSession: vi.fn(),
203
+ goToChatRoot: vi.fn(),
204
+ goToProviders: vi.fn(),
205
+ confirm: vi.fn(),
206
+ } as unknown as ConstructorParameters<typeof NcpChatThreadManager>[0];
207
+ const manager = new NcpChatThreadManager(
208
+ uiManager,
209
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[1],
210
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[2],
211
+ );
212
+
213
+ manager.openChildSessionPanel({
214
+ parentSessionKey: 'parent-session-1',
215
+ activeChildSessionKey: 'child-session-1',
216
+ });
217
+ manager.openFilePreview({
218
+ path: 'README.md',
219
+ label: 'README.md',
220
+ viewMode: 'preview',
221
+ });
222
+ manager.openSessionCronPanel('parent-session-1');
223
+
224
+ expect(useChatThreadStore.getState().snapshot.workspaceNavigationHistory).toEqual([
225
+ { kind: 'child-session', key: 'child-session-1' },
226
+ { kind: 'file', key: 'parent-session-1::preview::README.md' },
227
+ { kind: 'cron' },
228
+ ]);
229
+
230
+ manager.goBackWorkspacePanel();
231
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
232
+ activeWorkspacePanelKind: 'file',
233
+ activeWorkspaceFileKey: 'parent-session-1::preview::README.md',
234
+ workspaceNavigationHistoryIndex: 1,
235
+ });
236
+
237
+ manager.goBackWorkspacePanel();
238
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
239
+ activeWorkspacePanelKind: 'child-session',
240
+ activeChildSessionKey: 'child-session-1',
241
+ workspaceNavigationHistoryIndex: 0,
242
+ });
243
+
244
+ manager.goForwardWorkspacePanel();
245
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
246
+ activeWorkspacePanelKind: 'file',
247
+ activeWorkspaceFileKey: 'parent-session-1::preview::README.md',
248
+ workspaceNavigationHistoryIndex: 1,
249
+ });
250
+ });
251
+
252
+ it('truncates workspace forward history after selecting a new entry', () => {
253
+ useChatThreadStore.getState().setSnapshot({
254
+ childSessionTabs: [
255
+ ...useChatThreadStore.getState().snapshot.childSessionTabs,
256
+ {
257
+ sessionKey: 'child-session-2',
258
+ parentSessionKey: 'parent-session-1',
259
+ label: 'Child Session 2',
260
+ agentId: 'writer',
261
+ },
262
+ ],
263
+ });
264
+ const manager = new NcpChatThreadManager(
265
+ { goToSession: vi.fn() } as unknown as ConstructorParameters<typeof NcpChatThreadManager>[0],
266
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[1],
267
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[2],
268
+ );
269
+
270
+ manager.openChildSessionPanel({
271
+ parentSessionKey: 'parent-session-1',
272
+ activeChildSessionKey: 'child-session-1',
273
+ });
274
+ manager.openFilePreview({
275
+ path: 'README.md',
276
+ label: 'README.md',
277
+ viewMode: 'preview',
278
+ });
279
+ manager.goBackWorkspacePanel();
280
+ manager.selectChildSessionDetail('child-session-2');
281
+
282
+ expect(useChatThreadStore.getState().snapshot.workspaceNavigationHistory).toEqual([
283
+ { kind: 'child-session', key: 'child-session-1' },
284
+ { kind: 'child-session', key: 'child-session-2' },
285
+ ]);
286
+ expect(useChatThreadStore.getState().snapshot.workspaceNavigationHistoryIndex).toBe(1);
287
+ });
288
+
289
+ it('removes a closed active file from workspace history and restores the previous entry', () => {
290
+ const manager = new NcpChatThreadManager(
291
+ { goToSession: vi.fn() } as unknown as ConstructorParameters<typeof NcpChatThreadManager>[0],
292
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[1],
293
+ {} as ConstructorParameters<typeof NcpChatThreadManager>[2],
294
+ );
295
+
296
+ manager.openChildSessionPanel({
297
+ parentSessionKey: 'parent-session-1',
298
+ activeChildSessionKey: 'child-session-1',
299
+ });
300
+ manager.openFilePreview({
301
+ path: 'README.md',
302
+ label: 'README.md',
303
+ viewMode: 'preview',
304
+ });
305
+ manager.closeWorkspaceFile('parent-session-1::preview::README.md');
306
+
307
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
308
+ activeWorkspacePanelKind: 'child-session',
309
+ activeChildSessionKey: 'child-session-1',
310
+ activeWorkspaceFileKey: null,
311
+ workspaceNavigationHistory: [
312
+ { kind: 'child-session', key: 'child-session-1' },
313
+ ],
314
+ workspaceNavigationHistoryIndex: 0,
315
+ });
316
+ });
317
+ });
193
318
 
319
+ describe('NcpChatThreadManager deletion', () => {
194
320
  it('clears the selected thread state after deleting the current session', async () => {
195
321
  const removeQueries = vi.spyOn(appQueryClient, 'removeQueries').mockImplementation(async () => undefined);
196
322
  const uiManager = {
@@ -228,6 +354,8 @@ describe('NcpChatThreadManager', () => {
228
354
  activeChildSessionKey: null,
229
355
  workspaceFileTabs: [],
230
356
  activeWorkspaceFileKey: null,
357
+ workspaceNavigationHistory: [],
358
+ workspaceNavigationHistoryIndex: 0,
231
359
  });
232
360
  expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
233
361
  expect(deleteSummaryMock).toHaveBeenCalledWith(appQueryClient, 'parent-session-1');