@nextclaw/ui 0.15.5 → 0.15.7

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 (169) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/api-V8m62-13.js +13 -0
  3. package/dist/assets/app-presenter-provider-Bzj9h_ZS.js +106 -0
  4. package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DwBwu1AU.js} +1 -1
  5. package/dist/assets/{book-open-CN7Shpij.js → book-open-DtVSr5O9.js} +1 -1
  6. package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-CYIqMliJ.js} +2 -2
  7. package/dist/assets/{chat-page-152YuwfU.js → chat-page-BC0icTtI.js} +1 -1
  8. package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-B88prd8s.js} +1 -1
  9. package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-BB09IC2u.js} +1 -1
  10. package/dist/assets/desktop-update-config-B-q0OhxO.js +1 -0
  11. package/dist/assets/{dist-D5ycbwCo.js → dist-Bytser-a.js} +1 -1
  12. package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-B1FXvZm6.js} +1 -1
  13. package/dist/assets/doc-browser-Bodhf0fj2.js +1 -0
  14. package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-D5VLNoj3.js} +1 -1
  15. package/dist/assets/{download-CyPgglKr.js → download-CdO6NoHj.js} +1 -1
  16. package/dist/assets/{external-link-BahW1sl3.js → external-link-C5NFoh5I.js} +1 -1
  17. package/dist/assets/{eye-D8gGXMkD.js → eye-B51oNRRc.js} +1 -1
  18. package/dist/assets/{folder-DGWqFDvE.js → folder-DxH7Nef-.js} +1 -1
  19. package/dist/assets/{form-actions-D2imZLnU.js → form-actions-C9pUwtYV.js} +1 -1
  20. package/dist/assets/index-DtWf7kk9.css +1 -0
  21. package/dist/assets/index-UnY4HtVa.js +2 -0
  22. package/dist/assets/loader-circle-BRSfaztA.js +1 -0
  23. package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-D2vORX3a.js} +1 -1
  24. package/dist/assets/mcp-marketplace-page-CGVu7mfL.js +9 -0
  25. package/dist/assets/mcp-marketplace-page-DnV0e1JS.js +1 -0
  26. package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-DYpchGVE.js} +1 -1
  27. package/dist/assets/plus-BgMBV9HJ.js +1 -0
  28. package/dist/assets/{popover-Btp7KVc3.js → popover-CONujDHf.js} +1 -1
  29. package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-DA3Ua9v8.js} +1 -1
  30. package/dist/assets/providers-config-page-DXsoyMrh.js +1 -0
  31. package/dist/assets/{react-BY-U3h3t.js → react-Io6Pfq8D.js} +1 -1
  32. package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-C6j2tjul.js} +1 -1
  33. package/dist/assets/remote-C1y3ARHu.js +1 -0
  34. package/dist/assets/rotate-cw-Bv2B6fAV.js +1 -0
  35. package/dist/assets/runtime-config-page-eYrrKBrA.js +1 -0
  36. package/dist/assets/{save-DNg2yJTS.js → save-CBBZVvZ-.js} +1 -1
  37. package/dist/assets/{search-3ykfrAFt.js → search-DFF842z2.js} +1 -1
  38. package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-DUyuwD7d.js} +1 -1
  39. package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-DwzKQJ76.js} +2 -2
  40. package/dist/assets/security-config-D8me3pXl.js +1 -0
  41. package/dist/assets/{select-B5EecXJt.js → select-BiOpo2kV.js} +1 -1
  42. package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-C3WQXLtS.js} +1 -1
  43. package/dist/assets/{settings-2-D-3um9Az.js → settings-2-DyOvA_6K.js} +1 -1
  44. package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-CQIAEbA4.js} +1 -1
  45. package/dist/assets/{tabs-BVD8gp3y.js → tabs-BTSENlI2.js} +1 -1
  46. package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-BG5JIGwY.js} +1 -1
  47. package/dist/assets/{trash-2-cnsMppRC.js → trash-2-CTIfwpt1.js} +1 -1
  48. package/dist/assets/use-config-CY4uF2Ou.js +1 -0
  49. package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-Bj4u78F9.js} +1 -1
  50. package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-ChJUcSQ6.js} +1 -1
  51. package/dist/assets/x-C6cM8wkT.js +1 -0
  52. package/dist/index.html +30 -29
  53. package/index.html +1 -0
  54. package/package.json +8 -8
  55. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +97 -3
  56. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
  58. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
  59. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
  60. package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
  61. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
  62. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +70 -2
  63. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
  64. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
  65. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
  67. package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
  68. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +82 -0
  69. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +187 -63
  70. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +225 -0
  71. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
  72. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +6 -1
  73. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +357 -0
  74. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
  75. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
  76. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +67 -5
  77. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
  78. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
  79. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +83 -8
  80. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
  81. package/src/features/chat/features/message/components/__tests__/chat-inline-file-preview.test.tsx +159 -0
  82. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +92 -26
  83. package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +90 -21
  84. package/src/features/chat/features/message/components/chat-message-list.container.tsx +24 -3
  85. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
  86. package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +96 -3
  87. package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
  88. package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
  89. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
  90. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
  91. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
  92. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
  93. package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
  94. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
  95. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +82 -128
  96. package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
  97. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
  98. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
  99. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
  100. package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
  101. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
  102. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
  103. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +5 -6
  104. package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +67 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +9 -7
  106. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +95 -4
  107. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
  108. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +1 -0
  109. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +62 -0
  110. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  111. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -15
  112. package/src/features/marketplace/components/marketplace-page.tsx +10 -2
  113. package/src/features/marketplace/hooks/__tests__/use-marketplace.test.tsx +26 -1
  114. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +11 -0
  115. package/src/features/marketplace/hooks/use-marketplace-list-model.ts +1 -1
  116. package/src/features/marketplace/hooks/use-marketplace.ts +28 -1
  117. package/src/features/settings/components/config/provider-form.tsx +0 -4
  118. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
  119. package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
  120. package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
  121. package/src/features/system-status/index.ts +9 -0
  122. package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
  123. package/src/index.css +10 -16
  124. package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
  125. package/src/shared/components/common/brand-header.tsx +121 -35
  126. package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
  127. package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
  128. package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
  129. package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
  130. package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
  131. package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
  132. package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
  133. package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
  134. package/src/shared/hooks/use-projects.ts +28 -0
  135. package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
  136. package/src/shared/hooks/use-server-path-search.ts +40 -0
  137. package/src/shared/lib/api/index.ts +1 -0
  138. package/src/shared/lib/api/projects/project.utils.ts +14 -0
  139. package/src/shared/lib/api/types.ts +27 -0
  140. package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
  141. package/src/shared/lib/i18n/locales/en-US/chat.json +40 -2
  142. package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
  143. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +5 -1
  144. package/src/shared/lib/i18n/locales/en-US/marketplace.json +1 -0
  145. package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
  146. package/src/shared/lib/i18n/locales/zh-CN/chat.json +40 -2
  147. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
  148. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +5 -1
  149. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +1 -0
  150. package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
  151. package/dist/assets/api-IlX_CbwU.js +0 -13
  152. package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
  153. package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
  154. package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
  155. package/dist/assets/index-0av6WO9-.css +0 -1
  156. package/dist/assets/index-CT0iYlxV.js +0 -2
  157. package/dist/assets/loader-circle-DtKek-qG.js +0 -1
  158. package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
  159. package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
  160. package/dist/assets/plus-BwgE-Zwf.js +0 -1
  161. package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
  162. package/dist/assets/remote-BaVECEZw.js +0 -1
  163. package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
  164. package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
  165. package/dist/assets/security-config-BPzet76z.js +0 -1
  166. package/dist/assets/use-config-D6yx0lb0.js +0 -1
  167. package/dist/assets/x-B3o1dWR5.js +0 -1
  168. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
  169. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +0 -144
@@ -0,0 +1,203 @@
1
+ import { useState } from 'react';
2
+ import { FolderOpen } from 'lucide-react';
3
+ import type { ProjectCreateRequest, ProjectTemplateView } from '@/shared/lib/api';
4
+ import { ServerPathPickerDialog } from '@/shared/components/path-picker/server-path-picker-dialog';
5
+ import { Button } from '@/shared/components/ui/button';
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from '@/shared/components/ui/dialog';
14
+ import { Input } from '@/shared/components/ui/input';
15
+ import { Label } from '@/shared/components/ui/label';
16
+ import {
17
+ Select,
18
+ SelectContent,
19
+ SelectItem,
20
+ SelectTrigger,
21
+ SelectValue,
22
+ } from '@/shared/components/ui/select';
23
+ import { t } from '@/shared/lib/i18n';
24
+
25
+ type ProjectTemplateId = NonNullable<ProjectCreateRequest['template']>;
26
+
27
+ type ChatProjectCreateDialogProps = {
28
+ open: boolean;
29
+ defaultWorkspacePath?: string | null;
30
+ templates: readonly ProjectTemplateView[];
31
+ isCreating: boolean;
32
+ errorMessage?: string | null;
33
+ onOpenChange: (open: boolean) => void;
34
+ onCreate: (input: ProjectCreateRequest) => Promise<void>;
35
+ };
36
+
37
+ function projectTemplateLabel(templateId: ProjectTemplateId): string {
38
+ return t(
39
+ templateId === 'knowledge-base'
40
+ ? 'chatProjectTemplateKnowledgeBase'
41
+ : 'chatProjectTemplateEmpty',
42
+ );
43
+ }
44
+
45
+ export function ChatProjectCreateDialog({
46
+ open,
47
+ defaultWorkspacePath,
48
+ templates,
49
+ isCreating,
50
+ errorMessage,
51
+ onOpenChange,
52
+ onCreate,
53
+ }: ChatProjectCreateDialogProps) {
54
+ const [name, setName] = useState('');
55
+ const [rootPath, setRootPath] = useState('');
56
+ const [pathPickerOpen, setPathPickerOpen] = useState(false);
57
+ const [template, setTemplate] = useState<ProjectTemplateId>('empty');
58
+ const availableTemplateIds = templates.length > 0
59
+ ? templates.map((entry) => entry.id)
60
+ : ['empty' as const];
61
+ const normalizedName = name.trim();
62
+
63
+ const resetForm = () => {
64
+ setName('');
65
+ setRootPath('');
66
+ setTemplate('empty');
67
+ setPathPickerOpen(false);
68
+ };
69
+
70
+ const closeDialog = () => {
71
+ if (isCreating) {
72
+ return;
73
+ }
74
+ resetForm();
75
+ onOpenChange(false);
76
+ };
77
+
78
+ return (
79
+ <>
80
+ <Dialog
81
+ open={open}
82
+ onOpenChange={(nextOpen) => {
83
+ if (nextOpen) {
84
+ onOpenChange(true);
85
+ return;
86
+ }
87
+ closeDialog();
88
+ }}
89
+ >
90
+ <DialogContent className="sm:max-w-lg">
91
+ <DialogHeader>
92
+ <DialogTitle>{t('chatProjectCreateTitle')}</DialogTitle>
93
+ <DialogDescription>{t('chatProjectCreateDescription')}</DialogDescription>
94
+ </DialogHeader>
95
+ <form
96
+ className="space-y-4"
97
+ onSubmit={(event) => {
98
+ event.preventDefault();
99
+ if (!normalizedName || isCreating) {
100
+ return;
101
+ }
102
+ const normalizedRootPath = rootPath.trim();
103
+ void onCreate({
104
+ name: normalizedName,
105
+ template,
106
+ ...(normalizedRootPath ? { rootPath: normalizedRootPath } : {}),
107
+ }).then(resetForm).catch(() => undefined);
108
+ }}
109
+ >
110
+ <div className="space-y-2">
111
+ <Label htmlFor="chat-project-name">{t('chatProjectNameLabel')}</Label>
112
+ <Input
113
+ id="chat-project-name"
114
+ value={name}
115
+ onChange={(event) => setName(event.target.value)}
116
+ placeholder={t('chatProjectNamePlaceholder')}
117
+ autoFocus
118
+ disabled={isCreating}
119
+ />
120
+ </div>
121
+
122
+ <div className="space-y-2">
123
+ <Label htmlFor="chat-project-template">{t('chatProjectTemplateLabel')}</Label>
124
+ <Select
125
+ value={template}
126
+ onValueChange={(value) => setTemplate(value as ProjectTemplateId)}
127
+ disabled={isCreating}
128
+ >
129
+ <SelectTrigger id="chat-project-template">
130
+ <SelectValue />
131
+ </SelectTrigger>
132
+ <SelectContent>
133
+ {availableTemplateIds.map((templateId) => (
134
+ <SelectItem key={templateId} value={templateId}>
135
+ {projectTemplateLabel(templateId)}
136
+ </SelectItem>
137
+ ))}
138
+ </SelectContent>
139
+ </Select>
140
+ </div>
141
+
142
+ <div className="space-y-2">
143
+ <Label htmlFor="chat-project-root-path">{t('chatProjectPathLabel')}</Label>
144
+ <div className="flex gap-2">
145
+ <Input
146
+ id="chat-project-root-path"
147
+ value={rootPath}
148
+ onChange={(event) => setRootPath(event.target.value)}
149
+ placeholder={t('chatProjectPathPlaceholder')}
150
+ disabled={isCreating}
151
+ />
152
+ <Button
153
+ type="button"
154
+ variant="outline"
155
+ onClick={() => setPathPickerOpen(true)}
156
+ disabled={isCreating}
157
+ >
158
+ <FolderOpen className="mr-2 h-4 w-4" />
159
+ {t('browse')}
160
+ </Button>
161
+ </div>
162
+ <p className="text-xs leading-relaxed text-muted-foreground">
163
+ {t('chatProjectPathHint')}
164
+ </p>
165
+ </div>
166
+
167
+ {errorMessage ? (
168
+ <p role="alert" className="text-sm text-destructive">
169
+ {errorMessage}
170
+ </p>
171
+ ) : null}
172
+
173
+ <DialogFooter>
174
+ <Button type="button" variant="outline" onClick={closeDialog} disabled={isCreating}>
175
+ {t('cancel')}
176
+ </Button>
177
+ <Button type="submit" disabled={!normalizedName || isCreating}>
178
+ {isCreating ? t('saving') : t('chatProjectCreateAction')}
179
+ </Button>
180
+ </DialogFooter>
181
+ </form>
182
+ </DialogContent>
183
+ </Dialog>
184
+ <ServerPathPickerDialog
185
+ open={pathPickerOpen}
186
+ currentPath={rootPath}
187
+ defaultWorkspacePath={defaultWorkspacePath}
188
+ isSaving={false}
189
+ onOpenChange={setPathPickerOpen}
190
+ onConfirm={(path) => {
191
+ setRootPath(path);
192
+ setPathPickerOpen(false);
193
+ }}
194
+ title={t('chatProjectPathPickerTitle')}
195
+ description={t('chatProjectPathPickerDescription')}
196
+ pathLabel={t('chatProjectPathLabel')}
197
+ pathPlaceholder={t('chatProjectPathPlaceholder')}
198
+ confirmLabel={t('selectCurrentDirectory')}
199
+ hint={t('chatProjectPathHint')}
200
+ />
201
+ </>
202
+ );
203
+ }
@@ -32,7 +32,7 @@ describe('ChatSessionHeaderActions', () => {
32
32
  sessionKey="session-1"
33
33
  canDeleteSession
34
34
  isDeletePending={false}
35
- projectRoot="/tmp/project-alpha"
35
+ currentPath="/tmp/project-alpha"
36
36
  isWorkspaceOpen={false}
37
37
  onToggleWorkspace={mocks.onToggleWorkspace}
38
38
  onDeleteSession={mocks.onDeleteSession}
@@ -55,7 +55,7 @@ describe('ChatSessionHeaderActions', () => {
55
55
  sessionKey="draft-session"
56
56
  canDeleteSession={false}
57
57
  isDeletePending={false}
58
- projectRoot={null}
58
+ currentPath={null}
59
59
  isWorkspaceOpen={false}
60
60
  onToggleWorkspace={mocks.onToggleWorkspace}
61
61
  onDeleteSession={mocks.onDeleteSession}
@@ -77,7 +77,7 @@ describe('ChatSessionHeaderActions', () => {
77
77
  sessionKey="session-codex"
78
78
  canDeleteSession
79
79
  isDeletePending={false}
80
- projectRoot={null}
80
+ currentPath={null}
81
81
  metadata={{
82
82
  codex_thread_id: 'thread-123',
83
83
  runtime: 'codex',
@@ -106,7 +106,7 @@ describe('ChatSessionHeaderActions', () => {
106
106
  sessionKey="session-children"
107
107
  canDeleteSession
108
108
  isDeletePending={false}
109
- projectRoot={null}
109
+ currentPath={null}
110
110
  isWorkspaceOpen={false}
111
111
  onToggleWorkspace={mocks.onToggleWorkspace}
112
112
  onDeleteSession={mocks.onDeleteSession}
@@ -126,7 +126,7 @@ describe('ChatSessionHeaderActions', () => {
126
126
  sessionKey="session-actions"
127
127
  canDeleteSession
128
128
  isDeletePending={false}
129
- projectRoot={null}
129
+ currentPath={null}
130
130
  isWorkspaceOpen={false}
131
131
  onToggleWorkspace={mocks.onToggleWorkspace}
132
132
  onDeleteSession={mocks.onDeleteSession}
@@ -146,7 +146,7 @@ describe('ChatSessionHeaderActions', () => {
146
146
  sessionKey="session-actions"
147
147
  canDeleteSession
148
148
  isDeletePending={false}
149
- projectRoot={null}
149
+ currentPath={null}
150
150
  isWorkspaceOpen={false}
151
151
  onToggleWorkspace={mocks.onToggleWorkspace}
152
152
  onDeleteSession={mocks.onDeleteSession}
@@ -169,7 +169,7 @@ describe('ChatSessionHeaderActions', () => {
169
169
  sessionKey="session-actions"
170
170
  canDeleteSession
171
171
  isDeletePending={false}
172
- projectRoot={null}
172
+ currentPath={null}
173
173
  isWorkspaceOpen
174
174
  onToggleWorkspace={mocks.onToggleWorkspace}
175
175
  onDeleteSession={mocks.onDeleteSession}
@@ -0,0 +1,47 @@
1
+ import { render } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { ChatSessionProjectDialog } from "@/features/chat/features/session/components/session-header/chat-session-project-dialog";
4
+
5
+ const pathPickerSpy = vi.fn((_props: unknown) => null);
6
+
7
+ vi.mock("@/shared/components/path-picker/server-path-picker-dialog", () => ({
8
+ ServerPathPickerDialog: (props: unknown) => pathPickerSpy(props),
9
+ }));
10
+
11
+ describe("ChatSessionProjectDialog", () => {
12
+ it("uses the default workspace as the browse start and quick-access location", () => {
13
+ render(
14
+ <ChatSessionProjectDialog
15
+ open
16
+ currentProjectRoot={null}
17
+ defaultWorkspacePath="/tmp/default-workspace"
18
+ isSaving={false}
19
+ onOpenChange={() => undefined}
20
+ onSave={() => undefined}
21
+ />,
22
+ );
23
+
24
+ expect(pathPickerSpy).toHaveBeenCalledWith(expect.objectContaining({
25
+ currentPath: null,
26
+ defaultWorkspacePath: "/tmp/default-workspace",
27
+ }));
28
+ });
29
+
30
+ it("prefers an explicit project over the default workdir", () => {
31
+ render(
32
+ <ChatSessionProjectDialog
33
+ open
34
+ currentProjectRoot="/tmp/project"
35
+ defaultWorkspacePath="/tmp/default-workspace"
36
+ isSaving={false}
37
+ onOpenChange={() => undefined}
38
+ onSave={() => undefined}
39
+ />,
40
+ );
41
+
42
+ expect(pathPickerSpy).toHaveBeenLastCalledWith(expect.objectContaining({
43
+ currentPath: "/tmp/project",
44
+ defaultWorkspacePath: "/tmp/default-workspace",
45
+ }));
46
+ });
47
+ });
@@ -15,7 +15,8 @@ type ChatSessionHeaderActionsProps = {
15
15
  sessionKey: string;
16
16
  canDeleteSession: boolean;
17
17
  isDeletePending: boolean;
18
- projectRoot?: string | null;
18
+ currentPath?: string | null;
19
+ defaultWorkspacePath?: string | null;
19
20
  metadata?: Record<string, unknown> | null;
20
21
  isWorkspaceOpen: boolean;
21
22
  onToggleWorkspace: () => void;
@@ -26,7 +27,8 @@ export function ChatSessionHeaderActions({
26
27
  sessionKey,
27
28
  canDeleteSession,
28
29
  isDeletePending,
29
- projectRoot,
30
+ currentPath,
31
+ defaultWorkspacePath,
30
32
  metadata,
31
33
  isWorkspaceOpen,
32
34
  onToggleWorkspace,
@@ -119,7 +121,8 @@ export function ChatSessionHeaderActions({
119
121
 
120
122
  <ChatSessionProjectDialog
121
123
  open={isDialogOpen}
122
- currentProjectRoot={projectRoot}
124
+ currentProjectRoot={currentPath}
125
+ defaultWorkspacePath={defaultWorkspacePath}
123
126
  isSaving={isProjectPending}
124
127
  onOpenChange={setIsDialogOpen}
125
128
  onSave={runProjectUpdate}
@@ -15,6 +15,7 @@ type ChatSessionProjectBadgeProps = {
15
15
  sessionKey: string;
16
16
  projectName: string;
17
17
  projectRoot?: string | null;
18
+ defaultWorkspacePath?: string | null;
18
19
  persistToServer: boolean;
19
20
  };
20
21
 
@@ -26,6 +27,7 @@ export function ChatSessionProjectBadge({
26
27
  sessionKey,
27
28
  projectName,
28
29
  projectRoot,
30
+ defaultWorkspacePath,
29
31
  persistToServer,
30
32
  }: ChatSessionProjectBadgeProps) {
31
33
  const updateSessionProject = useChatSessionProject();
@@ -106,6 +108,7 @@ export function ChatSessionProjectBadge({
106
108
  <ChatSessionProjectDialog
107
109
  open={isDialogOpen}
108
110
  currentProjectRoot={projectRoot}
111
+ defaultWorkspacePath={defaultWorkspacePath}
109
112
  isSaving={isProjectPending}
110
113
  onOpenChange={setIsDialogOpen}
111
114
  onSave={runProjectUpdate}
@@ -4,6 +4,7 @@ import { t } from '@/shared/lib/i18n';
4
4
  type ChatSessionProjectDialogProps = {
5
5
  open: boolean;
6
6
  currentProjectRoot?: string | null;
7
+ defaultWorkspacePath?: string | null;
7
8
  isSaving: boolean;
8
9
  onOpenChange: (open: boolean) => void;
9
10
  onSave: (projectRoot: string) => Promise<void> | void;
@@ -12,6 +13,7 @@ type ChatSessionProjectDialogProps = {
12
13
  export function ChatSessionProjectDialog({
13
14
  open,
14
15
  currentProjectRoot,
16
+ defaultWorkspacePath,
15
17
  isSaving,
16
18
  onOpenChange,
17
19
  onSave,
@@ -20,6 +22,7 @@ export function ChatSessionProjectDialog({
20
22
  <ServerPathPickerDialog
21
23
  open={open}
22
24
  currentPath={currentProjectRoot}
25
+ defaultWorkspacePath={defaultWorkspacePath}
23
26
  isSaving={isSaving}
24
27
  onOpenChange={onOpenChange}
25
28
  onConfirm={onSave}
@@ -0,0 +1,245 @@
1
+ import { useId } from "react";
2
+ import { Check, ChevronDown, ChevronRight, Folder } from "lucide-react";
3
+ import type { NcpSessionListItemView } from "@/features/chat/features/ncp/hooks/use-ncp-session-list-view";
4
+ import { SessionContextIconNode } from "@/features/chat/features/session/components/session-context-icon";
5
+ import { SessionRunBadge } from "@/features/chat/features/session/components/session-run-badge";
6
+ import type {
7
+ ChatSidebarDateGroup,
8
+ ChatSidebarProjectGroup,
9
+ } from "@/features/chat/features/session/utils/chat-sidebar-session-groups.utils";
10
+ import {
11
+ formatSessionListTime,
12
+ sessionActivityPreviewText,
13
+ sessionDisplayName,
14
+ } from "@/features/chat/features/session/utils/chat-session-display.utils";
15
+ import { resolveSessionContextView } from "@/features/chat/features/session/utils/session-context.utils";
16
+ import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
17
+ import { shouldShowUnreadSessionIndicator } from "@/features/chat/stores/chat-session-list.store";
18
+ import { t } from "@/shared/lib/i18n";
19
+ import { cn } from "@/shared/lib/utils";
20
+
21
+ type SessionSwitchItemsProps = {
22
+ items: NcpSessionListItemView[];
23
+ selectedSessionKey: string | null;
24
+ optimisticReadAtBySessionKey: Record<string, string>;
25
+ sessionTypeOptions: ChatSessionTypeOption[];
26
+ onSelect: (sessionKey: string) => void;
27
+ className?: string;
28
+ };
29
+
30
+ function ChatSessionSwitchItem({
31
+ item,
32
+ selectedSessionKey,
33
+ optimisticReadAtBySessionKey,
34
+ sessionTypeOptions,
35
+ onSelect,
36
+ }: Omit<SessionSwitchItemsProps, "items" | "className"> & {
37
+ item: NcpSessionListItemView;
38
+ }) {
39
+ const { session, runStatus } = item;
40
+ const active = selectedSessionKey === session.key;
41
+ const optimisticReadAt = optimisticReadAtBySessionKey[session.key];
42
+ const effectiveReadAt =
43
+ optimisticReadAt && session.readAt
44
+ ? optimisticReadAt.localeCompare(session.readAt) > 0
45
+ ? optimisticReadAt
46
+ : session.readAt
47
+ : (optimisticReadAt ?? session.readAt);
48
+ const shouldShowUnread = shouldShowUnreadSessionIndicator({
49
+ active,
50
+ lastMessageAt: session.lastMessageAt,
51
+ readAt: effectiveReadAt,
52
+ runStatus,
53
+ });
54
+ const previewText =
55
+ sessionActivityPreviewText(session) ??
56
+ session.projectName ??
57
+ session.projectRoot ??
58
+ `${session.messageCount}`;
59
+ const trailingText = formatSessionListTime(
60
+ session.lastMessageAt ?? session.createdAt,
61
+ );
62
+ const sessionContext = resolveSessionContextView(session, sessionTypeOptions);
63
+
64
+ return (
65
+ <button
66
+ type="button"
67
+ aria-current={active ? "page" : undefined}
68
+ className={cn(
69
+ "flex w-full min-w-0 items-start gap-2 rounded-lg px-2.5 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
70
+ active
71
+ ? "bg-accent text-accent-foreground"
72
+ : "text-foreground hover:bg-accent/65 hover:text-accent-foreground",
73
+ )}
74
+ onClick={() => onSelect(session.key)}
75
+ >
76
+ <span className="relative mt-1 flex h-2.5 w-2.5 shrink-0 items-center justify-center">
77
+ {shouldShowUnread ? (
78
+ <span
79
+ aria-label={t("chatSidebarUnread")}
80
+ className="h-1.5 w-1.5 rounded-full bg-primary"
81
+ />
82
+ ) : active ? (
83
+ <Check className="h-3 w-3 text-primary" />
84
+ ) : (
85
+ <span className="h-1 w-1 rounded-full bg-muted-foreground/45" />
86
+ )}
87
+ </span>
88
+ <span className="min-w-0 flex-1">
89
+ <span className="flex min-w-0 items-center gap-2">
90
+ <span className="min-w-0 flex-1 truncate text-xs font-medium">
91
+ {sessionDisplayName(session)}
92
+ </span>
93
+ {sessionContext.label ? (
94
+ <span className="shrink-0 rounded-full border border-border/70 px-1.5 py-0.5 text-[10px] font-semibold leading-none text-muted-foreground">
95
+ {sessionContext.label}
96
+ </span>
97
+ ) : null}
98
+ {sessionContext.icon ? (
99
+ <span className="inline-flex h-[1.125rem] w-[1.125rem] shrink-0 items-center justify-center text-muted-foreground">
100
+ <SessionContextIconNode icon={sessionContext.icon} />
101
+ </span>
102
+ ) : null}
103
+ </span>
104
+ <span className="mt-0.5 flex min-w-0 items-center gap-2 text-[11px] text-muted-foreground">
105
+ <span className="min-w-0 truncate">{previewText}</span>
106
+ <span className="shrink-0">{trailingText}</span>
107
+ </span>
108
+ </span>
109
+ {runStatus ? (
110
+ <span className="mt-0.5 shrink-0">
111
+ <SessionRunBadge status={runStatus} />
112
+ </span>
113
+ ) : null}
114
+ </button>
115
+ );
116
+ }
117
+
118
+ function ChatSessionSwitchItems({
119
+ items,
120
+ selectedSessionKey,
121
+ optimisticReadAtBySessionKey,
122
+ sessionTypeOptions,
123
+ onSelect,
124
+ className,
125
+ }: SessionSwitchItemsProps) {
126
+ return (
127
+ <div className={cn("space-y-1", className)}>
128
+ {items.map((item) => (
129
+ <ChatSessionSwitchItem
130
+ key={item.session.key}
131
+ item={item}
132
+ selectedSessionKey={selectedSessionKey}
133
+ optimisticReadAtBySessionKey={optimisticReadAtBySessionKey}
134
+ sessionTypeOptions={sessionTypeOptions}
135
+ onSelect={onSelect}
136
+ />
137
+ ))}
138
+ </div>
139
+ );
140
+ }
141
+
142
+ function ChatSessionSwitchDateGroup({
143
+ group,
144
+ ...itemsProps
145
+ }: Omit<SessionSwitchItemsProps, "items"> & {
146
+ group: ChatSidebarDateGroup;
147
+ }) {
148
+ return (
149
+ <section aria-label={group.label}>
150
+ <div className="px-2 py-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70">
151
+ {group.label}
152
+ </div>
153
+ <ChatSessionSwitchItems items={group.items} {...itemsProps} />
154
+ </section>
155
+ );
156
+ }
157
+
158
+ function ChatSessionSwitchProjectGroup({
159
+ group,
160
+ isCollapsed,
161
+ onToggleCollapsed,
162
+ ...itemsProps
163
+ }: Omit<SessionSwitchItemsProps, "items"> & {
164
+ group: ChatSidebarProjectGroup;
165
+ isCollapsed: boolean;
166
+ onToggleCollapsed: (projectRoot: string) => void;
167
+ }) {
168
+ const contentId = useId();
169
+ const actionLabel = `${t(
170
+ isCollapsed ? "chatSidebarExpandProject" : "chatSidebarCollapseProject",
171
+ )} · ${group.projectName}`;
172
+
173
+ return (
174
+ <section aria-label={group.projectName}>
175
+ <button
176
+ type="button"
177
+ aria-controls={contentId}
178
+ aria-expanded={!isCollapsed}
179
+ aria-label={actionLabel}
180
+ className="group flex h-10 w-full min-w-0 items-center gap-1.5 rounded-lg px-2 text-left text-muted-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
181
+ onClick={() => onToggleCollapsed(group.projectRoot)}
182
+ >
183
+ <Folder className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
184
+ <span
185
+ className="min-w-0 flex-1 truncate text-[11px] font-medium uppercase tracking-wider"
186
+ title={group.projectRoot}
187
+ >
188
+ {group.projectName}
189
+ </span>
190
+ <span className="shrink-0 text-[10px] text-muted-foreground/70">
191
+ {group.items.length}
192
+ </span>
193
+ {isCollapsed ? (
194
+ <ChevronRight className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
195
+ ) : (
196
+ <ChevronDown className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
197
+ )}
198
+ </button>
199
+ {isCollapsed ? null : (
200
+ <ChatSessionSwitchItems
201
+ items={group.items}
202
+ className="mt-0.5"
203
+ {...itemsProps}
204
+ />
205
+ )}
206
+ </section>
207
+ );
208
+ }
209
+
210
+ export function ChatSessionSwitcherGroups({
211
+ collapsedProjectRoots,
212
+ dateGroups,
213
+ isProjectFirstView,
214
+ onToggleProjectCollapsed,
215
+ projectGroups,
216
+ ...itemsProps
217
+ }: Omit<SessionSwitchItemsProps, "items"> & {
218
+ collapsedProjectRoots: string[];
219
+ dateGroups: ChatSidebarDateGroup[];
220
+ isProjectFirstView: boolean;
221
+ onToggleProjectCollapsed: (projectRoot: string) => void;
222
+ projectGroups: ChatSidebarProjectGroup[];
223
+ }) {
224
+ return (
225
+ <div className="space-y-3">
226
+ {isProjectFirstView
227
+ ? projectGroups.map((group) => (
228
+ <ChatSessionSwitchProjectGroup
229
+ key={group.projectRoot}
230
+ group={group}
231
+ isCollapsed={collapsedProjectRoots.includes(group.projectRoot)}
232
+ onToggleCollapsed={onToggleProjectCollapsed}
233
+ {...itemsProps}
234
+ />
235
+ ))
236
+ : dateGroups.map((group) => (
237
+ <ChatSessionSwitchDateGroup
238
+ key={group.label}
239
+ group={group}
240
+ {...itemsProps}
241
+ />
242
+ ))}
243
+ </div>
244
+ );
245
+ }