@nextclaw/ui 0.15.3 → 0.15.4

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 (184) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/assets/aiden0z-pptx-renderer.es-Bd99vut2.js +94 -0
  3. package/dist/assets/api-BOXOrpcJ.js +13 -0
  4. package/dist/assets/app-presenter-provider-CkHLbvZQ.js +106 -0
  5. package/dist/assets/app-presenter-provider-D3BuuOyZ.css +1 -0
  6. package/dist/assets/appearance-settings-page-fLIJbx1C.js +1 -0
  7. package/dist/assets/{book-open-C3mJGwiy.js → book-open-DWUjS6aW.js} +1 -1
  8. package/dist/assets/channels-list-page-CvCTJraa.js +8 -0
  9. package/dist/assets/chat-page-B42UUIFR.js +1 -0
  10. package/dist/assets/chunk-DseTPa7n.js +1 -0
  11. package/dist/assets/config-split-page-DBgt9S8b.js +1 -0
  12. package/dist/assets/confirm-dialog-GSXIvoRU.js +5 -0
  13. package/dist/assets/desktop-update-config-7bmNeWIC.js +1 -0
  14. package/dist/assets/dist-DC7f4TJH.js +41 -0
  15. package/dist/assets/doc-browser-CgdoLUT92.js +1 -0
  16. package/dist/assets/doc-browser-D0monHjG.js +1 -0
  17. package/dist/assets/doc-browser-context-CGwEOhBy.js +1 -0
  18. package/dist/assets/docx-preview-eFH9DRsS.js +20 -0
  19. package/dist/assets/download-DiP9zx-J.js +1 -0
  20. package/dist/assets/{external-link-CU0kzzIP.js → external-link-C6ZeSPef.js} +1 -1
  21. package/dist/assets/eye-suvKmtwn.js +1 -0
  22. package/dist/assets/folder-CMELbQGG.js +1 -0
  23. package/dist/assets/{form-actions-BvCp_0lS.js → form-actions-CUAzpNe_.js} +1 -1
  24. package/dist/assets/index-BFeNDQ4R.css +1 -0
  25. package/dist/assets/index-D_6U-M94.js +2 -0
  26. package/dist/assets/jszip.min-Das2IAnb.js +2 -0
  27. package/dist/assets/loader-circle-tYHStefV.js +1 -0
  28. package/dist/assets/logo-badge-Cu5UGH_4.js +1 -0
  29. package/dist/assets/mcp-marketplace-page-BUzHDwW6.js +9 -0
  30. package/dist/assets/mcp-marketplace-page-ohUAdfqJ.js +1 -0
  31. package/dist/assets/model-config-page-CWP6pyBm.js +1 -0
  32. package/dist/assets/plus-tywizkeY.js +1 -0
  33. package/dist/assets/popover-DImguRok.js +1 -0
  34. package/dist/assets/provider-models-C_yOh6DE.js +1 -0
  35. package/dist/assets/provider-scoped-model-input-CE7p4SbJ.js +1 -0
  36. package/dist/assets/providers-config-page-BKPo8SjH.js +1 -0
  37. package/dist/assets/react-5sjAT_6d.js +8 -0
  38. package/dist/assets/{refresh-cw-CILd1vAP.js → refresh-cw-CHLBzWWj.js} +1 -1
  39. package/dist/assets/remote-DXJryUxB.js +1 -0
  40. package/dist/assets/rotate-cw-BefO5brU.js +1 -0
  41. package/dist/assets/runtime-config-page-1Axv4QGu.js +1 -0
  42. package/dist/assets/{save-ByJqcPTP.js → save-BABzyrjB.js} +1 -1
  43. package/dist/assets/{search-DVIV6pfD.js → search-BTqxG3ZG.js} +1 -1
  44. package/dist/assets/search-config-page-BkKKmxmS.js +1 -0
  45. package/dist/assets/secrets-config-page-CWJMyu-k.js +3 -0
  46. package/dist/assets/security-config-DeQUIXfi.js +1 -0
  47. package/dist/assets/select-TYZDBNmq.js +1 -0
  48. package/dist/assets/setting-row-DR23Z_5I.js +1 -0
  49. package/dist/assets/{settings-2-CH5shGg_.js → settings-2-D8WWcBnw.js} +1 -1
  50. package/dist/assets/{skeleton-DeX1FKw7.js → skeleton-CloHeg0S.js} +1 -1
  51. package/dist/assets/tabs-CX-ckWoo.js +1 -0
  52. package/dist/assets/tag-chip-mCc23Rhb.js +1 -0
  53. package/dist/assets/trash-2-SAW1Z4Po.js +1 -0
  54. package/dist/assets/use-config-BucdaK23.js +1 -0
  55. package/dist/assets/use-confirm-dialog-C95F6sT1.js +1 -0
  56. package/dist/assets/use-viewport-layout-C6m3Nte7.js +1 -0
  57. package/dist/assets/x-DtCPHwCu.js +1 -0
  58. package/dist/assets/xlsx-B6zVt1RK.js +64 -0
  59. package/dist/index.html +34 -28
  60. package/package.json +11 -6
  61. package/src/features/chat/components/__tests__/chat-popover-content.test.tsx +41 -0
  62. package/src/features/chat/components/chat-popover-content.tsx +26 -0
  63. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +13 -0
  64. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +21 -11
  65. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +2 -2
  66. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +28 -1
  67. package/src/features/chat/components/conversation/chat-conversation-content.tsx +5 -4
  68. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +9 -18
  69. package/src/features/chat/components/conversation/chat-conversation-track.tsx +35 -0
  70. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +1 -0
  71. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +7 -7
  72. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +3 -14
  73. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +20 -1
  74. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +69 -54
  75. package/src/features/chat/features/message/components/chat-message-list.container.tsx +16 -1
  76. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +30 -0
  77. package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +37 -0
  78. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +4 -3
  79. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +3 -8
  80. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +65 -20
  81. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +26 -28
  82. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
  83. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +3 -3
  84. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +3 -3
  85. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +3 -3
  86. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +403 -106
  87. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +113 -2
  88. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -0
  89. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +197 -44
  90. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +9 -3
  91. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +7 -5
  92. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +57 -177
  93. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +239 -1
  94. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +56 -6
  95. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +29 -7
  96. package/src/features/chat/features/workspace/components/file-content-preview/use-workspace-file-buffer.ts +42 -0
  97. package/src/features/chat/features/workspace/components/file-content-preview/workspace-document-preview-state.tsx +18 -0
  98. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.css +89 -0
  99. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.tsx +87 -0
  100. package/src/features/chat/features/workspace/components/file-content-preview/workspace-presentation-preview.tsx +82 -0
  101. package/src/features/chat/features/workspace/components/file-content-preview/workspace-spreadsheet-preview.tsx +180 -0
  102. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +191 -0
  103. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +1 -4
  104. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +53 -10
  105. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +68 -2
  106. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +17 -0
  107. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +63 -0
  108. package/src/features/chat/features/workspace/utils/chat-workspace-file-viewer.utils.ts +49 -0
  109. package/src/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils.ts +13 -0
  110. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +120 -38
  111. package/src/features/chat/index.ts +1 -0
  112. package/src/features/chat/managers/__tests__/chat-thread-workspace.manager.test.ts +76 -0
  113. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +44 -60
  114. package/src/features/chat/managers/chat-thread.manager.ts +123 -126
  115. package/src/features/chat/pages/ncp-chat-page.tsx +1 -2
  116. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +78 -1
  117. package/src/features/chat/stores/chat-message-layout.store.test.ts +37 -0
  118. package/src/features/chat/stores/chat-message-layout.store.ts +41 -0
  119. package/src/features/chat/stores/chat-thread.store.ts +70 -13
  120. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -8
  121. package/src/features/settings/pages/appearance-settings-page.tsx +92 -13
  122. package/src/index.css +36 -2
  123. package/src/shared/components/__tests__/file-type-icon.test.tsx +61 -0
  124. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +19 -0
  125. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +16 -0
  126. package/src/shared/components/doc-browser/doc-browser-context.tsx +1 -0
  127. package/src/shared/components/doc-browser/doc-browser.tsx +11 -3
  128. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +5 -0
  129. package/src/shared/components/doc-browser/stores/doc-browser.store.ts +6 -1
  130. package/src/shared/components/doc-browser/types/doc-browser.types.ts +2 -0
  131. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +11 -0
  132. package/src/shared/components/file-type-icon.tsx +272 -0
  133. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +22 -0
  134. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +17 -2
  135. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +38 -0
  136. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +82 -2
  137. package/src/shared/lib/api/__tests__/server-path.utils.test.ts +28 -0
  138. package/src/shared/lib/api/utils/server-path.utils.ts +19 -20
  139. package/src/shared/lib/i18n/locales/en-US/chat.json +17 -1
  140. package/src/shared/lib/i18n/locales/en-US/core.json +6 -0
  141. package/src/shared/lib/i18n/locales/zh-CN/chat.json +17 -1
  142. package/src/shared/lib/i18n/locales/zh-CN/core.json +6 -0
  143. package/dist/assets/api-iNBpppzj.js +0 -13
  144. package/dist/assets/appearance-settings-page-DMffQFR3.js +0 -1
  145. package/dist/assets/channels-list-page-CCtp0BRS.js +0 -8
  146. package/dist/assets/chat-page-H5a0dWvO.js +0 -1
  147. package/dist/assets/config-split-page-CrOJgB-Y.js +0 -1
  148. package/dist/assets/confirm-dialog-Chv50USy.js +0 -5
  149. package/dist/assets/desktop-update-config-KzTuXRf_.js +0 -1
  150. package/dist/assets/dist-BunzmC4B.js +0 -1
  151. package/dist/assets/dist-ChmGwvtK.js +0 -1
  152. package/dist/assets/doc-browser-DNRL4Zvd.js +0 -1
  153. package/dist/assets/doc-browser-Dg0pmkOg.js +0 -1
  154. package/dist/assets/doc-browser-context-BHY-0FSD.js +0 -1
  155. package/dist/assets/es2015-DQZ5JWwK.js +0 -41
  156. package/dist/assets/i18n-provider-Cu8XyZb_.js +0 -1
  157. package/dist/assets/index-CzfBdvK1.css +0 -1
  158. package/dist/assets/index-DGqHne1Q.js +0 -104
  159. package/dist/assets/loader-circle-CDSFuzmk.js +0 -1
  160. package/dist/assets/mcp-marketplace-page-BeEQK0DL.js +0 -9
  161. package/dist/assets/mcp-marketplace-page-D6idOTLp.js +0 -1
  162. package/dist/assets/model-config-page-bNNo-XyG.js +0 -1
  163. package/dist/assets/plus-D4DsdLN-.js +0 -1
  164. package/dist/assets/provider-scoped-model-input-IlgQCsoT.js +0 -1
  165. package/dist/assets/providers-config-page-wLo6CAJw.js +0 -1
  166. package/dist/assets/react-BPIsQIlO.js +0 -8
  167. package/dist/assets/remote-uKEYp7AR.js +0 -1
  168. package/dist/assets/rotate-cw-wPFPg1kH.js +0 -1
  169. package/dist/assets/runtime-config-page-DDn1sP4G.js +0 -1
  170. package/dist/assets/search-config-page-B-Cf4iiB.js +0 -1
  171. package/dist/assets/secrets-config-page-DPKsY8Iw.js +0 -3
  172. package/dist/assets/security-config-e9SSBlKr.js +0 -1
  173. package/dist/assets/select-BdgczSNw.js +0 -1
  174. package/dist/assets/setting-row-BkTVUTKx.js +0 -1
  175. package/dist/assets/side-dock-BsBvcJ9i.js +0 -3
  176. package/dist/assets/switch-Cv8Ekb3x.js +0 -1
  177. package/dist/assets/tag-chip-RB9uKcoC.js +0 -1
  178. package/dist/assets/tooltip-oQlEN-l6.js +0 -1
  179. package/dist/assets/use-config-l3W7y8Qz.js +0 -1
  180. package/dist/assets/use-confirm-dialog-BAxaVOJI.js +0 -1
  181. package/dist/assets/x-BBMffEN0.js +0 -1
  182. /package/dist/assets/{config-hints-C_iP1zl_.js → config-hints-fDrYfl0l.js} +0 -0
  183. /package/dist/assets/{host-capabilities-9bB_998Z.js → host-capabilities-zZvY9U63.js} +0 -0
  184. /package/dist/assets/{middleware-BILWbJE4.js → middleware-BLMHv9YF.js} +0 -0
@@ -49,6 +49,8 @@ import {
49
49
  } from '@/features/chat/managers/chat-recent-skills.manager';
50
50
 
51
51
  import { useSessionConversationInputAttachments } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-attachments';
52
+ import { ChatConversationTrack } from '@/features/chat/components/conversation/chat-conversation-track';
53
+ import { useChatMessageLayoutStore } from '@/features/chat/stores/chat-message-layout.store';
52
54
  import type { useSessionConversationInputQuery } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-query';
53
55
  import type {
54
56
  SessionConversationQueuedInput,
@@ -187,6 +189,7 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
187
189
  } = props;
188
190
  const presenter = usePresenter();
189
191
  const { language } = useI18n();
192
+ const messageLayout = useChatMessageLayoutStore((state) => state.layout);
190
193
  const { isMobile } = useViewportLayout();
191
194
  const inputBarRef = useRef<ChatInputBarHandle | null>(null);
192
195
  const fileInputRef = useRef<HTMLInputElement | null>(null);
@@ -389,62 +392,74 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
389
392
  selectedSkills: inputSnapshot.selectedSkills,
390
393
  });
391
394
 
395
+ const useReadingTrack = surface === 'default' && messageLayout === 'flat';
396
+ const inputBar = (
397
+ <ChatInputBar
398
+ ref={inputBarRef}
399
+ surface={useReadingTrack ? 'embedded' : surface}
400
+ sendError={inputSnapshot.sendError}
401
+ sendErrorDetailsLabel={t('chatErrorDetails', language)}
402
+ topSlot={controller.queuedInputs.length > 0
403
+ ? <SessionQueuedInputRows controller={controller} />
404
+ : null}
405
+ composer={{
406
+ nodes: [...inputSnapshot.nodes],
407
+ placeholder: textareaPlaceholder,
408
+ disabled: inputDisabled,
409
+ onNodesChange: handleNodesChange,
410
+ onFilesAdd: handleFilesAdd,
411
+ inputSurfaceTriggerSpecs: inputSurfaceState.triggerSpecs,
412
+ onInputSurfaceTriggerChange: setInputSurfaceTrigger,
413
+ }}
414
+ inputSurface={inputSurfaceState.panel ?? undefined}
415
+ hint={buildModelStateHint({
416
+ isModelOptionsLoading,
417
+ isModelOptionsEmpty,
418
+ onGoToProviders: presenter.chatUiManager.goToProviders,
419
+ texts: {
420
+ noModelOptionsLabel: t('chatModelNoOptions'),
421
+ configureProviderLabel: t('chatGoConfigureProvider'),
422
+ },
423
+ })}
424
+ toolbar={{
425
+ selects: [],
426
+ trailingSelects: toolbarSelects,
427
+ accessories: [
428
+ {
429
+ key: 'attach',
430
+ label: t('chatInputAttach'),
431
+ icon: 'paperclip' as const,
432
+ iconOnly: true,
433
+ disabled: !attachmentSupported || inputDisabled,
434
+ onClick: () => fileInputRef.current?.click(),
435
+ },
436
+ ],
437
+ skillPicker,
438
+ actions: {
439
+ isSending: controller.isSending,
440
+ canStopGeneration: controller.canStopGeneration,
441
+ sendDisabled: controller.sendDisabled,
442
+ stopDisabled: controller.stopDisabled,
443
+ stopHint: t('chatStopUnavailable'),
444
+ sendButtonLabel: controller.isSending ? t('chatQueueSend') : t('chatSend'),
445
+ stopButtonLabel: t('chatStop'),
446
+ contextWindow,
447
+ onSend: controller.send,
448
+ onStop: controller.stop,
449
+ },
450
+ }}
451
+ />
452
+ );
453
+
392
454
  return (
393
455
  <>
394
- <ChatInputBar
395
- ref={inputBarRef}
396
- surface={surface}
397
- topSlot={controller.queuedInputs.length > 0
398
- ? <SessionQueuedInputRows controller={controller} />
399
- : null}
400
- composer={{
401
- nodes: [...inputSnapshot.nodes],
402
- placeholder: textareaPlaceholder,
403
- disabled: inputDisabled,
404
- onNodesChange: handleNodesChange,
405
- onFilesAdd: handleFilesAdd,
406
- inputSurfaceTriggerSpecs: inputSurfaceState.triggerSpecs,
407
- onInputSurfaceTriggerChange: setInputSurfaceTrigger,
408
- }}
409
- inputSurface={inputSurfaceState.panel ?? undefined}
410
- hint={buildModelStateHint({
411
- isModelOptionsLoading,
412
- isModelOptionsEmpty,
413
- onGoToProviders: presenter.chatUiManager.goToProviders,
414
- texts: {
415
- noModelOptionsLabel: t('chatModelNoOptions'),
416
- configureProviderLabel: t('chatGoConfigureProvider'),
417
- },
418
- })}
419
- toolbar={{
420
- selects: [],
421
- trailingSelects: toolbarSelects,
422
- accessories: [
423
- {
424
- key: 'attach',
425
- label: t('chatInputAttach'),
426
- icon: 'paperclip' as const,
427
- iconOnly: true,
428
- disabled: !attachmentSupported || inputDisabled,
429
- onClick: () => fileInputRef.current?.click(),
430
- },
431
- ],
432
- skillPicker,
433
- actions: {
434
- sendError: inputSnapshot.sendError,
435
- isSending: controller.isSending,
436
- canStopGeneration: controller.canStopGeneration,
437
- sendDisabled: controller.sendDisabled,
438
- stopDisabled: controller.stopDisabled,
439
- stopHint: t('chatStopUnavailable'),
440
- sendButtonLabel: controller.isSending ? t('chatQueueSend') : t('chatSend'),
441
- stopButtonLabel: t('chatStop'),
442
- contextWindow,
443
- onSend: controller.send,
444
- onStop: controller.stop,
445
- },
446
- }}
447
- />
456
+ {useReadingTrack ? (
457
+ <ChatConversationTrack className="pb-4 pt-2" width="composer">
458
+ {inputBar}
459
+ </ChatConversationTrack>
460
+ ) : (
461
+ inputBar
462
+ )}
448
463
  <input
449
464
  ref={fileInputRef}
450
465
  type="file"
@@ -25,7 +25,11 @@ import {
25
25
  import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
26
26
  import { ChatInlinePanelAppCard } from "@/features/chat/features/message/components/chat-inline-panel-app-card";
27
27
  import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
28
+ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
29
+ import { useChatMessageLayoutStore } from "@/features/chat/stores/chat-message-layout.store";
30
+ import { useNcpChatSelectedSession } from "@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state";
28
31
  import { useI18n } from "@/app/components/i18n-provider";
32
+ import { buildServerPathContentUrl } from "@/shared/lib/api";
29
33
  import { formatDateTime, t } from "@/shared/lib/i18n";
30
34
 
31
35
  type ChatMessageListContainerProps = {
@@ -58,7 +62,6 @@ function renderChatInlineDisplay(display: ChatInlineDisplayViewModel) {
58
62
  />
59
63
  );
60
64
  }
61
-
62
65
  type ContextInheritanceTimelineView = {
63
66
  sourceSessionId: string;
64
67
  inheritedMessageCount: number;
@@ -286,6 +289,16 @@ export function ChatMessageListContainer({
286
289
  }: ChatMessageListContainerProps) {
287
290
  const presenter = usePresenter();
288
291
  const { language } = useI18n();
292
+ const messageLayout = useChatMessageLayoutStore((state) => state.layout);
293
+ const selectedSessionKey = useChatSessionListStore(
294
+ (state) => state.snapshot.selectedSessionKey,
295
+ );
296
+ const selectedSession = useNcpChatSelectedSession(selectedSessionKey);
297
+ const localFileBasePath = selectedSession?.workingDir ?? selectedSession?.projectRoot ?? null;
298
+ const resolveFileContentUrl = useCallback(
299
+ (action: { path: string }) => buildServerPathContentUrl(action.path, localFileBasePath),
300
+ [localFileBasePath],
301
+ );
289
302
  const texts = useMemo(
290
303
  () => buildChatMessageAdapterTexts(language),
291
304
  [language],
@@ -396,6 +409,7 @@ export function ChatMessageListContainer({
396
409
  ) : (
397
410
  <ChatMessageList
398
411
  key={item.key}
412
+ layout={messageLayout}
399
413
  messages={item.messages}
400
414
  isSending={index === timelineItems.length - 1 ? isSending : false}
401
415
  hasAssistantDraft={hasAssistantDraft}
@@ -404,6 +418,7 @@ export function ChatMessageListContainer({
404
418
  onFileOpen={presenter.chatThreadManager.openFilePreview}
405
419
  onAttachmentOpen={handleAttachmentOpen}
406
420
  onInlineTokenClick={handleInlineTokenClick}
421
+ resolveFileContentUrl={resolveFileContentUrl}
407
422
  renderInlineDisplay={renderChatInlineDisplay}
408
423
  renderToolAgent={(agentId) => (
409
424
  <AgentIdentityAvatar
@@ -63,6 +63,36 @@ it("builds edit-file previews from structured args before the tool finishes", ()
63
63
  expect(editLines[1]).not.toHaveProperty("newLineNumber");
64
64
  });
65
65
 
66
+ it("keeps incomplete edit-file arguments available before preview fields arrive", () => {
67
+ const partialArgs = '{"path":"src/app.ts","oldText":';
68
+ const adapted = adapt([
69
+ {
70
+ id: "assistant-partial-edit-input",
71
+ role: "assistant",
72
+ parts: [
73
+ {
74
+ type: "tool-invocation",
75
+ toolInvocation: {
76
+ status: ToolInvocationStatus.PARTIAL_CALL,
77
+ toolCallId: "edit-partial-1",
78
+ toolName: "edit_file",
79
+ args: partialArgs,
80
+ },
81
+ },
82
+ ],
83
+ },
84
+ ] as unknown as ChatMessageSource[]);
85
+
86
+ expect(adapted[0]?.parts[0]).toMatchObject({
87
+ type: "tool-card",
88
+ card: {
89
+ toolName: "edit_file",
90
+ input: partialArgs,
91
+ statusTone: "running",
92
+ },
93
+ });
94
+ });
95
+
66
96
  it("uses structured edit-file result line numbers after the tool finishes", () => {
67
97
  const adapted = adapt([
68
98
  {
@@ -0,0 +1,37 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { expect, it, vi } from 'vitest';
3
+ import { ChatSidebarSessionItem } from '@/features/chat/features/session/components/chat-sidebar-session-item';
4
+
5
+ it('shows session actions only on hover or when an action owns focus', () => {
6
+ render(
7
+ <ChatSidebarSessionItem
8
+ sessionKey="session:current"
9
+ active
10
+ showUnreadDot={false}
11
+ context={{ icon: null, label: null }}
12
+ isPinned={false}
13
+ title="Current Task"
14
+ previewText="Preview"
15
+ trailingText="Now"
16
+ isEditing={false}
17
+ draftLabel="Current Task"
18
+ isSaving={false}
19
+ onSelect={vi.fn()}
20
+ onStartEditing={vi.fn()}
21
+ onDraftLabelChange={vi.fn()}
22
+ onSave={vi.fn()}
23
+ onCancel={vi.fn()}
24
+ onTogglePinned={vi.fn()}
25
+ />,
26
+ );
27
+
28
+ screen.getByText('Current Task').closest('button')?.focus();
29
+ const actions = screen.getByLabelText('Pin session').parentElement;
30
+
31
+ expect(actions?.className).toContain('opacity-0');
32
+ expect(actions?.className).toContain('group-hover/session:opacity-100');
33
+ expect(actions?.className).toContain('focus-within:opacity-100');
34
+ expect(actions?.className).not.toContain(
35
+ 'group-focus-within/session:opacity-100',
36
+ );
37
+ });
@@ -2,7 +2,8 @@ import { useMemo, useState, type ReactNode } from 'react';
2
2
  import { ChevronDown, ChevronRight, Folder, Pin, Plus } from 'lucide-react';
3
3
  import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
4
4
  import { ChatSessionTypeMenu } from "@/features/chat/features/session-type/components/chat-session-type-menu";
5
- import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
5
+ import { Popover, PopoverTrigger } from '@/shared/components/ui/popover';
6
+ import { ChatPopoverContent } from '@/features/chat/components/chat-popover-content';
6
7
  import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
7
8
  import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
8
9
  import type { NcpSessionListItemView } from '@/features/chat/features/ncp/hooks/use-ncp-session-list-view';
@@ -107,7 +108,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
107
108
  label={actionLabel}
108
109
  />
109
110
  </PopoverTrigger>
110
- <PopoverContent
111
+ <ChatPopoverContent
111
112
  align="end"
112
113
  className="w-56 rounded-2xl border border-border bg-popover p-1.5 text-popover-foreground shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
113
114
  >
@@ -122,7 +123,7 @@ export function ChatSidebarProjectGroups(props: ChatSidebarProjectGroupsProps) {
122
123
  setOpenProjectRoot(null);
123
124
  }}
124
125
  />
125
- </PopoverContent>
126
+ </ChatPopoverContent>
126
127
  </Popover>
127
128
  ) : (
128
129
  <IconActionButton
@@ -169,12 +169,7 @@ function ChatSidebarSessionDisplayView({
169
169
  event.stopPropagation();
170
170
  onOpenChildSessions();
171
171
  }}
172
- className={cn(
173
- 'absolute right-12 top-0 inline-flex h-6 items-center gap-1 rounded-md px-1.5 text-[10px] font-medium text-muted-foreground transition-colors hover:bg-black/10 hover:text-foreground',
174
- active
175
- ? 'opacity-100'
176
- : 'opacity-0 group-hover/session:opacity-100 group-focus-within/session:opacity-100'
177
- )}
172
+ className="pointer-events-none absolute right-12 top-0 inline-flex h-6 items-center gap-1 rounded-md px-1.5 text-[10px] font-medium text-muted-foreground opacity-0 transition-colors hover:bg-black/10 hover:text-foreground group-hover/session:pointer-events-auto group-hover/session:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100"
178
173
  aria-label={t('chatSessionOpenChildSessions')}
179
174
  title={t('chatSessionOpenChildSessions')}
180
175
  >
@@ -183,11 +178,11 @@ function ChatSidebarSessionDisplayView({
183
178
  </button>
184
179
  ) : null}
185
180
  {runStatus ? (
186
- <span className="absolute right-0 top-0 inline-flex h-6 w-6 items-center justify-center transition-opacity group-hover/session:opacity-0 group-focus-within/session:opacity-0">
181
+ <span className="absolute right-0 top-0 inline-flex h-6 w-6 items-center justify-center transition-opacity group-hover/session:opacity-0">
187
182
  <SessionRunBadge status={runStatus} />
188
183
  </span>
189
184
  ) : null}
190
- <div className="absolute right-0 top-0 flex h-6 items-center gap-0.5 opacity-0 transition-opacity group-hover/session:opacity-100 group-focus-within/session:opacity-100">
185
+ <div className="pointer-events-none absolute right-0 top-0 flex h-6 items-center gap-0.5 opacity-0 transition-opacity group-hover/session:pointer-events-auto group-hover/session:opacity-100 focus-within:pointer-events-auto focus-within:opacity-100">
191
186
  <IconActionButton
192
187
  size="sm"
193
188
  tone="strong"
@@ -6,8 +6,7 @@ import { ChatSessionHeaderActions } from '@/features/chat/features/session/compo
6
6
  const mocks = vi.hoisted(() => ({
7
7
  updateSessionProject: vi.fn(),
8
8
  onDeleteSession: vi.fn(),
9
- onOpenChildSessions: vi.fn(),
10
- onOpenSessionCronJobs: vi.fn(),
9
+ onToggleWorkspace: vi.fn(),
11
10
  }));
12
11
 
13
12
  vi.mock('@/features/chat/features/session/hooks/use-chat-session-project', () => ({
@@ -22,8 +21,7 @@ describe('ChatSessionHeaderActions', () => {
22
21
  beforeEach(() => {
23
22
  mocks.updateSessionProject.mockReset();
24
23
  mocks.onDeleteSession.mockReset();
25
- mocks.onOpenChildSessions.mockReset();
26
- mocks.onOpenSessionCronJobs.mockReset();
24
+ mocks.onToggleWorkspace.mockReset();
27
25
  });
28
26
 
29
27
  it('keeps only the set-project action in the more-actions menu when a project is already attached', async () => {
@@ -35,8 +33,8 @@ describe('ChatSessionHeaderActions', () => {
35
33
  canDeleteSession
36
34
  isDeletePending={false}
37
35
  projectRoot="/tmp/project-alpha"
38
- childSessionCount={0}
39
- onOpenChildSessions={mocks.onOpenChildSessions}
36
+ isWorkspaceOpen={false}
37
+ onToggleWorkspace={mocks.onToggleWorkspace}
40
38
  onDeleteSession={mocks.onDeleteSession}
41
39
  />
42
40
  );
@@ -58,8 +56,8 @@ describe('ChatSessionHeaderActions', () => {
58
56
  canDeleteSession={false}
59
57
  isDeletePending={false}
60
58
  projectRoot={null}
61
- childSessionCount={0}
62
- onOpenChildSessions={mocks.onOpenChildSessions}
59
+ isWorkspaceOpen={false}
60
+ onToggleWorkspace={mocks.onToggleWorkspace}
63
61
  onDeleteSession={mocks.onDeleteSession}
64
62
  />
65
63
  );
@@ -84,6 +82,8 @@ describe('ChatSessionHeaderActions', () => {
84
82
  codex_thread_id: 'thread-123',
85
83
  runtime: 'codex',
86
84
  }}
85
+ isWorkspaceOpen={false}
86
+ onToggleWorkspace={mocks.onToggleWorkspace}
87
87
  onDeleteSession={mocks.onDeleteSession}
88
88
  />
89
89
  );
@@ -98,7 +98,7 @@ describe('ChatSessionHeaderActions', () => {
98
98
  expect(screen.getByText(/thread-123/)).toBeTruthy();
99
99
  });
100
100
 
101
- it('shows a dedicated child-session entry button when the current session has child sessions', async () => {
101
+ it('opens the session workspace from one stable header action', async () => {
102
102
  const user = userEvent.setup();
103
103
 
104
104
  render(
@@ -107,28 +107,28 @@ describe('ChatSessionHeaderActions', () => {
107
107
  canDeleteSession
108
108
  isDeletePending={false}
109
109
  projectRoot={null}
110
- childSessionCount={2}
111
- onOpenChildSessions={mocks.onOpenChildSessions}
110
+ isWorkspaceOpen={false}
111
+ onToggleWorkspace={mocks.onToggleWorkspace}
112
112
  onDeleteSession={mocks.onDeleteSession}
113
113
  />
114
114
  );
115
115
 
116
- await user.click(screen.getByRole('button', { name: 'View child sessions' }));
116
+ await user.click(screen.getByRole('button', { name: 'Open session workspace' }));
117
117
 
118
- expect(mocks.onOpenChildSessions).toHaveBeenCalledTimes(1);
118
+ expect(mocks.onToggleWorkspace).toHaveBeenCalledTimes(1);
119
+ expect(screen.queryByRole('button', { name: 'View child sessions' })).toBeNull();
120
+ expect(screen.queryByRole('button', { name: 'View session cron jobs' })).toBeNull();
119
121
  });
120
122
 
121
- it('uses a shared spaced action group for child, cron, and menu buttons', () => {
123
+ it('uses the shared compact action density for workspace and menu buttons', () => {
122
124
  render(
123
125
  <ChatSessionHeaderActions
124
126
  sessionKey="session-actions"
125
127
  canDeleteSession
126
128
  isDeletePending={false}
127
129
  projectRoot={null}
128
- childSessionCount={1}
129
- sessionCronJobCount={1}
130
- onOpenChildSessions={mocks.onOpenChildSessions}
131
- onOpenSessionCronJobs={mocks.onOpenSessionCronJobs}
130
+ isWorkspaceOpen={false}
131
+ onToggleWorkspace={mocks.onToggleWorkspace}
132
132
  onDeleteSession={mocks.onDeleteSession}
133
133
  />
134
134
  );
@@ -136,7 +136,52 @@ describe('ChatSessionHeaderActions', () => {
136
136
  const actionGroup = screen.getByRole('button', { name: 'More actions' }).parentElement;
137
137
 
138
138
  expect(actionGroup?.className).toContain('gap-1.5');
139
- expect(screen.getByRole('button', { name: 'View child sessions' }).className).toContain('h-7');
140
- expect(screen.getByRole('button', { name: 'View session cron jobs' }).className).toContain('w-7');
139
+ expect(screen.getByRole('button', { name: 'Open session workspace' }).className).toContain('h-7');
140
+ expect(screen.getByRole('button', { name: 'Open session workspace' }).className).toContain('w-7');
141
+ });
142
+
143
+ it('places the workspace toggle at the far right of the visible header actions', () => {
144
+ render(
145
+ <ChatSessionHeaderActions
146
+ sessionKey="session-actions"
147
+ canDeleteSession
148
+ isDeletePending={false}
149
+ projectRoot={null}
150
+ isWorkspaceOpen={false}
151
+ onToggleWorkspace={mocks.onToggleWorkspace}
152
+ onDeleteSession={mocks.onDeleteSession}
153
+ />
154
+ );
155
+
156
+ const menuButton = screen.getByRole('button', { name: 'More actions' });
157
+ const workspaceButton = screen.getByRole('button', { name: 'Open session workspace' });
158
+
159
+ expect(menuButton.compareDocumentPosition(workspaceButton)).toBe(
160
+ Node.DOCUMENT_POSITION_FOLLOWING,
161
+ );
162
+ });
163
+
164
+ it('changes to a pressed close action while the workspace is open', async () => {
165
+ const user = userEvent.setup();
166
+
167
+ render(
168
+ <ChatSessionHeaderActions
169
+ sessionKey="session-actions"
170
+ canDeleteSession
171
+ isDeletePending={false}
172
+ projectRoot={null}
173
+ isWorkspaceOpen
174
+ onToggleWorkspace={mocks.onToggleWorkspace}
175
+ onDeleteSession={mocks.onDeleteSession}
176
+ />
177
+ );
178
+
179
+ const closeButton = screen.getByRole('button', {
180
+ name: 'Close session workspace',
181
+ });
182
+
183
+ expect(closeButton.getAttribute('aria-pressed')).toBe('true');
184
+ await user.click(closeButton);
185
+ expect(mocks.onToggleWorkspace).toHaveBeenCalledTimes(1);
141
186
  });
142
187
  });
@@ -1,7 +1,8 @@
1
1
  import { useState } from 'react';
2
- import { AlarmClock, Braces, FolderOpen, GitBranch, MoreVertical, Trash2 } from 'lucide-react';
2
+ import { Braces, FolderOpen, MoreVertical, PanelRightClose, PanelRightOpen, Trash2 } from 'lucide-react';
3
3
  import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
4
- import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
4
+ import { Popover, PopoverTrigger } from '@/shared/components/ui/popover';
5
+ import { ChatPopoverContent } from '@/features/chat/components/chat-popover-content';
5
6
  import { useChatSessionProject } from '@/features/chat/features/session/hooks/use-chat-session-project';
6
7
  import { ChatSessionHeaderMenuItem } from './chat-session-header-menu-item';
7
8
  import { ChatSessionMetadataDialog } from './chat-session-metadata-dialog';
@@ -16,10 +17,8 @@ type ChatSessionHeaderActionsProps = {
16
17
  isDeletePending: boolean;
17
18
  projectRoot?: string | null;
18
19
  metadata?: Record<string, unknown> | null;
19
- childSessionCount?: number;
20
- sessionCronJobCount?: number;
21
- onOpenChildSessions?: () => void;
22
- onOpenSessionCronJobs?: () => void;
20
+ isWorkspaceOpen: boolean;
21
+ onToggleWorkspace: () => void;
23
22
  onDeleteSession: () => void;
24
23
  };
25
24
 
@@ -29,10 +28,8 @@ export function ChatSessionHeaderActions({
29
28
  isDeletePending,
30
29
  projectRoot,
31
30
  metadata,
32
- childSessionCount = 0,
33
- sessionCronJobCount = 0,
34
- onOpenChildSessions,
35
- onOpenSessionCronJobs,
31
+ isWorkspaceOpen,
32
+ onToggleWorkspace,
36
33
  onDeleteSession,
37
34
  }: ChatSessionHeaderActionsProps) {
38
35
  const updateSessionProject = useChatSessionProject();
@@ -60,22 +57,6 @@ export function ChatSessionHeaderActions({
60
57
 
61
58
  return (
62
59
  <div className={SESSION_HEADER_ACTION_GROUP_CLASS}>
63
- {childSessionCount > 0 && onOpenChildSessions ? (
64
- <IconActionButton
65
- icon={<GitBranch className="h-4 w-4" />}
66
- label={t('chatSessionOpenChildSessions')}
67
- onClick={onOpenChildSessions}
68
- disabled={isBusy}
69
- />
70
- ) : null}
71
- {sessionCronJobCount > 0 && onOpenSessionCronJobs ? (
72
- <IconActionButton
73
- icon={<AlarmClock className="h-4 w-4" />}
74
- label={t('chatSessionOpenCronJobs')}
75
- onClick={onOpenSessionCronJobs}
76
- disabled={isBusy}
77
- />
78
- ) : null}
79
60
  <Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
80
61
  <PopoverTrigger asChild>
81
62
  <IconActionButton
@@ -85,7 +66,7 @@ export function ChatSessionHeaderActions({
85
66
  disabled={isBusy}
86
67
  />
87
68
  </PopoverTrigger>
88
- <PopoverContent align="end" className="w-56 p-2">
69
+ <ChatPopoverContent align="end" className="w-56 p-2">
89
70
  <div className="space-y-1">
90
71
  <ChatSessionHeaderMenuItem
91
72
  icon={FolderOpen}
@@ -116,8 +97,25 @@ export function ChatSessionHeaderActions({
116
97
  destructive
117
98
  />
118
99
  </div>
119
- </PopoverContent>
100
+ </ChatPopoverContent>
120
101
  </Popover>
102
+ <IconActionButton
103
+ icon={
104
+ isWorkspaceOpen ? (
105
+ <PanelRightClose className="h-4 w-4" />
106
+ ) : (
107
+ <PanelRightOpen className="h-4 w-4" />
108
+ )
109
+ }
110
+ label={t(
111
+ isWorkspaceOpen
112
+ ? 'chatSessionCloseWorkspace'
113
+ : 'chatSessionOpenWorkspace',
114
+ )}
115
+ aria-pressed={isWorkspaceOpen}
116
+ onClick={onToggleWorkspace}
117
+ disabled={isBusy}
118
+ />
121
119
 
122
120
  <ChatSessionProjectDialog
123
121
  open={isDialogOpen}
@@ -5,10 +5,10 @@ import { ChatSessionHeaderMenuItem } from './chat-session-header-menu-item';
5
5
  import { ChatSessionProjectDialog } from './chat-session-project-dialog';
6
6
  import {
7
7
  Popover,
8
- PopoverContent,
9
8
  PopoverTrigger,
10
9
  createPopoverAvailableHeightLimit,
11
10
  } from '@/shared/components/ui/popover';
11
+ import { ChatPopoverContent } from '@/features/chat/components/chat-popover-content';
12
12
  import { t } from '@/shared/lib/i18n';
13
13
 
14
14
  type ChatSessionProjectBadgeProps = {
@@ -66,7 +66,7 @@ export function ChatSessionProjectBadge({
66
66
  </span>
67
67
  </button>
68
68
  </PopoverTrigger>
69
- <PopoverContent
69
+ <ChatPopoverContent
70
70
  align="start"
71
71
  className="w-72 p-2"
72
72
  style={SESSION_PROJECT_MENU_STYLE}
@@ -100,7 +100,7 @@ export function ChatSessionProjectBadge({
100
100
  disabled={isProjectPending}
101
101
  />
102
102
  </div>
103
- </PopoverContent>
103
+ </ChatPopoverContent>
104
104
  </Popover>
105
105
 
106
106
  <ChatSessionProjectDialog
@@ -32,9 +32,9 @@ import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
32
32
  import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
33
33
  import {
34
34
  Popover,
35
- PopoverContent,
36
35
  PopoverTrigger,
37
36
  } from "@/shared/components/ui/popover";
37
+ import { ChatPopoverContent } from "@/features/chat/components/chat-popover-content";
38
38
  import { Input } from "@/shared/components/ui/input";
39
39
  import { t } from "@/shared/lib/i18n";
40
40
  import { cn } from "@/shared/lib/utils";
@@ -217,7 +217,7 @@ function ChatSessionTitleSwitcherPopover({
217
217
  />
218
218
  </button>
219
219
  </PopoverTrigger>
220
- <PopoverContent
220
+ <ChatPopoverContent
221
221
  align="start"
222
222
  className="w-[22rem] max-w-[calc(100vw-2rem)] p-0"
223
223
  >
@@ -263,7 +263,7 @@ function ChatSessionTitleSwitcherPopover({
263
263
  />
264
264
  )}
265
265
  </div>
266
- </PopoverContent>
266
+ </ChatPopoverContent>
267
267
  </Popover>
268
268
  );
269
269
  }
@@ -3,10 +3,10 @@ import { ChevronDown, FolderOpen } from 'lucide-react';
3
3
  import type { ChatWelcomeProjectOption } from '@/features/chat/features/welcome/utils/chat-welcome-project-options.utils';
4
4
  import {
5
5
  Popover,
6
- PopoverContent,
7
6
  PopoverTrigger,
8
7
  createPopoverAvailableHeightLimit,
9
8
  } from '@/shared/components/ui/popover';
9
+ import { ChatPopoverContent } from '@/features/chat/components/chat-popover-content';
10
10
  import { t } from '@/shared/lib/i18n';
11
11
  import { getSessionProjectName } from '@/shared/lib/session-project';
12
12
 
@@ -64,7 +64,7 @@ export function ChatWelcomeProjectPicker({
64
64
  <ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70" />
65
65
  </button>
66
66
  </PopoverTrigger>
67
- <PopoverContent
67
+ <ChatPopoverContent
68
68
  align="start"
69
69
  className="flex w-[min(20rem,calc(100vw-1rem))] flex-col overflow-hidden rounded-2xl border border-border bg-popover p-0 text-popover-foreground shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
70
70
  style={{ maxHeight: PROJECT_PICKER_MAX_HEIGHT }}
@@ -121,7 +121,7 @@ export function ChatWelcomeProjectPicker({
121
121
  <span>{t('chatWelcomeProjectOpenFolder')}</span>
122
122
  </button>
123
123
  </div>
124
- </PopoverContent>
124
+ </ChatPopoverContent>
125
125
  </Popover>
126
126
  );
127
127
  }