From 8d9be01ef65c0a4dc19cae3de56556ea4111e5ea Mon Sep 17 00:00:00 2001 From: Master-Frank <1486493017@qq.com> Date: Mon, 12 Oct 2026 00:57:57 +0800 Subject: [PATCH] fix(ui): allow submitting the New project dialog without a name The New project dialog required a name before its submit button became usable, so a user who only wanted a folder-named project had to invent a title before the picker would even open. The catalog already falls back to the folder's basename when no name is given, so the dialog can simply treat the field as an optional override: submit is always available, an empty field submits as "no name", and the en/zh-CN/zh-TW copy now says so instead of implying the name comes first. Fixes #6047 Generated-by: OpenCode --- .../__tests__/project-add-restore.test.ts | 22 +++++++++++++ .../use-session-workspace-recovery.ts | 3 +- .../controller/use-task-entry-controller.ts | 5 +-- .../task-entry/ui/task-entry-host.tsx | 2 +- .../task-entry/ui/task-entry-provider.tsx | 2 +- .../settings/projects-settings-page.tsx | 15 +++++---- .../src/__tests__/new-project-dialog.test.tsx | 33 +++++++++++++++++-- packages/ui/src/conversation-copy.ts | 10 +++--- packages/ui/src/new-project-dialog.tsx | 26 +++++++-------- packages/ui/src/workspace-picker.tsx | 13 ++++---- 10 files changed, 91 insertions(+), 40 deletions(-) diff --git a/apps/desktop/src/main/__tests__/project-add-restore.test.ts b/apps/desktop/src/main/__tests__/project-add-restore.test.ts index 1ef2b9b0920..c8295a5259f 100644 --- a/apps/desktop/src/main/__tests__/project-add-restore.test.ts +++ b/apps/desktop/src/main/__tests__/project-add-restore.test.ts @@ -505,6 +505,28 @@ for (const invalidation of ['host', 'verification', 'unmount'] as const) { }); } +test('Settings submits a blank project name as no name', async () => { + const harness = installRenderer(); + const received: unknown[] = []; + bridge({ + add: async (_target: DesktopRuntimeHostRef, options?: { name?: string }) => { + received.push(options); + return { ok: true, project: restored }; + }, + }); + await harness.render(createElement(components.SettingsFixture)); + await click('New project'); + const form = document.querySelector('#maka-new-project-form'); + assert.ok(form, document.body.textContent); + // Submit without typing: the field is optional now, so the picker still opens. + await act(async () => { + form.dispatchEvent(new window.Event('submit', { bubbles: true, cancelable: true })); + }); + await flushFrames(); + assert.deepEqual(received, [undefined]); + assert.equal(document.querySelector('#maka-new-project-form'), null); +}); + test('remote registration rejects same-tick duplicate submissions without invalidating the first request', async () => { const harness = installRenderer(); const registration = deferred(); diff --git a/apps/desktop/src/renderer/features/task-entry/controller/use-session-workspace-recovery.ts b/apps/desktop/src/renderer/features/task-entry/controller/use-session-workspace-recovery.ts index 78d19e5208c..04f9a428e14 100644 --- a/apps/desktop/src/renderer/features/task-entry/controller/use-session-workspace-recovery.ts +++ b/apps/desktop/src/renderer/features/task-entry/controller/use-session-workspace-recovery.ts @@ -43,7 +43,8 @@ export interface AddSessionWorkspaceInput { sessionId: string; profileId: string; host: TaskEntryHostRef; - name: string; + /** The optional name from the New project dialog; absent means the folder name. */ + name?: string; request?: SessionWorkspaceRecoveryRequest; } diff --git a/apps/desktop/src/renderer/features/task-entry/controller/use-task-entry-controller.ts b/apps/desktop/src/renderer/features/task-entry/controller/use-task-entry-controller.ts index e5ac59c2881..a752e4298ae 100644 --- a/apps/desktop/src/renderer/features/task-entry/controller/use-task-entry-controller.ts +++ b/apps/desktop/src/renderer/features/task-entry/controller/use-task-entry-controller.ts @@ -521,7 +521,7 @@ export function useTaskEntryController( selectedProjectId: groupSelectedProjectId, onSelectProject: (projectId: string) => selectProject(host, projectId), ...(host.capabilities.chooseClientDirectory || host.capabilities.chooseHostDirectory - ? { onAdd: (name: string) => void addProjectForHost(host, name) } + ? { onAdd: (name?: string) => void addProjectForHost(host, name) } : {}), ...(host.capabilities.chooseClientDirectory ? { onRelink: (projectId: string) => void relinkProject(host, projectId) } @@ -685,7 +685,8 @@ export function useTaskEntryController( const openNewProject = useCallback(() => setNewProjectOpen(true), []); const newProjectDialog = useMemo(() => newProjectOpen ? { close: () => setNewProjectOpen(false), - submit: (name: string) => addSelectedProject(name), + // An empty submit is "no name": the folder's basename will be used. + submit: (name?: string) => addSelectedProject(name), } : undefined, [newProjectOpen, addSelectedProject]); const openFolder = useCallback(async ( folder: TaskEntryFolder, diff --git a/apps/desktop/src/renderer/features/task-entry/ui/task-entry-host.tsx b/apps/desktop/src/renderer/features/task-entry/ui/task-entry-host.tsx index 4db37a4cd90..aef2ef22a08 100644 --- a/apps/desktop/src/renderer/features/task-entry/ui/task-entry-host.tsx +++ b/apps/desktop/src/renderer/features/task-entry/ui/task-entry-host.tsx @@ -24,7 +24,7 @@ import type { TaskEntryHostRef } from '../ports.js'; import { useTaskEntryHostModel } from './task-entry-provider.js'; export interface TaskEntryHostModel { - newProjectDialog?: { close(): void; submit(name: string): void }; + newProjectDialog?: { close(): void; submit(name?: string): void }; directoryHost?: TaskEntryHostRef & { readonly name?: string }; directoryOpener?: HTMLElement | null; closeDirectoryPicker(): void; diff --git a/apps/desktop/src/renderer/features/task-entry/ui/task-entry-provider.tsx b/apps/desktop/src/renderer/features/task-entry/ui/task-entry-provider.tsx index d41a94aac4e..15e39e12010 100644 --- a/apps/desktop/src/renderer/features/task-entry/ui/task-entry-provider.tsx +++ b/apps/desktop/src/renderer/features/task-entry/ui/task-entry-provider.tsx @@ -427,7 +427,7 @@ export function TaskEntryWorkspacePickerConsumer({ }), onAdd: activeSession.profileKind === 'local' && activeGroup.hostId - ? (name: string) => void owner.commands.addSessionWorkspace({ + ? (name?: string) => void owner.commands.addSessionWorkspace({ sessionId: activeSession.id, profileId: activeSession.profileId, host: { profileId: activeGroup.id, hostId: activeGroup.hostId! }, diff --git a/apps/desktop/src/renderer/settings/projects-settings-page.tsx b/apps/desktop/src/renderer/settings/projects-settings-page.tsx index 7777e3384dc..b176a3c5b4f 100644 --- a/apps/desktop/src/renderer/settings/projects-settings-page.tsx +++ b/apps/desktop/src/renderer/settings/projects-settings-page.tsx @@ -108,15 +108,16 @@ export function ProjectsSettingsPage(props: { }, [host, mountedRef, props.runtimeHostTargetVerified]); /** - * Create a project from the name the New project dialog collected. The add - * call still owns the folder picker, so a cancelled picker simply leaves the - * list unchanged; the name rides along and the project is registered under it. + * Create a project from the optional name the New project dialog collected. + * The add call still owns the folder picker, so a cancelled picker simply + * leaves the list unchanged; a typed name rides along, and a blank one lets + * the catalog name the project after its folder. */ - async function addNamedProject(name: string, registration: ProjectRegistration) { + async function addNamedProject(name: string | undefined, registration: ProjectRegistration) { if (!host || !props.runtimeHostTargetVerified) return; await runRowAction('add', async () => { const result = await registration.register( - () => window.maka.projects.add(host, { name }), + () => window.maka.projects.add(host, name ? { name } : undefined), () => mountedRef.current, ); // Cancelled or invalidated registrations must not trigger a refresh. @@ -292,8 +293,8 @@ export function ProjectsSettingsPage(props: { if (props.runtimeHostTargetVerified) setProjectDialog('directory'); } : () => { - // The dialog names the project first; the folder picker it - // opens is the same one, just reached one step later. + // The dialog collects an optional name; the folder picker + // it opens is the same one, just reached one step later. if (props.runtimeHostTargetVerified) setProjectDialog('new'); }} /> diff --git a/packages/ui/src/__tests__/new-project-dialog.test.tsx b/packages/ui/src/__tests__/new-project-dialog.test.tsx index 5f5c8e66fc0..4ed592404a7 100644 --- a/packages/ui/src/__tests__/new-project-dialog.test.tsx +++ b/packages/ui/src/__tests__/new-project-dialog.test.tsx @@ -82,7 +82,7 @@ async function mountDialog() { installDomStubs(window); Object.assign(globalThis, { document, window, IS_REACT_ACT_ENVIRONMENT: true }); - const submitted: string[] = []; + const submitted: (string | undefined)[] = []; let openChanges = 0; const container = document.querySelector('#root'); assert.ok(container); @@ -135,7 +135,7 @@ test('a submit belongs to the dialog and never reaches the browser', async () => } }); -test('an empty name submits nothing at all', async () => { +test('an empty name submits as "no name" so the folder can still be chosen', async () => { const dialog = await mountDialog(); try { let event!: Event; @@ -144,7 +144,34 @@ test('an empty name submits nothing at all', async () => { }); assert.equal(event.defaultPrevented, true); - assert.deepEqual(dialog.submitted, []); + // The dialog asked to close and handed back no name — the caller opens + // the folder picker and the catalog uses the folder's basename. + assert.equal(dialog.openChanges(), 1); + assert.deepEqual(dialog.submitted, [undefined]); + } finally { + await dialog.dispose(); + } +}); + +test('a typed name is trimmed before it travels', async () => { + const dialog = await mountDialog(); + try { + const input = document.querySelector('input'); + assert.ok(input); + await act(() => { + input.value = ' Spaced '; + const propsKey = Object.keys(input).find((key) => key.startsWith('__reactProps$')); + assert.ok(propsKey); + const props = (input as unknown as Record)[propsKey] as { + onChange: (event: { target: HTMLInputElement; defaultPrevented: boolean }) => void; + }; + props.onChange({ target: input, defaultPrevented: false }); + }); + await act(() => { + dialog.submit(); + }); + + assert.deepEqual(dialog.submitted, ['Spaced']); } finally { await dialog.dispose(); } diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index 27764eb506a..c279edb2305 100644 --- a/packages/ui/src/conversation-copy.ts +++ b/packages/ui/src/conversation-copy.ts @@ -245,11 +245,11 @@ export interface ConversationCopy { workspace: { choose: string; current: string; - /** The picker's create action; the dialog that follows names the project. */ + /** The picker's create action; the dialog that follows optionally names the project. */ newProject: string; /** Header of the New project dialog. */ newProjectTitle: string; - /** One line under the header: name first, folder next. */ + /** One line under the header: the name is an optional override of the folder name. */ newProjectDescription: string; newProjectNameLabel: string; /** The dialog's submit, which opens the folder picker. */ @@ -513,7 +513,7 @@ const CONVERSATION_COPY = { forms: { keyboardHint: '1–9 选择 · ↑↓ 切换 · Enter 确认 · Esc 取消', requester: (name) => `由 ${name} 请求`, requesterWithSource: (name, source) => `由 ${name} 请求 · ${source}`, required: '必填', optional: '选填', include: (label) => `提供:${label}`, enabled: (label) => `启用:${label}`, enterValue: '输入内容', enterNumber: '输入数字', constraintSeparator: ';', lengthConstraint: (minimum, maximum) => minimum === undefined ? `最多 ${maximum} 个字符` : maximum === undefined ? `至少 ${minimum} 个字符` : `长度 ${minimum}–${maximum} 个字符`, numberConstraint: (minimum, maximum) => minimum === undefined ? `最大值 ${maximum}` : maximum === undefined ? `最小值 ${minimum}` : `范围 ${minimum}–${maximum}`, itemConstraint: (minimum, maximum) => minimum === undefined ? `最多选择 ${maximum} 项` : maximum === undefined ? `至少选择 ${minimum} 项` : `选择 ${minimum}–${maximum} 项`, formatConstraint: { email: '格式:email', uri: '格式:URI', date: '格式:date(YYYY-MM-DD)', 'date-time': '格式:date-time(RFC 3339)' }, invalid: '请提供符合要求的值。', cancel: '取消', decline: '拒绝', accept: '提交', submitting: '正在提交…' }, mentions: { noFiles: '未找到文件', noFilesOrSessions: '未找到文件或会话', noSkills: '暂无技能', noCommandsOrSkills: '没有匹配的命令或技能', filesAriaLabel: '工作区文件', filesAndSessionsAriaLabel: '工作区文件和会话', sessionsGroup: '会话', skillsAriaLabel: '技能', commandsAndSkillsAriaLabel: '命令和技能', commandsGroup: '命令', skillsGroup: 'Skills', loading: '加载中…', sessionReferenceUnavailableTitle: '无法引用会话', sessionReferenceUnavailableDetail: '该会话已不可用,请刷新任务列表后重试。', sessionReferenceEmptyTitle: '会话没有可引用内容', sessionReferenceEmptyDetail: '只会引用用户和助手文本;工具调用等内部记录不会注入当前任务。', sessionReferenceReadFailedTitle: '读取会话失败', sessionReferenceReadFailedDetail: '无法读取该会话的快照,请稍后重试。', sessionReferenceLimitDetail: '引用总数最多为 16,请移除部分引用后重试。' }, workspace: { - choose: '选择项目', current: '当前项目', newProject: '新建项目', newProjectTitle: '新建项目', newProjectDescription: '先给项目起个名字,再选择它所在的文件夹。', newProjectNameLabel: '项目名称', newProjectSubmit: '选择文件夹', manageProjects: '管理项目', noProject: '无项目', relink: '重新定位', unavailable: '不可用', + choose: '选择项目', current: '当前项目', newProject: '新建项目', newProjectTitle: '新建项目', newProjectDescription: '可以给项目起个名字,也可以留空——之后会使用所选文件夹的名称。', newProjectNameLabel: '项目名称(可选)', newProjectSubmit: '选择文件夹', manageProjects: '管理项目', noProject: '无项目', relink: '重新定位', unavailable: '不可用', chooseTitle: (branch) => branch ? `选择项目 · ${branch}` : '选择项目', chooseAriaLabel: (label, branch) => branch ? `选择项目:${label},当前分支 ${branch}` : `选择项目:${label}`, }, @@ -637,7 +637,7 @@ const CONVERSATION_COPY = { forms: { keyboardHint: '1–9 選擇 · ↑↓ 切換 · Enter 確認 · Esc 取消', requester: (name) => `由 ${name} 請求`, requesterWithSource: (name, source) => `由 ${name} 請求 · ${source}`, required: '必填', optional: '選填', include: (label) => `提供:${label}`, enabled: (label) => `啟用:${label}`, enterValue: '輸入內容', enterNumber: '輸入數字', constraintSeparator: ';', lengthConstraint: (minimum, maximum) => minimum === undefined ? `最多 ${maximum} 個字元` : maximum === undefined ? `至少 ${minimum} 個字元` : `長度 ${minimum}–${maximum} 個字元`, numberConstraint: (minimum, maximum) => minimum === undefined ? `最大值 ${maximum}` : maximum === undefined ? `最小值 ${minimum}` : `範圍 ${minimum}–${maximum}`, itemConstraint: (minimum, maximum) => minimum === undefined ? `最多選取 ${maximum} 項` : maximum === undefined ? `至少選取 ${minimum} 項` : `選取 ${minimum}–${maximum} 項`, formatConstraint: { email: '格式:email', uri: '格式:URI', date: '格式:date(YYYY-MM-DD)', 'date-time': '格式:date-time(RFC 3339)' }, invalid: '請提供符合要求的值。', cancel: '取消', decline: '拒絕', accept: '提交', submitting: '正在提交…' }, mentions: { noFiles: '未找到檔案', noFilesOrSessions: '未找到檔案或作業階段', noSkills: '暫無技能', noCommandsOrSkills: '沒有符合的命令或技能', filesAriaLabel: '工作區檔案', filesAndSessionsAriaLabel: '工作區檔案和作業階段', sessionsGroup: '作業階段', skillsAriaLabel: '技能', commandsAndSkillsAriaLabel: '命令和技能', commandsGroup: '命令', skillsGroup: 'Skills', loading: '載入中…', sessionReferenceUnavailableTitle: '無法引用作業階段', sessionReferenceUnavailableDetail: '這個作業階段已無法使用,請重新整理任務清單後再試一次。', sessionReferenceEmptyTitle: '沒有可引用的內容', sessionReferenceEmptyDetail: '只會引用使用者和助理的文字;工具呼叫等內部記錄不會注入目前任務。', sessionReferenceReadFailedTitle: '讀取作業階段失敗', sessionReferenceReadFailedDetail: '無法讀取這個作業階段的快照,請稍後再試。', sessionReferenceLimitDetail: '引用總數最多為 16,請移除部分引用後再試。' }, workspace: { - choose: '選擇專案', current: '目前專案', newProject: '新增專案', newProjectTitle: '新增專案', newProjectDescription: '先為專案命名,再選擇它所在的資料夾。', newProjectNameLabel: '專案名稱', newProjectSubmit: '選擇資料夾', manageProjects: '管理專案', noProject: '無專案', relink: '重新定位', unavailable: '不可用', + choose: '選擇專案', current: '目前專案', newProject: '新增專案', newProjectTitle: '新增專案', newProjectDescription: '可以為專案命名,也可以留空——之後會使用所選資料夾的名稱。', newProjectNameLabel: '專案名稱(可選)', newProjectSubmit: '選擇資料夾', manageProjects: '管理專案', noProject: '無專案', relink: '重新定位', unavailable: '不可用', chooseTitle: (branch) => branch ? `選擇專案 · ${branch}` : '選擇專案', chooseAriaLabel: (label, branch) => branch ? `選擇專案:${label},目前分支 ${branch}` : `選擇專案:${label}`, }, @@ -758,7 +758,7 @@ const CONVERSATION_COPY = { forms: { keyboardHint: '1–9 select · ↑↓ navigate · Enter confirm · Esc cancel', requester: (name) => `Requested by ${name}`, requesterWithSource: (name, source) => `Requested by ${name} · ${source}`, required: 'Required', optional: 'Optional', include: (label) => `Provide ${label}`, enabled: (label) => `Enable ${label}`, enterValue: 'Enter a value', enterNumber: 'Enter a number', constraintSeparator: ' · ', lengthConstraint: (minimum, maximum) => minimum === undefined ? `At most ${maximum} characters` : maximum === undefined ? `At least ${minimum} characters` : `${minimum}–${maximum} characters`, numberConstraint: (minimum, maximum) => minimum === undefined ? `Maximum ${maximum}` : maximum === undefined ? `Minimum ${minimum}` : `Range ${minimum}–${maximum}`, itemConstraint: (minimum, maximum) => minimum === undefined ? `Select at most ${maximum}` : maximum === undefined ? `Select at least ${minimum}` : `Select ${minimum}–${maximum}`, formatConstraint: { email: 'Format: email', uri: 'Format: URI', date: 'Format: date (YYYY-MM-DD)', 'date-time': 'Format: date-time (RFC 3339)' }, invalid: 'Provide a value that meets the requirements.', cancel: 'Cancel', decline: 'Decline', accept: 'Submit', submitting: 'Submitting…' }, mentions: { noFiles: 'No files found', noFilesOrSessions: 'No files or sessions found', noSkills: 'No skills available', noCommandsOrSkills: 'No matching commands or skills', filesAriaLabel: 'Workspace files', filesAndSessionsAriaLabel: 'Workspace files and sessions', sessionsGroup: 'Sessions', skillsAriaLabel: 'Skills', commandsAndSkillsAriaLabel: 'Commands and skills', commandsGroup: 'Commands', skillsGroup: 'Skills', loading: 'Loading…', sessionReferenceUnavailableTitle: 'Session reference unavailable', sessionReferenceUnavailableDetail: 'This session is no longer available. Refresh the task list and try again.', sessionReferenceEmptyTitle: 'No referenceable content', sessionReferenceEmptyDetail: 'Only user and assistant text is shared; tool calls and internal records stay out of the current task.', sessionReferenceReadFailedTitle: 'Could not read session', sessionReferenceReadFailedDetail: 'The session snapshot could not be read. Try again later.', sessionReferenceLimitDetail: 'A message can contain at most 16 quotes. Remove a quote and try again.' }, workspace: { - choose: 'Choose project', current: 'Current project', newProject: 'New project', newProjectTitle: 'New project', newProjectDescription: 'Name the project, then choose the folder it lives in.', newProjectNameLabel: 'Project name', newProjectSubmit: 'Choose folder', manageProjects: 'Manage projects', noProject: 'No project', relink: 'Relink', unavailable: 'Unavailable', + choose: 'Choose project', current: 'Current project', newProject: 'New project', newProjectTitle: 'New project', newProjectDescription: 'Name the project, or leave it blank to use the folder name.', newProjectNameLabel: 'Project name (optional)', newProjectSubmit: 'Choose folder', manageProjects: 'Manage projects', noProject: 'No project', relink: 'Relink', unavailable: 'Unavailable', chooseTitle: (branch) => branch ? `Choose project · ${branch}` : 'Choose project', chooseAriaLabel: (label, branch) => branch ? `Choose project: ${label}, current branch ${branch}` : `Choose project: ${label}`, }, diff --git a/packages/ui/src/new-project-dialog.tsx b/packages/ui/src/new-project-dialog.tsx index b206cf6155d..e657b61fec6 100644 --- a/packages/ui/src/new-project-dialog.tsx +++ b/packages/ui/src/new-project-dialog.tsx @@ -25,22 +25,22 @@ import { getConversationCopy } from './conversation-copy.js'; import { useUiLocale } from './locale-context.js'; /** - * Naming a project before its folder is chosen. + * Optionally naming a project before its folder is chosen. * * A project is directory-backed, so this dialog cannot create one on its own — * it collects the one thing the folder picker cannot (a name) and hands it to - * the caller, which then asks for the directory. The name therefore leads: by - * the time the picker opens, the user has already said what this is, and the - * project is registered under it rather than under the folder's basename. + * the caller, which then asks for the directory. The name is an override, not + * a requirement: leaving it blank means "call this project after the folder I + * pick", and the catalog's own basename fallback does the naming. * * One button, no 取消: the header's close control and Escape are already two - * ways out, the convention `SessionRenameDialog` set. The submit is disabled - * while the field is empty, so there is nothing to validate on the way through. + * ways out, the convention `SessionRenameDialog` set. There is nothing to + * validate on the way through — an empty field is a valid choice. */ export function NewProjectDialog(props: { onOpenChange(open: boolean): void; - /** The typed name, trimmed and non-empty; the caller opens the folder picker. */ - onSubmit(name: string): void; + /** The typed name, trimmed; absent when the caller should use the folder name. */ + onSubmit(name?: string): void; }) { const copy = getConversationCopy(useUiLocale()).workspace; const [name, setName] = useState(''); @@ -67,11 +67,10 @@ export function NewProjectDialog(props: { function submit(event: FormEvent) { event.preventDefault(); props.onOpenChange(false); - // Escape and the header's close control are the two ways out; an empty field - // cannot reach here, because the only submit affordance is disabled while it - // is empty. The guard stands for the browsers that run implicit submission - // through a disabled default button anyway. - if (trimmed) props.onSubmit(trimmed); + // Escape and the header's close control are the two ways out. An empty + // field submits as "no name" — the caller's basename fallback does the + // naming — so nothing is blocked on the way through. + props.onSubmit(trimmed || undefined); } return ( @@ -87,7 +86,6 @@ export function NewProjectDialog(props: { variant="primary" type="submit" form="maka-new-project-form" - isDisabled={!trimmed} label={copy.newProjectSubmit} /> diff --git a/packages/ui/src/workspace-picker.tsx b/packages/ui/src/workspace-picker.tsx index 90329339727..b4f9065e5e0 100644 --- a/packages/ui/src/workspace-picker.tsx +++ b/packages/ui/src/workspace-picker.tsx @@ -38,11 +38,12 @@ export interface WorkspacePickerGroup { selectedProjectId?: string | null; onSelectProject?(projectId: string): void; /** - * Create a project on this host. The picker collects the name first (see + * Create a project on this host. The dialog collects an optional name (see * `NewProjectDialog`) and passes it here; the handler then asks for the - * directory and registers the project under that name. + * directory and registers the project under that name, or under the folder + * name when none was given. */ - onAdd?(name: string): void; + onAdd?(name?: string): void; onManage?(): void; onRelink?(projectId: string): void; onSelectNoProject?(): void; @@ -68,9 +69,9 @@ export function WorkspacePicker({ workspacePicker: picker }: { }) { const copy = getConversationCopy(useUiLocale()).workspace; const locked = picker.pending === true; - // Which host's New project dialog is open. The picker names the project, then - // the caller's `onAdd` opens the folder picker — so the name is collected - // before the directory step, not after it. + // Which host's New project dialog is open. The dialog optionally names the + // project, then the caller's `onAdd` opens the folder picker — a blank name + // simply means the folder's name will be used. const [newProjectGroupId, setNewProjectGroupId] = useState(null); const newProjectGroup = picker.groups.find((group) => group.id === newProjectGroupId);