@nextclaw/ui 0.14.1-beta.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 (179) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/assets/{api-dQ6uQSZG.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-CeFfbaqc.js → config-split-page-B5b9B0uE.js} +1 -1
  6. package/dist/assets/{confirm-dialog-C95Y4QPC.js → confirm-dialog-ClH_Ec7n.js} +1 -1
  7. package/dist/assets/{desktop-update-config-CcK6jiEb.js → desktop-update-config-DPNDnVpk.js} +1 -1
  8. package/dist/assets/{dist-DTGzeil6.js → dist-BtCS2Ucq.js} +1 -1
  9. package/dist/assets/{dist-fGogBmPA.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-D2F4KMiU.js → ellipsis-CtJXjIol.js} +1 -1
  14. package/dist/assets/{external-link-CcgtBK26.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-CTqdhFMk.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-C9fKWfVB.js → save-DKA0x4zk.js} +1 -1
  29. package/dist/assets/{search-D5FQ5q0X.js → search-BNlcoj5c.js} +1 -1
  30. package/dist/assets/{search-config-page-AMEJJNIS.js → search-config-page-Cq9uJ-N8.js} +1 -1
  31. package/dist/assets/{secrets-config-page-CcgJnvBO.js → secrets-config-page-CxSbZLBo.js} +2 -2
  32. package/dist/assets/{select-Cu5n0E-3.js → select-CvTlDZTQ.js} +1 -1
  33. package/dist/assets/{tag-chip-C5aOtjxu.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 +77 -12
  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 +182 -21
  60. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +82 -25
  61. package/src/features/chat/components/layout/chat-sidebar.tsx +79 -93
  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/hooks/use-chat-model-favorites.ts +2 -1
  73. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
  74. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
  75. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
  76. package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
  77. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
  78. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
  79. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
  80. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
  81. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
  82. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
  83. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
  84. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
  85. package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
  86. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
  87. package/src/features/chat/features/message/components/chat-message-list.container.tsx +93 -12
  88. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
  89. package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
  90. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  91. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +5 -4
  92. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  93. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
  94. package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
  95. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
  96. package/src/features/chat/features/session-type/components/__tests__/{chat-sidebar-create-menu.test.tsx → chat-session-type-menu.test.tsx} +5 -5
  97. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +29 -1
  98. package/src/features/chat/features/session-type/components/{chat-sidebar-create-menu.tsx → chat-session-type-menu.tsx} +13 -9
  99. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +17 -5
  100. package/src/features/chat/features/session-type/hooks/__tests__/use-chat-new-session-type-preference.test.tsx +152 -0
  101. package/src/features/chat/features/session-type/hooks/use-chat-new-session-type-preference.ts +159 -0
  102. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
  103. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -2
  104. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
  105. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +2 -2
  106. package/src/features/chat/features/welcome/components/chat-welcome.tsx +2 -2
  107. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
  108. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
  109. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
  110. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
  111. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
  112. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
  113. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
  114. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
  115. package/src/features/chat/index.ts +0 -1
  116. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
  117. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
  118. package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
  119. package/src/features/chat/managers/chat-thread.manager.ts +108 -34
  120. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
  121. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
  122. package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
  123. package/src/features/chat/presenters/chat.presenter.ts +2 -19
  124. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
  125. package/src/features/chat/stores/chat-thread.store.ts +42 -26
  126. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +24 -0
  127. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +8 -3
  128. package/src/features/marketplace/components/marketplace-item-list-view.tsx +2 -2
  129. package/src/features/marketplace/components/marketplace-list-card.tsx +124 -98
  130. package/src/features/marketplace/components/marketplace-page-parts.tsx +11 -6
  131. package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
  132. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
  133. package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
  134. package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
  135. package/src/shared/components/common/__tests__/agent-avatar.test.tsx +3 -1
  136. package/src/shared/components/common/agent-avatar.tsx +5 -2
  137. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
  138. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
  139. package/src/shared/lib/api/index.ts +1 -0
  140. package/src/shared/lib/api/preferences/preference-keys.config.ts +9 -0
  141. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
  142. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  143. package/src/shared/lib/i18n/locales/en-US/marketplace.json +2 -0
  144. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
  145. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  146. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +2 -0
  147. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
  148. package/src/shared/lib/ui-document-title/index.ts +50 -21
  149. package/dist/assets/channels-list-page-DfpjB8XB.js +0 -8
  150. package/dist/assets/chat-page-D3QaEqjs.js +0 -105
  151. package/dist/assets/doc-browser-CuBTcBEZ.js +0 -1
  152. package/dist/assets/doc-browser-DVHhEb96.js +0 -1
  153. package/dist/assets/doc-browser-context-BfMXwnrz.js +0 -1
  154. package/dist/assets/index-ByJCbQqr.css +0 -1
  155. package/dist/assets/index-DjXqiloY.js +0 -103
  156. package/dist/assets/mcp-marketplace-page-98-V__3u.js +0 -1
  157. package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +0 -40
  158. package/dist/assets/model-config-page-aAUkZkSX.js +0 -1
  159. package/dist/assets/plus-1Rxl5FlV.js +0 -1
  160. package/dist/assets/provider-scoped-model-input-we4_Z4gP.js +0 -1
  161. package/dist/assets/providers-config-page-CtMkPSyu.js +0 -1
  162. package/dist/assets/remote-fQ7N_r8u.js +0 -1
  163. package/dist/assets/rotate-cw-CzfIeAJj.js +0 -1
  164. package/dist/assets/runtime-config-page-TD1UrTbG.js +0 -1
  165. package/dist/assets/use-config-D8TESeo6.js +0 -1
  166. package/dist/assets/x-1qETdQ70.js +0 -1
  167. package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
  168. package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
  169. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
  170. package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
  171. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
  172. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
  173. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
  174. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
  175. package/src/features/chat/managers/chat-input.manager.ts +0 -430
  176. package/src/features/chat/managers/chat-run.manager.ts +0 -110
  177. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
  178. package/src/features/chat/stores/chat-input.store.ts +0 -104
  179. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
@@ -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";