@nextclaw/ui 0.14.1 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/assets/{api-DPcvCLfa.js → api-a14tR_z1.js} +7 -7
  3. package/dist/assets/channels-list-page-CFGkm4DT.js +8 -0
  4. package/dist/assets/chat-page-CEdavJXc.js +105 -0
  5. package/dist/assets/{config-split-page-BCJhqdK9.js → config-split-page-B5b9B0uE.js} +1 -1
  6. package/dist/assets/{confirm-dialog-CWh5FfK2.js → confirm-dialog-ClH_Ec7n.js} +1 -1
  7. package/dist/assets/{desktop-update-config-DsRhBizi.js → desktop-update-config-DPNDnVpk.js} +1 -1
  8. package/dist/assets/{dist-DPrgcEC0.js → dist-BtCS2Ucq.js} +1 -1
  9. package/dist/assets/{dist-DsE4OsSS.js → dist-CLGGLm8b.js} +1 -1
  10. package/dist/assets/doc-browser-5nMWYKBD.js +1 -0
  11. package/dist/assets/doc-browser-BJRLhztv.js +1 -0
  12. package/dist/assets/doc-browser-context-D4JwMMxK.js +1 -0
  13. package/dist/assets/{ellipsis-B81FStLm.js → ellipsis-CtJXjIol.js} +1 -1
  14. package/dist/assets/{external-link-C_dmG-WJ.js → external-link-CdJiG13p.js} +1 -1
  15. package/dist/assets/index-BDN1zzMQ.js +105 -0
  16. package/dist/assets/index-g3uSZXoc.css +1 -0
  17. package/dist/assets/mcp-marketplace-page-BU0kIjhR.js +40 -0
  18. package/dist/assets/mcp-marketplace-page-sMNFSq_T.js +1 -0
  19. package/dist/assets/middleware-CkFT3R4v.js +1 -0
  20. package/dist/assets/model-config-page-D6hYXROd.js +1 -0
  21. package/dist/assets/plus-DPjb0piO.js +1 -0
  22. package/dist/assets/provider-scoped-model-input-DAX2HgmR.js +1 -0
  23. package/dist/assets/providers-config-page-BzAeaACA.js +1 -0
  24. package/dist/assets/{react-sZz5HSCm.js → react-C6cZgP44.js} +1 -1
  25. package/dist/assets/remote-B0G80uPr.js +1 -0
  26. package/dist/assets/rotate-cw-DnEB1Rju.js +1 -0
  27. package/dist/assets/runtime-config-page-C5xKaV3i.js +1 -0
  28. package/dist/assets/{save-MzjoiHri.js → save-DKA0x4zk.js} +1 -1
  29. package/dist/assets/{search-B29dE4GN.js → search-BNlcoj5c.js} +1 -1
  30. package/dist/assets/{search-config-page-Dl5sZzob.js → search-config-page-Cq9uJ-N8.js} +1 -1
  31. package/dist/assets/{secrets-config-page-C_l-IFCK.js → secrets-config-page-CxSbZLBo.js} +2 -2
  32. package/dist/assets/{select-BKmh05hd.js → select-CvTlDZTQ.js} +1 -1
  33. package/dist/assets/{tag-chip-BS_7Ueom.js → tag-chip-8ffF2y2C.js} +1 -1
  34. package/dist/assets/use-config-DmT0o1gv.js +1 -0
  35. package/dist/assets/x-BiSSpgqB.js +1 -0
  36. package/dist/index.html +15 -14
  37. package/package.json +9 -9
  38. package/src/app/components/app-presenter-provider.tsx +11 -2
  39. package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
  40. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
  41. package/src/app/components/layout/app-layout.tsx +18 -4
  42. package/src/app/components/layout/sidebar-items.tsx +194 -71
  43. package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
  44. package/src/app/components/layout/sidebar.tsx +340 -83
  45. package/src/app/managers/viewport-layout.manager.ts +10 -0
  46. package/src/app/stores/viewport-layout.store.ts +39 -3
  47. package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
  48. package/src/features/agents/components/agents-page.tsx +10 -6
  49. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
  50. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
  51. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
  52. package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
  53. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +14 -15
  54. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +18 -18
  55. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
  56. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +16 -14
  58. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
  59. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +80 -7
  60. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +82 -25
  61. package/src/features/chat/components/layout/chat-sidebar.tsx +59 -92
  62. package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
  63. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +289 -0
  64. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +427 -0
  65. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +204 -0
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
  67. package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +32 -17
  68. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
  69. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
  70. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
  71. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
  72. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
  73. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
  74. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
  75. package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
  76. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
  77. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
  78. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
  79. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
  80. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
  81. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
  82. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
  83. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
  84. package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
  85. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
  86. package/src/features/chat/features/message/components/chat-message-list.container.tsx +93 -12
  87. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
  88. package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
  89. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  90. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  91. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  92. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
  93. package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
  94. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
  95. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +2 -2
  96. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +2 -0
  97. package/src/features/chat/features/session-type/components/chat-session-type-menu.tsx +2 -2
  98. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +2 -2
  99. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
  100. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -2
  101. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
  102. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +2 -2
  103. package/src/features/chat/features/welcome/components/chat-welcome.tsx +2 -2
  104. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
  106. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
  107. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
  108. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
  109. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
  110. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
  111. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
  112. package/src/features/chat/index.ts +0 -1
  113. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
  114. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
  115. package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
  116. package/src/features/chat/managers/chat-thread.manager.ts +108 -34
  117. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
  118. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
  119. package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
  120. package/src/features/chat/presenters/chat.presenter.ts +2 -19
  121. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
  122. package/src/features/chat/stores/chat-thread.store.ts +42 -26
  123. package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
  124. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
  125. package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
  126. package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
  127. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
  128. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
  129. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
  130. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  131. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
  132. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  133. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
  134. package/src/shared/lib/ui-document-title/index.ts +50 -21
  135. package/dist/assets/channels-list-page-D6jtziu4.js +0 -8
  136. package/dist/assets/chat-page-BBnNJoKG.js +0 -105
  137. package/dist/assets/doc-browser-C8xOF_9F.js +0 -1
  138. package/dist/assets/doc-browser-DgQedeAn.js +0 -1
  139. package/dist/assets/doc-browser-context-B46tQGmO.js +0 -1
  140. package/dist/assets/index-7o1WdUbU.js +0 -103
  141. package/dist/assets/index-DQWmEWzk.css +0 -1
  142. package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +0 -1
  143. package/dist/assets/mcp-marketplace-page-Dzull_4-.js +0 -40
  144. package/dist/assets/model-config-page-Cv0O3BIj.js +0 -1
  145. package/dist/assets/plus-Bnt56oqG.js +0 -1
  146. package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +0 -1
  147. package/dist/assets/providers-config-page-CPDz0Lxc.js +0 -1
  148. package/dist/assets/remote-B0LMs8u7.js +0 -1
  149. package/dist/assets/rotate-cw-BfyG_KEF.js +0 -1
  150. package/dist/assets/runtime-config-page-q0fvv8ME.js +0 -1
  151. package/dist/assets/use-config-DZkBWIdL.js +0 -1
  152. package/dist/assets/x-CW2KO-Hz.js +0 -1
  153. package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
  154. package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
  155. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
  156. package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
  157. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
  158. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
  159. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
  160. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
  161. package/src/features/chat/managers/chat-input.manager.ts +0 -430
  162. package/src/features/chat/managers/chat-run.manager.ts +0 -110
  163. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
  164. package/src/features/chat/stores/chat-input.store.ts +0 -104
  165. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
@@ -1,15 +1,11 @@
1
- import { useRef } from "react";
2
- import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
3
- import { FolderGit2, Loader2 } from "lucide-react";
1
+ import { MessageSquarePlus } from "lucide-react";
4
2
  import type { CronJobView } from "@/shared/lib/api";
5
- import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
6
3
  import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
4
+ import { SessionConversationArea } from "@/features/chat/features/conversation/components/session-conversation-area";
7
5
  import { ChatSessionWorkspaceFilePreview } from "@/features/chat/features/workspace/components/chat-session-workspace-file-preview";
8
6
  import { SessionCronJobContent } from "@/features/chat/features/workspace/components/session-cron-job-content";
9
7
  import type { ResolvedChildSessionTab } from "@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view";
10
- import { useNcpSessionConversation } from "@/features/chat/features/ncp/hooks/use-ncp-session-conversation";
11
8
  import type { WorkspaceSelection } from "@/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils";
12
- import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
13
9
  import { t } from "@/shared/lib/i18n";
14
10
 
15
11
  type ChatSessionWorkspacePanelContentProps = {
@@ -19,53 +15,9 @@ type ChatSessionWorkspacePanelContentProps = {
19
15
  sessionWorkingDir: string | null;
20
16
  };
21
17
 
22
- function ChildSessionContent({ sessionKey }: { sessionKey: string }) {
23
- const agent = useNcpSessionConversation(sessionKey);
24
- const messages = agent.visibleMessages;
25
- const scrollRef = useRef<HTMLDivElement>(null);
26
- const { onScroll } = useStickyBottomScroll({
27
- scrollRef,
28
- resetKey: sessionKey,
29
- isLoading: agent.isHydrating,
30
- hasContent: messages.length > 0,
31
- contentVersion: messages[messages.length - 1] ?? null,
32
- stickyThresholdPx: 20,
33
- });
34
-
35
- return (
36
- <div
37
- ref={scrollRef}
38
- onScroll={onScroll}
39
- className="h-full overflow-y-auto custom-scrollbar"
40
- >
41
- {agent.isHydrating ? (
42
- <div className="flex h-full items-center justify-center text-sm text-gray-500">
43
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
44
- {t("chatChildSessionLoading")}
45
- </div>
46
- ) : agent.hydrateError ? (
47
- <div className="px-4 py-5 text-sm text-rose-600">
48
- {agent.hydrateError.message}
49
- </div>
50
- ) : messages.length === 0 && !agent.isRunning ? (
51
- <div className="px-4 py-5 text-sm text-gray-500">
52
- {t("chatChildSessionEmpty")}
53
- </div>
54
- ) : (
55
- <div className="px-4 py-5">
56
- <ChatMessageListContainer
57
- messages={messages}
58
- isSending={agent.isRunning}
59
- />
60
- </div>
61
- )}
62
- </div>
63
- );
64
- }
65
-
66
18
  function ChildSessionMetaChip({ value }: { value: string }) {
67
19
  return (
68
- <span className="inline-flex max-w-full items-center rounded border border-gray-200 bg-gray-50 px-2 py-0.5 text-[10px] font-medium text-gray-600">
20
+ <span className="inline-flex max-w-full shrink-0 items-center rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 text-[10px] font-medium text-gray-600">
69
21
  <span className="truncate">{value}</span>
70
22
  </span>
71
23
  );
@@ -83,41 +35,32 @@ function ChildSessionMetaStrip({ tab }: { tab: ResolvedChildSessionTab }) {
83
35
  }
84
36
 
85
37
  return (
86
- <div className="mt-3 space-y-2">
87
- {metaItems.length > 0 ? (
88
- <div className="flex flex-wrap gap-1.5">
89
- {metaItems.map((item) => (
90
- <ChildSessionMetaChip key={item} value={item} />
91
- ))}
92
- </div>
93
- ) : null}
38
+ <div className="flex min-h-9 min-w-0 items-center gap-1.5 border-b border-gray-200/70 px-3 py-1.5 text-[11px] text-gray-500">
39
+ {metaItems.map((item) => (
40
+ <ChildSessionMetaChip key={item} value={item} />
41
+ ))}
94
42
  {tab.projectRoot ? (
95
- <div
43
+ <span
96
44
  title={tab.projectRoot}
97
- className="truncate rounded border border-gray-200 bg-gray-50 px-2 py-1.5 font-mono text-[11px] text-gray-500"
45
+ className="min-w-0 flex-1 truncate rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 font-mono text-[10px] text-gray-500"
98
46
  >
99
47
  {tab.projectRoot}
100
- </div>
48
+ </span>
101
49
  ) : null}
102
50
  </div>
103
51
  );
104
52
  }
105
53
 
106
- function WorkspaceActiveChildHeader({ tab }: { tab: ResolvedChildSessionTab }) {
54
+ function WorkspaceSideChatDraftHeader() {
107
55
  return (
108
56
  <div className="border-b border-gray-200/70 px-4 py-3">
109
57
  <div className="flex min-w-0 items-center gap-2 text-sm font-semibold text-gray-900">
110
- {tab.agentId ? (
111
- <AgentIdentityAvatar
112
- agentId={tab.agentId}
113
- className="h-4 w-4 shrink-0"
114
- />
115
- ) : (
116
- <FolderGit2 className="h-4 w-4 shrink-0 text-gray-400" />
117
- )}
118
- <span className="truncate">{tab.title}</span>
58
+ <MessageSquarePlus className="h-4 w-4 shrink-0 text-primary" />
59
+ <span className="truncate">{t("chatWorkspaceSideChatDraftTitle")}</span>
119
60
  </div>
120
- <ChildSessionMetaStrip tab={tab} />
61
+ <p className="mt-1 text-xs leading-5 text-gray-500">
62
+ {t("chatWorkspaceSideChatDraftSubtitle")}
63
+ </p>
121
64
  </div>
122
65
  );
123
66
  }
@@ -130,12 +73,37 @@ export function ChatSessionWorkspacePanelContent({
130
73
  }: ChatSessionWorkspacePanelContentProps) {
131
74
  const presenter = usePresenter();
132
75
 
76
+ if (activeSelection.kind === "side-chat-draft") {
77
+ return (
78
+ <>
79
+ <WorkspaceSideChatDraftHeader />
80
+ <div className="flex min-h-0 flex-1 flex-col">
81
+ <SessionConversationArea
82
+ materializationContext={{
83
+ kind: "child",
84
+ parentSessionKey: activeSelection.draft.parentSessionKey,
85
+ inheritContext: true,
86
+ }}
87
+ sessionKey={null}
88
+ showWelcomeForDraft={false}
89
+ onSessionMaterialized={(sessionKey) =>
90
+ presenter.chatThreadManager.materializeSideChatDraft({
91
+ draftKey: activeSelection.draft.draftKey,
92
+ sessionKey,
93
+ })
94
+ }
95
+ />
96
+ </div>
97
+ </>
98
+ );
99
+ }
100
+
133
101
  if (activeSelection.kind === "child-session") {
134
102
  return (
135
103
  <>
136
- <WorkspaceActiveChildHeader tab={activeSelection.tab} />
137
- <div className="flex-1 min-h-0">
138
- <ChildSessionContent sessionKey={activeSelection.tab.sessionKey} />
104
+ <ChildSessionMetaStrip tab={activeSelection.tab} />
105
+ <div className="flex min-h-0 flex-1 flex-col">
106
+ <SessionConversationArea sessionKey={activeSelection.tab.sessionKey} />
139
107
  </div>
140
108
  </>
141
109
  );
@@ -1,4 +1,4 @@
1
- import { AlarmClock, ArrowLeft, ArrowRight, FileCode2, MessageSquareText, X } from "lucide-react";
1
+ import { AlarmClock, ArrowLeft, ArrowRight, FileCode2, MessageSquarePlus, MessageSquareText, X } from "lucide-react";
2
2
  import type { WorkspaceTabViewModel } from "@/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils";
3
3
  import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
4
4
  import {
@@ -17,6 +17,10 @@ function WorkspaceTabIcon({ agentId, kind }: Pick<WorkspaceTabViewModel, "agentI
17
17
  return <FileCode2 className="h-3.5 w-3.5 shrink-0 text-gray-400" />;
18
18
  }
19
19
 
20
+ if (kind === "side-chat-draft") {
21
+ return <MessageSquarePlus className="h-3.5 w-3.5 shrink-0 text-primary" />;
22
+ }
23
+
20
24
  if (agentId) {
21
25
  return (
22
26
  <AgentIdentityAvatar agentId={agentId} className="h-3.5 w-3.5 shrink-0" />
@@ -4,6 +4,7 @@ import { useNcpChildSessionTabsView } from "@/features/chat/features/ncp/hooks/u
4
4
  import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
5
5
  import type {
6
6
  ChatChildSessionTab,
7
+ ChatWorkspaceSideChatDraft,
7
8
  ChatWorkspaceNavigationEntry,
8
9
  ChatWorkspaceFileTab,
9
10
  } from "@/features/chat/stores/chat-thread.store";
@@ -26,11 +27,12 @@ type ChatSessionWorkspacePanelProps = {
26
27
  sessionKey: string | null;
27
28
  childSessionTabs: readonly ChatChildSessionTab[];
28
29
  activeChildSessionKey: string | null;
30
+ activeSideChatDraft: ChatWorkspaceSideChatDraft | null;
29
31
  workspaceFileTabs: readonly ChatWorkspaceFileTab[];
30
32
  activeWorkspaceFileKey: string | null;
31
33
  workspaceNavigationHistory?: readonly ChatWorkspaceNavigationEntry[];
32
34
  workspaceNavigationHistoryIndex?: number;
33
- activePanelKind?: "child-session" | "file" | "cron" | null;
35
+ activePanelKind?: "child-session" | "side-chat-draft" | "file" | "cron" | null;
34
36
  sessionCronJobs?: readonly CronJobView[];
35
37
  sessionProjectRoot: string | null;
36
38
  sessionWorkingDir: string | null;
@@ -41,6 +43,7 @@ export function ChatSessionWorkspacePanel({
41
43
  sessionKey,
42
44
  childSessionTabs,
43
45
  activeChildSessionKey,
46
+ activeSideChatDraft,
44
47
  workspaceFileTabs,
45
48
  activeWorkspaceFileKey,
46
49
  workspaceNavigationHistory = [],
@@ -58,6 +61,7 @@ export function ChatSessionWorkspacePanel({
58
61
  );
59
62
  const activeSelection = resolveWorkspaceSelection({
60
63
  activeChildSessionKey,
64
+ activeSideChatDraft,
61
65
  activeWorkspaceFileKey,
62
66
  activePanelKind,
63
67
  childSessionTabs: resolvedChildTabs,
@@ -77,6 +81,7 @@ export function ChatSessionWorkspacePanel({
77
81
  () =>
78
82
  buildWorkspaceTabsViewModel({
79
83
  resolvedChildTabs,
84
+ activeSideChatDraft,
80
85
  workspaceFileTabs,
81
86
  sessionCronJobCount: sessionCronJobs.length,
82
87
  activeSelection,
@@ -91,6 +96,7 @@ export function ChatSessionWorkspacePanel({
91
96
  }),
92
97
  [
93
98
  activeSelection,
99
+ activeSideChatDraft,
94
100
  optimisticReadAtBySessionKey,
95
101
  presenter.chatThreadManager,
96
102
  resolvedChildTabs,
@@ -1,63 +1,72 @@
1
1
  import { useMemo } from "react";
2
2
  import { useCronJobs } from "@/features/cron";
3
3
  import type { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
4
+ import { adaptNcpSessionSummaries } from "@/features/chat/features/session/utils/ncp-session-adapter.utils";
4
5
  import { isCronJobForSession } from "@/shared/lib/cron";
6
+ import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
5
7
 
6
8
  type ChatThreadSnapshot = ReturnType<
7
9
  typeof useChatThreadStore.getState
8
10
  >["snapshot"];
9
11
 
10
- function shouldShowWorkspacePanel(
11
- snapshot: ChatThreadSnapshot,
12
- childSessionTabs: ChatThreadSnapshot["childSessionTabs"],
13
- workspaceFileTabs: ChatThreadSnapshot["workspaceFileTabs"],
14
- sessionCronJobCount: number,
15
- ) {
16
- if (snapshot.workspacePanelParentKey !== snapshot.sessionKey) {
17
- return false;
18
- }
19
- return (
20
- childSessionTabs.length > 0 ||
21
- workspaceFileTabs.length > 0 ||
22
- sessionCronJobCount > 0
23
- );
24
- }
12
+ const EMPTY_SESSION_SUMMARIES: never[] = [];
25
13
 
26
14
  export function useChatConversationWorkspaceState(
27
15
  snapshot: ChatThreadSnapshot,
16
+ sessionKey: string | null,
28
17
  ) {
29
- const childSessionTabs = useMemo(
30
- () =>
31
- snapshot.childSessionTabs.filter(
32
- (tab) => tab.parentSessionKey === snapshot.sessionKey,
33
- ),
34
- [snapshot.childSessionTabs, snapshot.sessionKey],
18
+ const sessionSummaries = useChatQueryStore(
19
+ (state) =>
20
+ state.snapshot.sessionsQuery?.data?.sessions ?? EMPTY_SESSION_SUMMARIES,
35
21
  );
22
+ const childSessionTabs = useMemo(() => {
23
+ const tabs = snapshot.childSessionTabs.filter(
24
+ (tab) => tab.parentSessionKey === sessionKey,
25
+ );
26
+ const tabKeys = new Set(tabs.map((tab) => tab.sessionKey));
27
+ return [
28
+ ...tabs,
29
+ ...adaptNcpSessionSummaries(sessionSummaries)
30
+ .filter(
31
+ (session) =>
32
+ session.parentSessionId === sessionKey && !tabKeys.has(session.key),
33
+ )
34
+ .map((session) => ({
35
+ sessionKey: session.key,
36
+ parentSessionKey: sessionKey,
37
+ })),
38
+ ];
39
+ }, [sessionKey, sessionSummaries, snapshot.childSessionTabs]);
36
40
  const workspaceFileTabs = useMemo(
37
41
  () =>
38
42
  snapshot.workspaceFileTabs.filter(
39
- (tab) => tab.parentSessionKey === snapshot.sessionKey,
43
+ (tab) => tab.parentSessionKey === sessionKey,
40
44
  ),
41
- [snapshot.sessionKey, snapshot.workspaceFileTabs],
45
+ [sessionKey, snapshot.workspaceFileTabs],
42
46
  );
47
+ const activeSideChatDraft =
48
+ snapshot.activeSideChatDraft?.parentSessionKey === sessionKey
49
+ ? snapshot.activeSideChatDraft
50
+ : null;
43
51
  const cronQuery = useCronJobs({ all: true });
44
52
  const sessionCronJobs = useMemo(
45
53
  () =>
46
54
  (cronQuery.data?.jobs ?? []).filter((job) =>
47
- isCronJobForSession(job, snapshot.sessionKey),
55
+ isCronJobForSession(job, sessionKey),
48
56
  ),
49
- [cronQuery.data?.jobs, snapshot.sessionKey],
57
+ [cronQuery.data?.jobs, sessionKey],
50
58
  );
51
59
 
52
60
  return {
53
61
  childSessionTabs,
62
+ activeSideChatDraft,
54
63
  workspaceFileTabs,
55
64
  sessionCronJobs,
56
- showWorkspacePanel: shouldShowWorkspacePanel(
57
- snapshot,
58
- childSessionTabs,
59
- workspaceFileTabs,
60
- sessionCronJobs.length,
61
- ),
65
+ showWorkspacePanel:
66
+ snapshot.workspacePanelParentKey === sessionKey &&
67
+ (activeSideChatDraft !== null ||
68
+ childSessionTabs.length > 0 ||
69
+ workspaceFileTabs.length > 0 ||
70
+ sessionCronJobs.length > 0),
62
71
  };
63
72
  }
@@ -3,7 +3,7 @@ import type { ResolvedChildSessionTab } from "@/features/chat/features/ncp/hooks
3
3
  import {
4
4
  buildWorkspaceTabsViewModel,
5
5
  resolveWorkspaceSelection,
6
- } from "../chat-workspace-panel-view-model.utils";
6
+ } from "@/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils";
7
7
 
8
8
  function createChildTab(
9
9
  overrides: Partial<ResolvedChildSessionTab> = {},
@@ -39,6 +39,7 @@ describe("resolveWorkspaceSelection", () => {
39
39
  resolveWorkspaceSelection({
40
40
  activePanelKind: "file",
41
41
  activeChildSessionKey: childTab.sessionKey,
42
+ activeSideChatDraft: null,
42
43
  activeWorkspaceFileKey: fileTab.key,
43
44
  childSessionTabs: [childTab],
44
45
  workspaceFileTabs: [fileTab],
@@ -57,6 +58,7 @@ describe("resolveWorkspaceSelection", () => {
57
58
  resolveWorkspaceSelection({
58
59
  activePanelKind: null,
59
60
  activeChildSessionKey: null,
61
+ activeSideChatDraft: null,
60
62
  activeWorkspaceFileKey: null,
61
63
  childSessionTabs: [childTab],
62
64
  workspaceFileTabs: [],
@@ -67,6 +69,28 @@ describe("resolveWorkspaceSelection", () => {
67
69
  tab: childTab,
68
70
  });
69
71
  });
72
+
73
+ it("honors the explicit side chat draft selection", () => {
74
+ const draft = {
75
+ draftKey: "draft-1",
76
+ parentSessionKey: "parent-1",
77
+ };
78
+
79
+ expect(
80
+ resolveWorkspaceSelection({
81
+ activePanelKind: "side-chat-draft",
82
+ activeChildSessionKey: null,
83
+ activeSideChatDraft: draft,
84
+ activeWorkspaceFileKey: null,
85
+ childSessionTabs: [createChildTab()],
86
+ workspaceFileTabs: [],
87
+ sessionCronJobCount: 0,
88
+ }),
89
+ ).toMatchObject({
90
+ kind: "side-chat-draft",
91
+ draft,
92
+ });
93
+ });
70
94
  });
71
95
 
72
96
  describe("buildWorkspaceTabsViewModel", () => {
@@ -79,6 +103,7 @@ describe("buildWorkspaceTabsViewModel", () => {
79
103
 
80
104
  const tabs = buildWorkspaceTabsViewModel({
81
105
  resolvedChildTabs: [childTab],
106
+ activeSideChatDraft: null,
82
107
  workspaceFileTabs: [],
83
108
  sessionCronJobCount: 0,
84
109
  activeSelection: null,
@@ -100,4 +125,37 @@ describe("buildWorkspaceTabsViewModel", () => {
100
125
  tabs[0]?.onSelect();
101
126
  expect(onSelectSession).toHaveBeenCalledWith("child-1");
102
127
  });
128
+
129
+ it("places the side chat draft tab before child session tabs", () => {
130
+ const draft = {
131
+ draftKey: "draft-1",
132
+ parentSessionKey: "parent-1",
133
+ };
134
+ const childTab = createChildTab();
135
+
136
+ const tabs = buildWorkspaceTabsViewModel({
137
+ resolvedChildTabs: [childTab],
138
+ activeSideChatDraft: draft,
139
+ workspaceFileTabs: [],
140
+ sessionCronJobCount: 0,
141
+ activeSelection: {
142
+ kind: "side-chat-draft",
143
+ draft,
144
+ },
145
+ optimisticReadAtBySessionKey: {},
146
+ onSelectSession: vi.fn(),
147
+ onSelectFile: vi.fn(),
148
+ onCloseFile: vi.fn(),
149
+ onSelectCronJobs: vi.fn(),
150
+ });
151
+
152
+ expect(tabs.map((tab) => tab.key)).toEqual([
153
+ "side-chat-draft:draft-1",
154
+ "child:child-1",
155
+ ]);
156
+ expect(tabs[0]).toMatchObject({
157
+ kind: "side-chat-draft",
158
+ active: true,
159
+ });
160
+ });
103
161
  });
@@ -0,0 +1,109 @@
1
+ import type {
2
+ ChatChildSessionTab,
3
+ ChatThreadSnapshot,
4
+ ChatWorkspaceNavigationEntry,
5
+ ChatWorkspaceSideChatDraft,
6
+ } from "@/features/chat/stores/chat-thread.store";
7
+ import {
8
+ filterNavigationHistoryEntries,
9
+ pushNavigationHistoryEntry,
10
+ } from "@/shared/lib/navigation-history";
11
+
12
+ export function areWorkspaceNavigationEntriesEqual(
13
+ current: ChatWorkspaceNavigationEntry,
14
+ next: ChatWorkspaceNavigationEntry,
15
+ ): boolean {
16
+ if (current.kind !== next.kind) {
17
+ return false;
18
+ }
19
+ if (current.kind === 'cron') {
20
+ return true;
21
+ }
22
+ return next.kind !== 'cron' && current.key === next.key;
23
+ }
24
+
25
+ export function createSideChatDraft(parentSessionKey: string): ChatWorkspaceSideChatDraft {
26
+ const randomPart =
27
+ typeof globalThis.crypto?.randomUUID === 'function'
28
+ ? globalThis.crypto.randomUUID()
29
+ : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
30
+ return {
31
+ draftKey: `side-chat-draft:${parentSessionKey}:${randomPart}`,
32
+ parentSessionKey,
33
+ };
34
+ }
35
+
36
+ export function upsertChildSessionTab(
37
+ childSessionTabs: readonly ChatChildSessionTab[],
38
+ nextTab: ChatChildSessionTab,
39
+ ): ChatChildSessionTab[] {
40
+ const existingIndex = childSessionTabs.findIndex((tab) => tab.sessionKey === nextTab.sessionKey);
41
+ if (existingIndex === -1) {
42
+ return [nextTab, ...childSessionTabs];
43
+ }
44
+ const existingTab = childSessionTabs[existingIndex];
45
+ const nextTabs = [...childSessionTabs];
46
+ nextTabs[existingIndex] = {
47
+ ...existingTab,
48
+ parentSessionKey: existingTab.parentSessionKey ?? nextTab.parentSessionKey,
49
+ label: existingTab.label?.trim() ? existingTab.label : nextTab.label,
50
+ agentId: existingTab.agentId?.trim() ? existingTab.agentId : nextTab.agentId,
51
+ };
52
+ return nextTabs;
53
+ }
54
+
55
+ export function materializeSideChatDraftSnapshot(params: {
56
+ snapshot: ChatThreadSnapshot;
57
+ draftKey: string;
58
+ sessionKey: string;
59
+ label?: string | null;
60
+ agentId?: string | null;
61
+ }): { patch: Partial<ChatThreadSnapshot>; parentSessionKey: string } | null {
62
+ const {
63
+ agentId: rawAgentId,
64
+ draftKey: rawDraftKey,
65
+ label: rawLabel,
66
+ sessionKey: rawSessionKey,
67
+ snapshot,
68
+ } = params;
69
+ const sessionKey = rawSessionKey.trim();
70
+ const draftKey = rawDraftKey.trim();
71
+ const { activeSideChatDraft } = snapshot;
72
+ if (!sessionKey || !draftKey || activeSideChatDraft?.draftKey !== draftKey) {
73
+ return null;
74
+ }
75
+ const { parentSessionKey } = activeSideChatDraft;
76
+ const filteredHistory = filterNavigationHistoryEntries(
77
+ {
78
+ entries: snapshot.workspaceNavigationHistory,
79
+ index: snapshot.workspaceNavigationHistoryIndex,
80
+ },
81
+ (entry) => entry.kind !== 'side-chat-draft' || entry.key !== draftKey,
82
+ );
83
+ const history = pushNavigationHistoryEntry(
84
+ filteredHistory,
85
+ {
86
+ kind: 'child-session',
87
+ key: sessionKey,
88
+ },
89
+ areWorkspaceNavigationEntriesEqual,
90
+ );
91
+ return {
92
+ parentSessionKey,
93
+ patch: {
94
+ workspacePanelParentKey: parentSessionKey,
95
+ activeWorkspacePanelKind: 'child-session',
96
+ activeChildSessionKey: sessionKey,
97
+ activeSideChatDraft: null,
98
+ activeWorkspaceFileKey: null,
99
+ childSessionTabs: upsertChildSessionTab(snapshot.childSessionTabs, {
100
+ sessionKey,
101
+ parentSessionKey,
102
+ label: rawLabel?.trim() || null,
103
+ agentId: rawAgentId?.trim() || null,
104
+ }),
105
+ workspaceNavigationHistory: [...history.entries],
106
+ workspaceNavigationHistoryIndex: history.index,
107
+ },
108
+ };
109
+ }
@@ -1,6 +1,9 @@
1
1
  import type { ResolvedChildSessionTab } from "@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view";
2
2
  import { shouldShowUnreadSessionIndicator } from "@/features/chat/stores/chat-session-list.store";
3
- import type { ChatWorkspaceFileTab } from "@/features/chat/stores/chat-thread.store";
3
+ import type {
4
+ ChatWorkspaceFileTab,
5
+ ChatWorkspaceSideChatDraft,
6
+ } from "@/features/chat/stores/chat-thread.store";
4
7
  import { t } from "@/shared/lib/i18n";
5
8
 
6
9
  export type WorkspaceSelection =
@@ -8,6 +11,10 @@ export type WorkspaceSelection =
8
11
  kind: "child-session";
9
12
  tab: ResolvedChildSessionTab;
10
13
  }
14
+ | {
15
+ kind: "side-chat-draft";
16
+ draft: ChatWorkspaceSideChatDraft;
17
+ }
11
18
  | {
12
19
  kind: "file";
13
20
  file: ChatWorkspaceFileTab;
@@ -18,7 +25,7 @@ export type WorkspaceSelection =
18
25
 
19
26
  export type WorkspaceTabViewModel = {
20
27
  key: string;
21
- kind: "child-session" | "file" | "cron";
28
+ kind: "child-session" | "side-chat-draft" | "file" | "cron";
22
29
  title: string;
23
30
  tooltip: string;
24
31
  active: boolean;
@@ -35,8 +42,9 @@ export function readWorkspaceFileTitle(file: ChatWorkspaceFileTab): string {
35
42
  }
36
43
 
37
44
  export function resolveWorkspaceSelection(params: {
38
- activePanelKind?: "child-session" | "file" | "cron" | null;
45
+ activePanelKind?: "child-session" | "side-chat-draft" | "file" | "cron" | null;
39
46
  activeChildSessionKey: string | null;
47
+ activeSideChatDraft: ChatWorkspaceSideChatDraft | null;
40
48
  activeWorkspaceFileKey: string | null;
41
49
  childSessionTabs: ResolvedChildSessionTab[];
42
50
  workspaceFileTabs: readonly ChatWorkspaceFileTab[];
@@ -45,6 +53,7 @@ export function resolveWorkspaceSelection(params: {
45
53
  const {
46
54
  activePanelKind,
47
55
  activeChildSessionKey,
56
+ activeSideChatDraft,
48
57
  activeWorkspaceFileKey,
49
58
  childSessionTabs,
50
59
  workspaceFileTabs,
@@ -55,6 +64,13 @@ export function resolveWorkspaceSelection(params: {
55
64
  return { kind: "cron" };
56
65
  }
57
66
 
67
+ if (activePanelKind === "side-chat-draft" && activeSideChatDraft) {
68
+ return {
69
+ kind: "side-chat-draft",
70
+ draft: activeSideChatDraft,
71
+ };
72
+ }
73
+
58
74
  if (activePanelKind !== "child-session" && activeWorkspaceFileKey) {
59
75
  const activeFile = workspaceFileTabs.find(
60
76
  (file) => file.key === activeWorkspaceFileKey,
@@ -86,6 +102,13 @@ export function resolveWorkspaceSelection(params: {
86
102
  };
87
103
  }
88
104
 
105
+ if (activeSideChatDraft) {
106
+ return {
107
+ kind: "side-chat-draft",
108
+ draft: activeSideChatDraft,
109
+ };
110
+ }
111
+
89
112
  if (workspaceFileTabs[0]) {
90
113
  return {
91
114
  kind: "file",
@@ -102,6 +125,7 @@ export function resolveWorkspaceSelection(params: {
102
125
 
103
126
  export function buildWorkspaceTabsViewModel(params: {
104
127
  resolvedChildTabs: ResolvedChildSessionTab[];
128
+ activeSideChatDraft: ChatWorkspaceSideChatDraft | null;
105
129
  workspaceFileTabs: readonly ChatWorkspaceFileTab[];
106
130
  sessionCronJobCount: number;
107
131
  activeSelection: WorkspaceSelection | null;
@@ -112,6 +136,7 @@ export function buildWorkspaceTabsViewModel(params: {
112
136
  onSelectCronJobs: () => void;
113
137
  }): WorkspaceTabViewModel[] {
114
138
  const {
139
+ activeSideChatDraft,
115
140
  resolvedChildTabs,
116
141
  workspaceFileTabs,
117
142
  sessionCronJobCount,
@@ -123,6 +148,21 @@ export function buildWorkspaceTabsViewModel(params: {
123
148
  onSelectCronJobs,
124
149
  } = params;
125
150
 
151
+ const sideChatDraftTabs = activeSideChatDraft
152
+ ? [
153
+ {
154
+ key: `side-chat-draft:${activeSideChatDraft.draftKey}`,
155
+ kind: "side-chat-draft" as const,
156
+ title: t("chatWorkspaceSideChatDraftTitle"),
157
+ tooltip: t("chatWorkspaceSideChatDraftSubtitle"),
158
+ active:
159
+ activeSelection?.kind === "side-chat-draft" &&
160
+ activeSelection.draft.draftKey === activeSideChatDraft.draftKey,
161
+ onSelect: () => undefined,
162
+ },
163
+ ]
164
+ : [];
165
+
126
166
  const childTabs = resolvedChildTabs.map((tab) => {
127
167
  const optimisticReadAt = optimisticReadAtBySessionKey[tab.sessionKey];
128
168
  const effectiveReadAt =
@@ -179,5 +219,5 @@ export function buildWorkspaceTabsViewModel(params: {
179
219
  ]
180
220
  : [];
181
221
 
182
- return [...childTabs, ...fileTabs, ...cronTab];
222
+ return [...sideChatDraftTabs, ...childTabs, ...fileTabs, ...cronTab];
183
223
  }
@@ -4,7 +4,6 @@ export { ChatPresenterProvider } from "./components/providers/chat-presenter.pro
4
4
  export { usePresenter } from "./components/providers/chat-presenter.provider";
5
5
  export { ChatPresenter } from "./presenters/chat.presenter";
6
6
  export { ChatDraftIntentManager } from "./managers/chat-draft-intent.manager";
7
- export { useChatInputStore } from "./stores/chat-input.store";
8
7
  export { useChatSessionListStore } from "./stores/chat-session-list.store";
9
8
  export { useChatThreadStore } from "./stores/chat-thread.store";
10
9
  export { useNcpChatSessionTypes } from "./features/session-type/hooks/use-ncp-chat-session-types";