@1yefuwang1/dsh-worktrees 0.2.13 → 0.2.18

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.
package/client.js CHANGED
@@ -9,19 +9,22 @@ window.__ModuleLoader__.load({
9
9
  const h = React.createElement;
10
10
  const NS = 'worktrees.ui';
11
11
  const PANEL = 'dsh-worktrees';
12
+ const VIEW_SWITCH = PANEL + '-projects-view';
12
13
  const en = {
13
14
  searchProjects: 'Search projects and sessions', closeSearch: 'Close search', viewOptions: 'View options',
15
+ switchProjectsView: 'Switch to Projects View', switchWorkspaceView: 'Switch to Workspace View',
14
16
  'time.now': 'now', 'time.minutes': '{n}min', 'time.hours': '{n}h', 'time.days': '{n}d', 'time.months': '{n}mo', 'time.years': '{n}y', 'date.ymd': '{y}-{m}-{d}', lastActive: 'Last active {time}',
15
- projects: 'Projects', folderView: 'Folder view', newProject: 'New project', editProject: 'Manage project', projectName: 'Project name', project: 'Project', folders: 'Folders', folder: 'Folder', addFolder: 'Add folder', chooseDirectory: 'Choose directory', browse: 'Browse folders', absolutePath: 'Absolute Host directory path', save: 'Save', newThread: 'New thread', threadMode: 'Thread mode', threadActions: 'Thread actions', threadDetails: 'Thread details', unassigned: 'Other threads', showMore: 'Show {n} more sessions', showLess: 'Show less', filterThreads: 'Search threads or projects', allThreads: 'All threads', archivedOnly: 'Archived only', hideArchived: 'Hide archived', untitled: 'Untitled', blankThread: 'New thread', collapse: 'Collapse project', expand: 'Expand project', waiting: 'Waiting for interaction', done: 'Finished, not yet viewed', localThread: 'Local thread', worktreeThread: 'Worktree thread', draftWarning: 'This draft contains text or attachments. Keep it in this thread before choosing another project or folder.', chooseFolder: 'Choose a folder', createFolder: 'Create folder', folderName: 'Folder name', chooseThisFolder: 'Use this folder', noProjects: 'Add a project to organize your threads.',
17
+ projects: 'Projects', folderView: 'Folder view', newProject: 'New project', editProject: 'Manage project', projectName: 'Project name', project: 'Project', folders: 'Folders', folder: 'Folder', addFolder: 'Add folder', chooseDirectory: 'Choose a folder…', browse: 'Browse folders', absolutePath: 'Absolute Host directory path', save: 'Save', newThread: 'New thread', threadMode: 'Thread mode', threadActions: 'Thread actions', threadDetails: 'Thread details', unassigned: 'Other threads', showMore: 'Show {n} more sessions', showLess: 'Show less', filterThreads: 'Search threads or projects', allThreads: 'All threads', archivedOnly: 'Archived only', hideArchived: 'Hide archived', untitled: 'Untitled', blankThread: 'New thread', collapse: 'Collapse project', expand: 'Expand project', waiting: 'Waiting for interaction', done: 'Finished, not yet viewed', localThread: 'Local thread', worktreeThread: 'Worktree thread', draftWarning: 'This draft contains text or attachments. Keep it in this thread before choosing another project or folder.', chooseFolder: 'Choose a folder', createFolder: 'Create folder', folderName: 'Folder name', chooseThisFolder: 'Use this folder', noProjects: 'Add a project to organize your threads.',
16
18
  repoUnavailable: 'Worktree mode is unavailable or unverified for this folder. Choose Local to continue.',
17
19
  projectChanged: 'The project or selected folder changed. Choose Local or keep the current folder before selecting New worktree again.', keepCurrentFolder: 'Keep current folder',
18
20
  removeProject: 'Remove project', removeProjectConfirm: 'Remove this project?', removeProjectHelp: 'Only the project metadata will be removed. All folders, files, conversations and worktrees will be kept. Conversations remain available under Other threads and in Folder view.',
19
21
  mainFolder: 'Main folder', main: 'Main', chooseMainFolder: 'Select the main folder', mainFolderRequired: 'Choose a main folder from the project source folders.', mainFolderHelp: 'Default for new conversations. You can choose another folder before sending.', conversationFolder: 'Conversation folder',
20
- createProject: 'Create project', sourceFolders: 'Source folders', localFolders: 'Add a folder on this computer', enterFolderPath: 'Enter a folder path', add: 'Add', cancel: 'Cancel', selectedFolders: 'Selected folders', selectFolder: 'Select folder', folderLimit: 'A project can contain at most 32 folders.', invalidFolder: 'Enter a valid absolute Host folder path.', alreadyAdded: 'Already added', multiFolderHelp: 'Select multiple folders using the checkboxes. Selections are kept as you browse.', hostFolderHelp: 'Folders are on the Host computer; adding them does not move files.', noSubfolders: 'No subfolders.', directoryTruncated: 'This listing is truncated. Enter a path to browse other folders.',
22
+ createProject: 'Create project', sourceFolders: 'Source folders', localFolders: 'Browse for folders', enterFolderPath: 'Enter a folder path', checkingFolderOptions: 'Checking available folder options…', browseUnavailable: 'This session cannot list Host folders. Use Choose a folder… or enter an absolute path.', folderPickerUnavailable: 'The native folder chooser is unavailable here. Browse folders or enter an absolute Host folder path instead.', add: 'Add', cancel: 'Cancel', selectedFolders: 'Selected folders', selectFolder: 'Select folder', folderLimit: 'A project can contain at most 32 folders.', invalidFolder: 'Enter a valid absolute Host folder path.', alreadyAdded: 'Already added', multiFolderHelp: 'Select multiple folders using the checkboxes. Selections are kept as you browse.', hostFolderHelp: 'Folders are on the Host computer; adding them does not move files.', noSubfolders: 'No subfolders.', directoryTruncated: 'This listing is truncated. Enter a path to browse other folders.',
21
23
  rootFolder: 'Worktree root folder', rootSummary: 'Choose where new worktrees are created.', rootHelp: 'Absolute folder path on the Host, outside your source repositories. Changes apply to new worktrees only; existing directories and in-flight creates are not moved.', rootReset: 'Reset to inherited default', rootSaved: 'Saved. New worktrees will use this location.', rootUnavailable: 'Host-backed settings are unavailable for this entry.', rootInvalid: 'Enter a valid absolute Host folder path.', rootConflict: 'The setting changed or the Host refused the write. Your draft is preserved; reopen this page to read the latest value.',
22
- pinned: 'Pinned', title: 'Worktrees', local: 'Local', new: 'New worktree', remote: 'Remote', branch: 'Remote base branch',
24
+ pinned: 'Pinned', title: 'Worktrees', local: 'Local', new: 'New worktree', remote: 'Remote', branch: 'Starting branch',
25
+ localBranches: 'Local branches', remoteBranches: 'Remote branches', currentBranch: 'Current branch', localBranchHelp: 'Start from a local commit. No remote fetch.', remoteBranchHelp: 'Start from the latest remote commit. Fetches when the worktree is created.', noBranches: 'No matching branches.', noRemotes: 'No remotes configured.', branchSource: 'Branch source',
23
26
  setupProgress: 'Preparing worktree', preparingMessage: 'Preparing your first message…', fetchingLatest: 'Fetching latest remote branch…', creatingWorktree: 'Creating worktree…', namingBranch: 'Generating and applying branch name…', openingConversation: 'Opening worktree conversation…', setupReady: 'Worktree ready', stagingAttachments: 'Preparing attachments for this worktree…', startingConversation: 'Starting normal conversation…', setupFailed: 'Worktree preparation failed', cancelSetup: 'Cancel setup', unsupported: 'This native composer version is not supported by the guarded adapter.', filesLost: 'A draft attachment is no longer available.', upload: 'Attachment upload failed', uncertain: 'Prompt admission is uncertain. Open the created conversation before trying again.',
24
- search: 'Search remote branches', select: 'Base branch', refresh: 'Refresh', more: 'Load more', close: 'Close', open: 'Open conversation', remove: 'Remove', creatingWorktree: 'Creating worktree…',
27
+ search: 'Search branches', select: 'Choose branch', refresh: 'Refresh', more: 'Load more', close: 'Close', open: 'Open conversation', remove: 'Remove', creatingWorktree: 'Creating worktree…',
25
28
  createBranch: 'Create branch here', branchName: 'New branch name', protect: 'Protect', unprotect: 'Unprotect',
26
29
  archive: 'Archive (retain files)', restore: 'Restore', archived: 'Archived', includeArchived: 'Include archived',
27
30
  preview: 'Preview handoff', export: 'Export patch', apply: 'Apply to Local and continue',
@@ -45,17 +48,19 @@ window.__ModuleLoader__.load({
45
48
  };
46
49
  const zh = { ...en,
47
50
  searchProjects: '搜索项目和会话', closeSearch: '关闭搜索', viewOptions: '视图选项',
51
+ switchProjectsView: '切换到项目视图', switchWorkspaceView: '切换到工作区视图',
48
52
  'time.now': '刚刚', 'time.minutes': '{n}分钟', 'time.hours': '{n}小时', 'time.days': '{n}天', 'time.months': '{n}个月', 'time.years': '{n}年', 'date.ymd': '{y}年{m}月{d}日', lastActive: '最后活跃于 {time}',
49
- projects: '项目', folderView: '文件夹视图', newProject: '新建项目', editProject: '管理项目', projectName: '项目名称', project: '项目', folders: '文件夹', folder: '文件夹', addFolder: '添加文件夹', chooseDirectory: '选择目录', browse: '浏览文件夹', absolutePath: '主机绝对目录路径', save: '保存', newThread: '新建对话', threadMode: '对话模式', threadActions: '对话操作', threadDetails: '对话详情', unassigned: '其他对话', showMore: '展开其余 {n} 个会话', showLess: '收起', filterThreads: '搜索对话或项目', allThreads: '所有对话', archivedOnly: '仅已归档', hideArchived: '隐藏已归档', untitled: '未命名', blankThread: '新对话', collapse: '收起项目', expand: '展开项目', waiting: '等待交互', done: '已完成,尚未查看', localThread: '本地对话', worktreeThread: '工作树对话', draftWarning: '此草稿包含文字或附件。请先保留当前对话,再选择其他项目或文件夹。', chooseFolder: '选择文件夹', createFolder: '创建文件夹', folderName: '文件夹名称', chooseThisFolder: '使用此文件夹', noProjects: '添加项目以组织您的对话。',
53
+ projects: '项目', folderView: '文件夹视图', newProject: '新建项目', editProject: '管理项目', projectName: '项目名称', project: '项目', folders: '文件夹', folder: '文件夹', addFolder: '添加文件夹', chooseDirectory: '选择文件夹…', browse: '浏览文件夹', absolutePath: '主机绝对目录路径', save: '保存', newThread: '新建对话', threadMode: '对话模式', threadActions: '对话操作', threadDetails: '对话详情', unassigned: '其他对话', showMore: '展开其余 {n} 个会话', showLess: '收起', filterThreads: '搜索对话或项目', allThreads: '所有对话', archivedOnly: '仅已归档', hideArchived: '隐藏已归档', untitled: '未命名', blankThread: '新对话', collapse: '收起项目', expand: '展开项目', waiting: '等待交互', done: '已完成,尚未查看', localThread: '本地对话', worktreeThread: '工作树对话', draftWarning: '此草稿包含文字或附件。请先保留当前对话,再选择其他项目或文件夹。', chooseFolder: '选择文件夹', createFolder: '创建文件夹', folderName: '文件夹名称', chooseThisFolder: '使用此文件夹', noProjects: '添加项目以组织您的对话。',
50
54
  repoUnavailable: '此文件夹的工作树模式不可用或尚未验证,请选择本地模式继续。',
51
55
  projectChanged: '项目或选定文件夹已更改,请先选择本地模式或保留当前文件夹,再重新选择新建工作树。', keepCurrentFolder: '保留当前文件夹',
52
56
  removeProject: '移除项目', removeProjectConfirm: '移除此项目?', removeProjectHelp: '仅移除项目元数据,所有文件夹、文件、对话和工作树都会保留。对话仍可在“其他对话”和“文件夹视图”中访问。',
53
57
  mainFolder: '主文件夹', main: '主目录', chooseMainFolder: '选择主文件夹', mainFolderRequired: '请从项目源文件夹中选择一个主文件夹。', mainFolderHelp: '新对话默认使用此文件夹,发送前仍可选择其他文件夹。', conversationFolder: '对话文件夹',
54
- createProject: '创建项目', sourceFolders: '源文件夹', localFolders: '添加此计算机上的文件夹', enterFolderPath: '输入文件夹路径', add: '添加', cancel: '取消', selectedFolders: '已选文件夹', selectFolder: '选择文件夹', folderLimit: '一个项目最多可包含 32 个文件夹。', invalidFolder: '请输入有效的主机绝对文件夹路径。', alreadyAdded: '已添加', multiFolderHelp: '使用复选框选择多个文件夹,浏览其他目录时会保留已选项。', hostFolderHelp: '文件夹位于主机计算机上;添加不会移动文件。', noSubfolders: '没有子文件夹。', directoryTruncated: '目录列表已截断,请输入路径浏览其他文件夹。',
58
+ createProject: '创建项目', sourceFolders: '源文件夹', localFolders: '浏览文件夹', enterFolderPath: '输入文件夹路径', checkingFolderOptions: '正在检查可用的文件夹选项…', browseUnavailable: '此会话无法列出主机文件夹。请使用“选择文件夹…”或输入绝对路径。', folderPickerUnavailable: '此处无法使用系统文件夹选择器。请改用浏览文件夹或输入主机绝对文件夹路径。', add: '添加', cancel: '取消', selectedFolders: '已选文件夹', selectFolder: '选择文件夹', folderLimit: '一个项目最多可包含 32 个文件夹。', invalidFolder: '请输入有效的主机绝对文件夹路径。', alreadyAdded: '已添加', multiFolderHelp: '使用复选框选择多个文件夹,浏览其他目录时会保留已选项。', hostFolderHelp: '文件夹位于主机计算机上;添加不会移动文件。', noSubfolders: '没有子文件夹。', directoryTruncated: '目录列表已截断,请输入路径浏览其他文件夹。',
55
59
  rootFolder: '工作树根目录', rootSummary: '选择新工作树的创建位置。', rootHelp: '主机上的绝对目录路径,需位于源仓库之外。仅影响新工作树,已有目录和进行中的创建不会移动。', rootReset: '恢复继承的默认值', rootSaved: '已保存,新工作树将使用此位置。', rootUnavailable: '此条目的主机设置不可用。', rootInvalid: '请输入有效的主机绝对目录路径。', rootConflict: '设置已更改或主机拒绝了保存,草稿已保留。请重新打开页面读取最新值。',
56
- pinned: '已固定', title: '工作树', local: '本地', new: '新工作树', remote: '远程', branch: '远程基础分支', search: '搜索远程分支',
60
+ pinned: '已固定', title: '工作树', local: '本地', new: '新工作树', remote: '远程', branch: '起始分支', search: '搜索分支',
61
+ localBranches: '本地分支', remoteBranches: '远程分支', currentBranch: '当前分支', localBranchHelp: '从本地提交开始,不获取远程更新。', remoteBranchHelp: '从最新远程提交开始,创建工作树时获取更新。', noBranches: '没有匹配的分支。', noRemotes: '未配置远程仓库。', branchSource: '分支来源',
57
62
  setupProgress: '正在准备工作树', preparingMessage: '正在准备第一条消息…', fetchingLatest: '正在获取最新远程分支…', creatingWorktree: '正在创建工作树…', namingBranch: '正在生成并应用分支名称…', openingConversation: '正在打开工作树对话…', setupReady: '工作树已就绪', stagingAttachments: '正在为工作树准备附件…', startingConversation: '正在开始正常对话…', setupFailed: '工作树准备失败', cancelSetup: '取消准备', unsupported: '该输入区版本不受受保护适配器支持。', filesLost: '草稿附件已不可用。', upload: '附件上传失败', uncertain: '消息发送结果不确定,请先打开创建的对话检查。',
58
- select: '基础分支', refresh: '刷新', more: '加载更多', close: '关闭', open: '打开对话', remove: '移除', creatingWorktree: '正在创建工作树…', createBranch: '在此创建分支', branchName: '新分支名称',
63
+ select: '选择分支', refresh: '刷新', more: '加载更多', close: '关闭', open: '打开对话', remove: '移除', creatingWorktree: '正在创建工作树…', createBranch: '在此创建分支', branchName: '新分支名称',
59
64
  protect: '保护', unprotect: '取消保护', archive: '归档(保留文件)', restore: '恢复', archived: '已归档',
60
65
  includeArchived: '包含已归档', preview: '预览交接', export: '导出补丁', apply: '应用到本地并继续',
61
66
  previewTitle: '审阅本地交接', original: '原始本地检出', target: '本地目标路径',
@@ -100,7 +105,7 @@ window.__ModuleLoader__.load({
100
105
  .dsh-wt-project-dialog .dsh-wt-main-folder{display:flex;flex-direction:column;align-items:stretch;gap:6px;margin-top:12px;font-size:13px}.dsh-wt-main-folder select{width:100%;min-width:0;border-color:var(--dsw-alias-border-l2)}.dsh-wt-main-folder small{color:var(--dsw-alias-label-secondary);font-size:12px}.dsh-wt-create-controls .dsh-wt-conversation-folder{max-width:180px}
101
106
  .dsh-wt-folder-picker{width:min(640px,calc(100vw - 32px))}.dsh-wt-picker-path{display:flex;gap:8px;align-items:flex-end}.dsh-wt-picker-path label{flex:1;min-width:0}.dsh-wt-picker-crumbs{margin:12px 0;gap:4px}.dsh-wt-picker-entries{list-style:none;padding:0;margin:8px 0;max-height:240px;overflow:auto}.dsh-wt-picker-entries li,.dsh-wt-picker-current{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:8px}.dsh-wt-picker-entries li:hover{background:var(--dsw-alias-bg-layer-2)}.dsh-wt-picker-check{flex-shrink:0}.dsh-wt-picker-check input{accent-color:var(--dsw-alias-brand-primary)}.dsh-wt-picker-entries .dsh-wt-picker-open{border:0;flex:1;min-width:0;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsh-wt-picker-current{border:1px solid var(--dsw-alias-border-l1)}.dsh-wt-picker-selected{margin-top:12px}.dsh-wt-picker-selected .dsh-wt-source-list{max-height:128px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px}.dsh-wt-picker-selected h3{font-size:13px}.dsh-wt-picker-create{margin-top:12px}.dsh-wt-picker-create summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:13px}.dsh-wt-picker-create form{display:flex;align-items:flex-end;gap:8px;margin-top:8px}
102
107
  .dsh-wt-progress{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);font-size:12px}.dsh-wt-progress ol{margin:8px 0;padding-left:18px;display:flex;flex-direction:column;gap:5px}.dsh-wt-progress li{color:var(--dsw-alias-label-secondary)}.dsh-wt-progress li[data-active=true]{font-weight:600;color:var(--dsw-alias-label-primary)}
103
- .dsh-wt-create-controls{position:relative;font-size:12px}.dsh-wt-create-controls select{max-width:160px;min-height:28px}.dsh-wt-base-options{position:absolute;bottom:calc(100% + 8px);left:0;z-index:20;min-width:240px;max-width:360px;padding:12px;display:flex;flex-direction:column;gap:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-overlay)}.dsh-wt-create-error{max-width:360px;font-size:12px;color:var(--dsw-alias-state-error-primary)}
108
+ .dsh-wt-create-controls{position:relative;font-size:13px;gap:4px}.dsh-wt-create-controls select{max-width:160px;min-height:28px}.dsh-wt-create-controls .dsh-wt-control{display:inline-flex;align-items:center;gap:6px;min-height:28px;padding:4px 8px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}.dsh-wt-create-controls .dsh-wt-control:hover:not(:disabled),.dsh-wt-create-controls .dsh-wt-control[aria-expanded=true]{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}.dsh-wt-control-text{max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsh-wt-control svg{flex:none}.dsh-wt-choice{position:relative;display:inline-flex}.dsh-wt-choice-menu,.dsh-wt-base-options{position:absolute;bottom:calc(100% + 6px);left:0;z-index:100;box-sizing:border-box;padding:4px;border:0;border-radius:10px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-label-primary);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent)}.dsh-wt-choice-menu{min-width:168px;max-width:360px}.dsh-wt-create-controls .dsh-wt-choice-item,.dsh-wt-create-controls .dsh-wt-branch-item{display:flex;align-items:center;gap:8px;width:100%;min-height:34px;padding:6px 8px;border:0;border-radius:6px;text-align:left;font-size:13px;line-height:20px;background:transparent;color:var(--dsw-alias-label-primary)}.dsh-wt-create-controls :is(.dsh-wt-choice-item,.dsh-wt-branch-item):is(:hover,:focus-visible):not(:disabled){background:var(--dsw-alias-interactive-bg-hover);outline:none}.dsh-wt-choice-check{display:inline-flex;align-items:center;justify-content:center;width:16px;flex:none}.dsh-wt-base-options{width:min(340px,calc(100vw - 32px));padding:10px;display:flex;flex-direction:column;gap:8px}.dsh-wt-branch-heading{font-size:13px;font-weight:600}.dsh-wt-branch-sources{display:flex;gap:4px;padding:3px;border-radius:8px;background:var(--dsw-alias-interactive-bg-hover)}.dsh-wt-create-controls .dsh-wt-branch-sources button{flex:1;border:0;min-height:28px;padding:4px 8px;border-radius:6px;font-size:12px;color:var(--dsw-alias-label-secondary);background:transparent}.dsh-wt-create-controls .dsh-wt-branch-sources button[aria-checked=true]{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-elevation-soft)}.dsh-wt-create-controls .dsh-wt-branch-search{display:flex;align-items:center;flex-wrap:nowrap;gap:6px;min-height:32px;padding:0 8px;box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}.dsh-wt-branch-search:focus-within{border-color:var(--dsw-alias-state-business-primary)}.dsh-wt-branch-search svg{flex:none}.dsh-wt-create-controls .dsh-wt-branch-search input[type=search]{flex:1;width:0;min-width:0;max-width:none;padding:0;border:0;border-radius:0;background:transparent;box-shadow:none;outline:none;appearance:none;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary)}.dsh-wt-create-controls .dsh-wt-branch-search input[type=search]::placeholder{color:var(--dsw-alias-label-secondary)}.dsh-wt-branch-list{max-height:240px;overflow:auto}.dsh-wt-branch-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsh-wt-branch-badge{flex:none;color:var(--dsw-alias-label-secondary);font-size:11px}.dsh-wt-branch-help{font-size:12px;color:var(--dsw-alias-label-secondary)}.dsh-wt-create-error{max-width:360px;font-size:12px;color:var(--dsw-alias-state-error-primary)}.dsh-wt-create-controls button:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.dsh-wt-create-controls .dsh-wt-branch-search input[type=search]:focus-visible,.dsh-wt-create-controls :is(.dsh-wt-choice-item,.dsh-wt-branch-item):focus-visible{outline:none}
104
109
  .dsh-wt-projects{min-height:0;display:flex;flex-direction:column;gap:6px;padding:8px;width:100%;box-sizing:border-box;font-size:13px}.dsh-wt-projects-header{display:flex;align-items:center;justify-content:space-between;padding:4px}.dsh-wt-project-list{min-height:0;overflow:auto}.dsh-wt-project-head{display:flex;align-items:center;gap:4px;margin-top:8px}.dsh-wt-project-head>button:first-child{flex:1;min-width:0;text-align:left;display:flex;align-items:center;gap:7px}.dsh-wt-projects button{border:0;padding:4px 6px}.dsh-wt-project-title,.dsh-wt-thread-title{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.dsh-wt-project-title{font-weight:600}.dsh-wt-thread{border-radius:7px;position:relative}.dsh-wt-thread[aria-selected=true]{background:var(--dsw-alias-interactive-bg-hover)}.dsh-wt-thread-main{display:flex;align-items:center;gap:5px;min-height:32px;padding-left:12px}.dsh-wt-thread-main>button:first-child{display:flex;align-items:center;gap:6px;flex:1;min-width:0;text-align:left}.dsh-wt-thread-title{flex:1}.dsh-wt-thread-main:hover .dsh-wt-thread-title{overflow:auto;text-overflow:clip}.dsh-wt-thread-actions{display:flex;align-items:center;opacity:0}.dsh-wt-thread:is(:hover,:focus-within) .dsh-wt-thread-actions{opacity:1}.dsh-wt-thread-actions button{min-width:24px}.dsh-wt-thread-leading{width:16px;display:inline-flex;flex-shrink:0}.dsh-wt-thread-status{font-size:11px;color:var(--dsw-alias-state-business-primary)}.dsh-wt-thread-pending{color:var(--dsw-alias-state-warning-primary)}.dsh-wt-thread-archived{opacity:.65}.dsh-wt-branch-chip{display:inline-flex;align-items:center;justify-content:center;width:16px;flex-shrink:0;color:var(--dsw-alias-label-secondary)}.dsh-wt-thread-hover{padding:8px;border:1px solid var(--dsw-alias-border-l3);border-radius:6px;background:var(--dsw-alias-bg-layer-2)}.dsh-wt-project-menu{display:flex;flex-direction:column;align-items:stretch}.dsh-wt-project-menu button{text-align:left}.dsh-wt-project-picker{display:flex;align-items:center;gap:4px;max-width:100%}.dsh-wt-project-picker select{max-width:190px}.dsh-wt-folder-list{padding:0;list-style:none}.dsh-wt-folder-list li{display:flex;align-items:center;gap:8px;padding:4px 0;overflow-wrap:anywhere}.dsh-wt-folder-list code{flex:1}.dsh-wt-folder-browser{max-height:45vh;overflow:auto}.dsh-wt-folder-browser button{display:block;width:100%;text-align:left}.dsh-wt-dialog::backdrop{background:var(--dsw-alias-bg-mask-1)}.dsh-wt-details{display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 12px}.dsh-wt-details dd{margin:0;overflow-wrap:anywhere}
105
110
  /* Sidebar cell metrics follow the native Folder view; dialogs keep their own rules. */
106
111
  .dsh-wt-projects{font-family:inherit;font-size:14px;font-weight:400;line-height:20px;gap:4px}.dsh-wt-projects-header{height:36px;box-sizing:border-box;padding:0 4px;margin:2px 0 4px;color:var(--dsw-alias-label-secondary)}.dsh-wt-projects-header strong{font-weight:inherit}.dsh-wt-projects-header>button{width:28px;height:28px;min-height:28px;padding:0}.dsh-wt-project-list{padding-bottom:16px}.dsh-wt-project-group+.dsh-wt-project-group{margin-top:4px}
@@ -148,7 +153,7 @@ window.__ModuleLoader__.load({
148
153
  else if (request.id) valid = valid && record(data.worktree) && (data.status === null || (typeof data.status?.dirty === 'boolean' && string(data.status.head)));
149
154
  else valid = valid && string(data.repository?.root) && string(data.repository.head) && (data.repository.branch === null || typeof data.repository.branch === 'string') && Array.isArray(data.remotes) && data.remotes.every(item => string(item?.name) && string(item.identity));
150
155
  } else if (request.action === 'branches') {
151
- valid = data && data.remote === request.remote && string(data.remoteIdentity) && (!request.remoteIdentity || data.remoteIdentity === request.remoteIdentity) && Array.isArray(data.items) && data.items.every(item => string(item?.name) && string(item.oid)) && Number.isFinite(data.observedAt) && (data.defaultBranch === null || typeof data.defaultBranch === 'string') && (!data.nextCursor || string(data.nextCursor));
156
+ valid = data && (request.branchSource === 'local' ? data.branchSource === 'local' && (data.currentBranch === null || string(data.currentBranch)) : data.branchSource !== 'local' && data.remote === request.remote && string(data.remoteIdentity) && (!request.remoteIdentity || data.remoteIdentity === request.remoteIdentity)) && Array.isArray(data.items) && data.items.every(item => string(item?.name) && string(item.oid)) && Number.isFinite(data.observedAt) && (data.defaultBranch === null || typeof data.defaultBranch === 'string') && (!data.nextCursor || string(data.nextCursor));
152
157
  } else if (request.action === 'list') valid = data && Array.isArray(data.items) && data.items.every(record) && (!data.nextCursor || string(data.nextCursor));
153
158
  else if (['create', 'start', 'handoff'].includes(request.action)) valid = data && string(data.sessionId) && string(data.workspaceId) && string(data.settingsHash) && record(data.worktree) && operation(data.operation);
154
159
  else if (request.action === 'preview') valid = data && string(data.id) && data.worktreeId === request.id && string(data.sourceSessionId) && string(data.targetRoot) && string(data.baseOid) && string(data.patchHash) && string(data.patchPath) && Number.isFinite(data.bytes) && Array.isArray(data.files) && data.files.every(file => string(file?.path) && typeof file.status === 'string' && typeof file.binary === 'boolean');
@@ -227,8 +232,63 @@ window.__ModuleLoader__.load({
227
232
  const separator = /^[A-Za-z]:[\\/]|^\\\\/.test(path) ? /[\\/]/ : /\//;
228
233
  return path.split(separator).filter(Boolean).at(-1) || path;
229
234
  }
230
- function createFolderScanner(uiWorkspace, publish) {
231
- let active, generation = 0, disposed = false;
235
+ // Two independent sources can serve one native folder pick:
236
+ // * the shell bridge `__DSH_DIRECTORY_PICKER__.pick` (desktop shell dialog), and
237
+ // * the Host-backed `uiWorkspace.pickDirectory` (server session dialog).
238
+ // Contract, chosen because asking both at once opens two dialogs when both work:
239
+ // the first source decides. A path wins; null is the operator cancelling *that*
240
+ // dialog and is returned as-is, never handed to the next source; only a failure —
241
+ // including a synchronous throw — falls through to the next source. A first
242
+ // source that never settles cannot be escaped without a deadline or an explicit
243
+ // capability probe, so that case stays with the operator to dismiss.
244
+ function settleFirst(pickers) {
245
+ return new Promise((resolve, reject) => {
246
+ if (!pickers.length) { reject(new WorktreeError('folderPickerUnavailable', 'no-directory-picker')); return; }
247
+ let index = 0, firstError;
248
+ const attempt = () => {
249
+ if (index >= pickers.length) { reject(firstError); return; }
250
+ const pick = pickers[index]; index += 1;
251
+ let call;
252
+ // A synchronous throw from one source must not abort the next.
253
+ try { call = pick(); }
254
+ catch (error) { firstError = firstError ?? error; attempt(); return; }
255
+ Promise.resolve(call).then(value => {
256
+ // A cancellation is an answer: it must not open a second dialog.
257
+ if (typeof value === 'string' && value) resolve(value); else resolve(null);
258
+ }, error => { firstError = firstError ?? error; attempt(); });
259
+ };
260
+ attempt();
261
+ });
262
+ }
263
+ function nativeFolderPicker(ctx) {
264
+ const bridge = globalThis.__DSH_DIRECTORY_PICKER__;
265
+ const pickers = [];
266
+ if (typeof bridge?.pick === 'function') pickers.push(() => bridge.pick());
267
+ if (typeof ctx.uiWorkspace?.pickDirectory === 'function') pickers.push(() => ctx.uiWorkspace.pickDirectory());
268
+ return pickers.length ? () => settleFirst(pickers) : undefined;
269
+ }
270
+ function addModesFor({ picker, browse }) {
271
+ return ['path', picker && 'native', browse && 'browse'].filter(Boolean);
272
+ }
273
+ function matchingAddMode(mode, modes) {
274
+ if (!modes.length || modes.includes(mode)) return mode;
275
+ return modes.includes('native') ? 'native' : modes.includes('browse') ? 'browse' : 'path';
276
+ }
277
+ /** Entry to select while nothing has been chosen yet: the least typing for this session. */
278
+ function preferredAddMode(modes) {
279
+ return modes.includes('native') ? 'native' : modes.includes('browse') ? 'browse' : 'path';
280
+ }
281
+ // The composed picker advertises both verbs, so availability is learned by
282
+ // asking: one bounded probe (no path = the Host home) decides whether this
283
+ // session can serve the in-app browser at all.
284
+ async function browseSupported(uiWorkspace) {
285
+ if (typeof uiWorkspace?.listDirectory !== 'function') return false;
286
+ const controller = new AbortController();
287
+ try { await uiWorkspace.listDirectory(undefined, controller.signal); return true; }
288
+ catch { return false; }
289
+ finally { controller.abort(); }
290
+ }
291
+ function createFolderScanner(uiWorkspace, publish) { let active, generation = 0, disposed = false;
232
292
  return {
233
293
  async scan(path) {
234
294
  if (disposed) return;
@@ -599,7 +659,7 @@ window.__ModuleLoader__.load({
599
659
  }
600
660
  function createRepositoryAvailability(ctx, projects) {
601
661
  const listeners = new Set(); let disposed = false, started = false, observedSession, active, version = 0, stopBinding = () => {}, queued = false, connectionId = ctx.connection.generation.getSnapshot()?.id;
602
- let state = { target: null, phase: 'idle', available: false };
662
+ let state = { target: null, phase: 'idle', available: false, status: null };
603
663
  const publish = patch => { if (disposed) return; state = { ...state, ...patch }; for (const listener of [...listeners]) listener(); };
604
664
  const store = { getSnapshot: () => state, subscribe: listener => { listeners.add(listener); return () => listeners.delete(listener); } };
605
665
  function target(sessionId) {
@@ -610,7 +670,7 @@ window.__ModuleLoader__.load({
610
670
  return { sessionId, binding, connection: connection.id, project, path: project?.path };
611
671
  }
612
672
  const same = (left, right) => !!left && !!right && left.sessionId === right.sessionId && left.binding === right.binding && left.connection === right.connection && sameProject(left.project, right.project);
613
- function reset() { active?.controller.abort(); active = undefined; ++version; stopBinding(); stopBinding = () => {}; publish({ target: null, phase: 'idle', available: false }); }
673
+ function reset() { active?.controller.abort(); active = undefined; ++version; stopBinding(); stopBinding = () => {}; publish({ target: null, phase: 'idle', available: false, status: null }); }
614
674
  function schedule() { if (!started || queued || disposed) return; queued = true; void Promise.resolve().then(() => { queued = false; if (!disposed) void ensure(projectActor(ctx.sessions.list.getSnapshot())); }); }
615
675
  function changed() { const current = target(projectActor(ctx.sessions.list.getSnapshot())); if (state.target && !same(current, state.target)) reset(); if (current && !state.target) schedule(); }
616
676
  function ensure(sessionId) {
@@ -627,7 +687,7 @@ window.__ModuleLoader__.load({
627
687
  // Read-only local discovery through the existing caller-authorized status operation.
628
688
  const data = await requestHost(ctx, sessionId, { action: 'status', ...(next.path ? { repoPath: next.path } : {}) }, controller.signal);
629
689
  if (next.path && data.projectPath !== next.path) throw new WorktreeError('decode', 'repository-target-mismatch');
630
- if (!disposed && !controller.signal.aborted && seq === version && same(target(sessionId), next)) publish({ phase: data.remotes.length ? 'ready' : 'unavailable', available: data.remotes.length > 0 });
690
+ if (!disposed && !controller.signal.aborted && seq === version && same(target(sessionId), next)) publish({ phase: 'ready', available: true, status: data });
631
691
  } catch { if (!disposed && !controller.signal.aborted && seq === version && same(target(sessionId), next)) publish({ phase: 'unavailable', available: false }); }
632
692
  finally { if (active === operation) active = undefined; }
633
693
  });
@@ -644,7 +704,8 @@ window.__ModuleLoader__.load({
644
704
  }
645
705
  function createWorktreeFlow(ctx, t, projects, repositories) {
646
706
  const conversation = ctx.get('conversation'), listeners = new Set(), leases = new Map(), operations = new Set(), restaged = new Set();
647
- let state = { sessionId: undefined, mode: 'local', sourceChanged: false, busy: false, loading: false, error: '', status: null, remote: '', branch: '', branches: null, query: '', steps: [], phase: '', op: null };
707
+ let state = { sessionId: undefined, mode: 'local', sourceChanged: false, busy: false, loading: false, error: '', status: null, branchSource: 'local', remote: '', branch: '', branchOid: '', branches: null, query: '', steps: [], phase: '', op: null };
708
+ const branchCache = new Map();
648
709
  let query, selectedSource, generation = 0, disposed = false;
649
710
  const publish = patch => { if (disposed) return; state = { ...state, ...patch }; for (const listener of [...listeners]) listener(); };
650
711
  const store = { getSnapshot: () => state, subscribe: listener => { listeners.add(listener); return () => listeners.delete(listener); } };
@@ -655,7 +716,6 @@ window.__ModuleLoader__.load({
655
716
  try {
656
717
  const data = await requestHost(ctx, sessionId, { action: 'status', ...(project ? { repoPath: project.path } : {}) }, signal);
657
718
  if (project && data.projectPath !== project.path) throw new WorktreeError('decode', 'repository-target-mismatch');
658
- if (!data.remotes.length) throw new WorktreeError('noRemote', 'no-remote');
659
719
  return data;
660
720
  } catch (error) { if (!signal.aborted) repositories?.unavailable(sessionId, binding, project); throw error; }
661
721
  }
@@ -684,8 +744,8 @@ window.__ModuleLoader__.load({
684
744
  const captured = source(sessionId); requireIntent(sessionId, captured); if (captured.binding !== binding) throw new WorktreeError('changed', 'binding-changed');
685
745
  const old = state.op;
686
746
  if (old && !old.retryable) throw new WorktreeError('recovery', 'check-created-conversation-before-retry');
687
- const intent = Object.freeze({ remote: state.remote, branch: state.branch, branches: state.branches && Object.freeze({ ...state.branches, items: Object.freeze(state.branches.items.map(item => Object.freeze({ ...item }))) }) });
688
- query?.abort(); ++generation;
747
+ const intent = Object.freeze({ branchSource: state.branchSource, branchOid: state.branchOid, remote: state.remote, branch: state.branch, branches: state.branches && Object.freeze({ ...state.branches, items: Object.freeze(state.branches.items.map(item => Object.freeze({ ...item }))) }) });
748
+ query?.abort(); ++generation; branchCache.clear();
689
749
  const navigation = ctx.layout.beginNavigation();
690
750
  const op = { ...captured, intent, mode: 'new', id: old?.id || crypto.randomUUID(), sessionId, controller, signal, navigation, result: old?.result || null, request: old?.request || null, retryable: false, admission: 'none', reference: null, sourceReference: null, staged: false, cleanup: () => {} };
691
751
  op.sourceReference = ctx.sessions.retain(sessionId, { source: 'controllerOperation' });
@@ -709,15 +769,22 @@ window.__ModuleLoader__.load({
709
769
  try {
710
770
  check(op);
711
771
  if (!text.trim() && !ids.length) { publish({ busy: false, phase: '', op: null }); return { kind: 'success' }; }
712
- publish({ phase: 'fetching' });
772
+ publish({ phase: op.intent.branchSource === 'local' ? 'creating' : 'fetching' });
713
773
  if (!op.result) {
714
774
  const status = await repositoryStatus(op.sessionId, op.binding, op.project, signal); check(op);
715
- const remote = op.intent.remote || (status.remotes.some(item => item.name === status.defaults?.remote) ? status.defaults.remote : status.remotes[0]?.name || '');
716
- if (!remote) throw new WorktreeError('noRemote', 'no-remote');
717
- const identity = status.remotes.find(item => item.name === remote)?.identity;
718
- const advertised = op.intent.branches && op.intent.remote === remote ? op.intent.branches : await requestHost(ctx, op.sessionId, { action: 'branches', repoPath: projectPath(status, op.project), remote, remoteIdentity: identity, limit: 100 }, signal); check(op);
719
- const branch = op.intent.branch || advertisedDefault(advertised, status.defaults?.branch); if (!branch) throw new WorktreeError('noRemote', 'select-remote-base');
720
- op.request = Object.freeze({ action: 'create', operationId: op.id, repoPath: projectPath(status, op.project), remote, remoteIdentity: advertised.remoteIdentity, remoteBranch: branch, sourceSessionId: op.sessionId, sessionMode: 'new', requireBlankSource: true, settingsHash: status.settingsHash, firstPrompt: text, ...(op.project ? { projectId: op.project.projectId, folderId: op.project.folderId } : {}) });
775
+ let startingBranch;
776
+ if (op.intent.branchSource === 'local') {
777
+ const branch = op.intent.branchOid ? op.intent.branch : status.repository.branch || 'HEAD';
778
+ startingBranch = { branchSource: 'local', localBranch: branch, localOid: op.intent.branchOid || status.repository.head };
779
+ } else {
780
+ const remote = op.intent.remote || (status.remotes.some(item => item.name === status.defaults?.remote) ? status.defaults.remote : status.remotes[0]?.name || '');
781
+ if (!remote) throw new WorktreeError('noRemote', 'no-remote');
782
+ const identity = status.remotes.find(item => item.name === remote)?.identity;
783
+ const advertised = op.intent.branches && op.intent.remote === remote ? op.intent.branches : await requestHost(ctx, op.sessionId, { action: 'branches', repoPath: projectPath(status, op.project), remote, remoteIdentity: identity, limit: 100 }, signal); check(op);
784
+ const branch = op.intent.branch || advertisedDefault(advertised, status.repository.branch || status.defaults?.branch); if (!branch) throw new WorktreeError('noRemote', 'select-remote-branch');
785
+ startingBranch = { branchSource: 'remote', remote, remoteIdentity: advertised.remoteIdentity, remoteBranch: branch };
786
+ }
787
+ op.request = Object.freeze({ action: 'create', operationId: op.id, repoPath: projectPath(status, op.project), ...startingBranch, sourceSessionId: op.sessionId, sessionMode: 'new', requireBlankSource: true, settingsHash: status.settingsHash, firstPrompt: text, ...(op.project ? { projectId: op.project.projectId, folderId: op.project.folderId } : {}) });
721
788
  op.result = await prepareWorktree(ctx, op.sessionId, op.request, signal, steps => publish({ steps, phase: steps.at(-1)?.stage || 'fetching' })); check(op);
722
789
  } else if (op.request?.firstPrompt !== text) throw new WorktreeError('changed', 'retry-message-changed');
723
790
  const result = op.result;
@@ -774,36 +841,55 @@ window.__ModuleLoader__.load({
774
841
  }
775
842
  if (mode === 'local') { for (const lease of leases.values()) lease.cancelNew(); }
776
843
  const changed = state.sessionId !== sessionId || state.sourceChanged || selectedSource?.binding !== current.binding || !sameProject(selectedSource?.project, current.project);
777
- if (changed) { query?.abort(); ++generation; }
844
+ if (changed) { query?.abort(); ++generation; branchCache.clear(); }
778
845
  selectedSource = { sessionId, binding: current.binding, project: current.project };
779
- publish({ sessionId, mode, sourceChanged: false, error: '', ...(changed ? { status: null, branches: null, branch: '', remote: '', ...(state.sessionId === sessionId ? {} : { op: null, steps: [], phase: '' }) } : {}) });
846
+ const observed = repositories.store.getSnapshot().status;
847
+ publish({ sessionId, mode, sourceChanged: false, error: '', ...(changed ? { status: null, branches: null, branchSource: 'local', branch: observed?.repository.branch || '', branchOid: '', remote: '', query: '', ...(state.sessionId === sessionId ? {} : { op: null, steps: [], phase: '' }) } : {}) });
780
848
  } catch (error) { publish({ sessionId, mode: latchedNew ? 'new' : 'local', error: report(t, error) }); }
781
849
  }
782
- async function loadBranches(sessionId, text = '', more = false) {
783
- if (disposed || state.busy || state.sessionId !== sessionId || !state.status || !state.remote || (more && !state.branches?.nextCursor)) return;
850
+ async function loadBranches(sessionId, refresh = false) {
851
+ if (disposed || state.busy || state.sessionId !== sessionId || !state.status || (state.branchSource === 'remote' && !state.remote)) return;
784
852
  let current;
785
853
  try { current = source(sessionId); requireRepository(sessionId); requireIntent(sessionId, current); } catch (error) { publish({ error: report(t, error) }); return; }
786
- query?.abort(); query = new AbortController(); const controller = query, seq = ++generation, before = state.branches, remote = state.remote, project = current.project;
787
- publish({ loading: true, query: text, ...(more ? {} : { branches: null, branch: '' }) });
854
+ query?.abort(); query = new AbortController(); const controller = query, seq = ++generation, remote = state.remote, branchSource = state.branchSource, project = current.project;
855
+ const key = branchSource === 'local' ? 'local' : 'remote:' + remote, cached = !refresh && branchCache.get(key);
856
+ const choose = branches => {
857
+ const preferred = branchSource === 'local' ? state.status.repository.branch || 'HEAD' : state.status.repository.branch || state.status.defaults?.branch;
858
+ const branch = branches.items.some(item => item.name === state.branch) ? state.branch : advertisedDefault(branches, preferred);
859
+ publish({ branches, branch, branchOid: branches.items.find(item => item.name === branch)?.oid || '', loading: false });
860
+ };
861
+ if (cached) { choose(cached); return; }
862
+ publish({ loading: true, error: '', branches: null });
788
863
  try {
789
- const data = await requestHost(ctx, sessionId, { action: 'branches', repoPath: projectPath(state.status, project), remote, remoteIdentity: state.status.remotes.find(item => item.name === remote)?.identity, query: text, ...(more ? { cursor: before.nextCursor } : {}), limit: 100 }, controller.signal);
790
- if (disposed || controller.signal.aborted || seq !== generation) return; checkSource(sessionId, current);
791
- const names = new Set(more ? before.items.map(item => item.name) : []), branches = { ...data, items: [...(more ? before.items : []), ...data.items.filter(item => !names.has(item.name))] };
792
- publish({ branches, branch: more ? state.branch : text ? '' : advertisedDefault(branches, state.status.defaults?.branch), loading: false });
864
+ // Download one bounded generation, following its cursors. Search only
865
+ // filters these items; it never triggers discovery or remote fetches.
866
+ let branches, cursor; const cursors = new Set();
867
+ do {
868
+ const data = await requestHost(ctx, sessionId, { action: 'branches', repoPath: projectPath(state.status, project), ...(branchSource === 'local' ? { branchSource: 'local' } : { remote, remoteIdentity: state.status.remotes.find(item => item.name === remote)?.identity }), ...(cursor ? { cursor } : {}), limit: 100 }, controller.signal);
869
+ if (disposed || controller.signal.aborted || seq !== generation) return; checkSource(sessionId, current);
870
+ if (branches && (data.observedAt !== branches.observedAt || data.defaultBranch !== branches.defaultBranch)) throw new WorktreeError('decode', 'branch-generation-changed');
871
+ const names = new Set(branches?.items.map(item => item.name) || []);
872
+ branches = { ...data, items: [...(branches?.items || []), ...data.items.filter(item => !names.has(item.name))] };
873
+ cursor = data.nextCursor;
874
+ if (cursor && cursors.has(cursor)) throw new WorktreeError('decode', 'repeated-branch-cursor');
875
+ if (cursor) cursors.add(cursor);
876
+ } while (cursor);
877
+ branchCache.set(key, branches); choose(branches);
793
878
  } catch (error) { if (!controller.signal.aborted && seq === generation) publish({ loading: false, error: report(t, error) }); }
794
879
  }
795
880
  async function configure(sessionId) {
796
- if (disposed || state.busy) return;
881
+ if (disposed || state.busy || state.loading) return;
797
882
  let current;
798
883
  try { current = source(sessionId); requireRepository(sessionId); requireIntent(sessionId, current); } catch (error) { publish({ error: report(t, error) }); return; }
884
+ if (state.sessionId === sessionId && state.status) { await loadBranches(sessionId); return; }
799
885
  query?.abort(); query = new AbortController(); const controller = query, seq = ++generation, project = current.project;
800
886
  if (state.sessionId !== sessionId || state.mode !== 'new') selectedSource = { sessionId, binding: current.binding, project };
801
- publish({ sessionId, loading: true, status: null, branches: null, branch: '', remote: '', error: '', ...(state.sessionId === sessionId ? {} : { mode: 'local', sourceChanged: false, op: null, steps: [], phase: '' }) });
887
+ publish({ sessionId, loading: true, status: null, branches: null, branchSource: 'local', branch: '', branchOid: '', remote: '', query: '', error: '', ...(state.sessionId === sessionId ? {} : { mode: 'local', sourceChanged: false, op: null, steps: [], phase: '' }) });
802
888
  try {
803
889
  const status = await repositoryStatus(sessionId, current.binding, project, controller.signal);
804
890
  if (disposed || controller.signal.aborted || seq !== generation) return; checkSource(sessionId, current);
805
891
  const remote = status.remotes.some(item => item.name === status.defaults?.remote) ? status.defaults.remote : status.remotes[0]?.name || '';
806
- publish({ status, remote, loading: false }); if (remote) await loadBranches(sessionId);
892
+ publish({ status, remote, branch: status.repository.branch || 'HEAD', loading: false }); await loadBranches(sessionId);
807
893
  } catch (error) { if (!controller.signal.aborted && seq === generation) publish({ loading: false, error: report(t, error) }); }
808
894
  }
809
895
  async function reconcile(sessionId) {
@@ -817,28 +903,30 @@ window.__ModuleLoader__.load({
817
903
  }
818
904
  const repositoryChanged = repositories?.store.subscribe(() => {
819
905
  if (!state.sessionId || repositoryAllowed(state.sessionId)) return;
820
- query?.abort(); ++generation;
906
+ query?.abort(); ++generation; branchCache.clear();
821
907
  if (state.mode === 'new') for (const lease of leases.values()) lease.cancelNew();
822
- publish({ loading: false, status: null, branches: null, remote: '', branch: '', ...(state.mode === 'new' ? { error: t('repoUnavailable') } : {}) });
908
+ publish({ loading: false, status: null, branches: null, remote: '', branch: '', branchOid: '', query: '', ...(state.mode === 'new' ? { error: t('repoUnavailable') } : {}) });
823
909
  });
824
910
  function selectionChanged() {
825
911
  if (!selectedSource || state.sessionId !== selectedSource.sessionId || (state.mode === 'new' && state.sourceChanged)) return;
826
912
  const binding = ctx.sessions.binding(state.sessionId), project = captureProject(projects.context(state.sessionId));
827
913
  if (binding === selectedSource.binding && sameProject(project, selectedSource.project)) return;
828
- query?.abort(); ++generation;
914
+ query?.abort(); ++generation; branchCache.clear();
829
915
  if (state.mode === 'new') for (const lease of leases.values()) lease.cancelNew();
830
916
  else selectedSource = { sessionId: state.sessionId, binding, project };
831
- publish({ loading: false, status: null, branches: null, query: '', remote: '', branch: '', ...(state.mode === 'new' ? { sourceChanged: true, error: t('projectChanged') } : {}) });
917
+ publish({ loading: false, status: null, branches: null, query: '', remote: '', branch: '', branchOid: '', ...(state.mode === 'new' ? { sourceChanged: true, error: t('projectChanged') } : {}) });
832
918
  }
833
919
  const projectChanged = projects.store.subscribe(selectionChanged);
834
920
  const unselect = ctx.sessions.list.subscribe(() => { for (const op of operations) if (!op.committing && projectActor(ctx.sessions.list.getSnapshot()) !== op.sessionId) op.controller.abort(); selectionChanged(); });
835
- const reconnect = ctx.connection.generation.subscribe(() => { query?.abort(); ++generation; for (const lease of leases.values()) lease.cancelNew(); publish({ loading: false, status: null, branches: null, branch: '', remote: '' }); });
921
+ const reconnect = ctx.connection.generation.subscribe(() => { query?.abort(); ++generation; branchCache.clear(); for (const lease of leases.values()) lease.cancelNew(); publish({ loading: false, status: null, branches: null, branch: '', branchOid: '', query: '', remote: '' }); });
836
922
  return { store, observe, select, configure, loadBranches, reconcile,
837
923
  cancel() { for (const lease of leases.values()) lease.cancelNew(); },
838
- setRemote(sessionId, remote) { if (!state.busy && !state.sourceChanged && state.sessionId === sessionId && repositoryAllowed(sessionId) && state.status?.remotes.some(item => item.name === remote)) { publish({ remote, query: '' }); void loadBranches(sessionId); } },
839
- setBranch(branch) { if (!state.busy && !state.sourceChanged && repositoryAllowed(state.sessionId) && state.branches?.items.some(item => item.name === branch)) publish({ branch }); },
924
+ setRemote(sessionId, remote) { if (!state.busy && !state.sourceChanged && state.sessionId === sessionId && repositoryAllowed(sessionId) && state.status?.remotes.some(item => item.name === remote)) { publish({ remote, query: '', branch: '', branchOid: '', branches: null }); void loadBranches(sessionId); } },
925
+ setBranchSource(sessionId, branchSource) { if (!state.busy && !state.sourceChanged && state.sessionId === sessionId && repositoryAllowed(sessionId) && ['local', 'remote'].includes(branchSource) && (branchSource === 'local' || state.remote)) { publish({ branchSource, branch: '', branchOid: '', branches: null, query: '' }); void loadBranches(sessionId); } },
926
+ setQuery(query) { if (!state.busy) publish({ query }); },
927
+ setBranch(branch) { const item = state.branches?.items.find(item => item.name === branch); if (!state.busy && !state.loading && !state.sourceChanged && repositoryAllowed(state.sessionId) && item) { publish({ branch, branchOid: item.oid }); return true; } return false; },
840
928
  openTarget(sessionId) { const op = state.sessionId === sessionId && state.op; if (op?.result?.sessionId) ctx.uiWorkspace.openSession(op.result.sessionId); },
841
- async dispose() { disposed = true; query?.abort(); ++generation; repositoryChanged?.(); projectChanged(); unselect(); reconnect(); for (const op of operations) op.controller.abort(); await Promise.allSettled([...leases.values()].map(lease => lease.close())); for (const lease of leases.values()) lease.stopScope?.(); leases.clear(); selectedSource = undefined; },
929
+ async dispose() { disposed = true; query?.abort(); ++generation; branchCache.clear(); repositoryChanged?.(); projectChanged(); unselect(); reconnect(); for (const op of operations) op.controller.abort(); await Promise.allSettled([...leases.values()].map(lease => lease.close())); for (const lease of leases.values()) lease.stopScope?.(); leases.clear(); selectedSource = undefined; },
842
930
  };
843
931
  }
844
932
  const projectSlotName = key => PANEL + '.projects.' + key;
@@ -851,7 +939,7 @@ window.__ModuleLoader__.load({
851
939
  // Only plugin-owned aliases are declared/mirrored; unloading never clears or
852
940
  // redeclares installed extension seats. Copy their inject/locale/store faces,
853
941
  // not DOM or private components from a Harness module import.
854
- function mirrorProjectSlot(ctx, source, destination = projectSlotName(source)) {
942
+ function mirrorProjectSlot(ctx, source, destination = projectSlotName(source), mapKey = projectSlotName) {
855
943
  return ctx.slots.inject(destination, () => ctx.slots.inject(source, () => {
856
944
  const copies = new Map();
857
945
  const clear = () => { for (const dispose of copies.values()) dispose(); copies.clear(); };
@@ -860,10 +948,10 @@ window.__ModuleLoader__.load({
860
948
  for (const [entry, dispose] of copies) if (!entries.includes(entry)) { dispose(); copies.delete(entry); }
861
949
  for (const entry of entries) {
862
950
  if (copies.has(entry)) continue;
863
- const children = entry.children && Object.fromEntries(Object.entries(entry.children).map(([key, spec]) => [projectSlotName(key), spec]));
864
- const component = children ? props => h(entry.component, { ...props, renderSlot: (key, owner, options) => props.renderSlot(projectSlotName(key), owner, options), ...(props.renderSlotChain ? { renderSlotChain: (key, owner, options) => props.renderSlotChain(projectSlotName(key), owner, options) } : {}) }) : entry.component;
951
+ const children = entry.children && Object.fromEntries(Object.entries(entry.children).map(([key, spec]) => [mapKey(key), spec]));
952
+ const component = children ? props => h(entry.component, { ...props, renderSlot: (key, owner, options) => props.renderSlot(mapKey(key), owner, options), ...(props.renderSlotChain ? { renderSlotChain: (key, owner, options) => props.renderSlotChain(mapKey(key), owner, options) } : {}) }) : entry.component;
865
953
  const remove = ctx.slots.register({ ...entry.options, name: destination, ...(entry.inject ? { inject: entry.inject } : {}), ...(entry.locale ? { locale: entry.locale } : {}), ...(entry.store ? { store: entry.store } : {}), ...(entry.select ? { select: entry.select } : {}), ...(children ? { children } : {}) }, component);
866
- const nested = Object.keys(entry.children || {}).map(key => mirrorProjectSlot(ctx, key));
954
+ const nested = Object.keys(entry.children || {}).map(key => mirrorProjectSlot(ctx, key, mapKey(key), mapKey));
867
955
  copies.set(entry, () => { for (const dispose of nested) dispose(); remove(); });
868
956
  }
869
957
  };
@@ -876,6 +964,31 @@ window.__ModuleLoader__.load({
876
964
  const store = { getSnapshot: () => native, subscribe: listener => { listeners.add(listener); return () => listeners.delete(listener); } };
877
965
  return { store, toggle() { if (disposed) return; if (native) registration = register(); else { registration(); registration = null; } native = !native; for (const listener of [...listeners]) listener(); }, dispose() { disposed = true; registration?.(); listeners.clear(); } };
878
966
  }
967
+ const sidebarViewLabel = (sidebarMode, t) => t(sidebarMode.store.getSnapshot() ? 'switchProjectsView' : 'switchWorkspaceView');
968
+ function registerSidebarViewSwitch(ctx, sidebarMode, component) {
969
+ const t = ctx.locale.bind(NS);
970
+ return ctx.slots.inject('sidebar', () => ctx.slots.inject('sidebar.panellist', () => {
971
+ let removeRow = () => {};
972
+ const synchronize = () => {
973
+ removeRow();
974
+ removeRow = ctx.slots.register({ name: 'sidebar.panellist', id: VIEW_SWITCH, order: 400, label: () => sidebarViewLabel(sidebarMode, t) }, component);
975
+ };
976
+ synchronize();
977
+ const unsubscribe = sidebarMode.store.subscribe(synchronize);
978
+ // Keep native declarations installed. The renderer authorizes child
979
+ // dispatch per entry, so bridge the native component through owned aliases.
980
+ const native = ctx.slots.entriesOfSlot('sidebar')[0], inject = native.inject;
981
+ const mapKey = key => VIEW_SWITCH + '.' + key;
982
+ const children = Object.fromEntries(Object.entries(native.children).map(([key, spec]) => [mapKey(key), spec]));
983
+ const componentWithActions = props => h(native.component, { ...props, renderSlot: (key, owner, options) => props.renderSlot(mapKey(key), owner, options) });
984
+ const removeSidebar = ctx.slots.register({ name: 'sidebar', priority: -50, locale: native.locale, children, inject: (...args) => {
985
+ const props = inject(...args);
986
+ return { ...props, selectPanel(id) { if (id === VIEW_SWITCH) sidebarMode.toggle(); else props.selectPanel(id); } };
987
+ } }, componentWithActions);
988
+ const mirrors = Object.keys(native.children).map(key => mirrorProjectSlot(ctx, key, mapKey(key), mapKey));
989
+ return () => { for (const stop of mirrors) stop(); removeSidebar(); unsubscribe(); removeRow(); };
990
+ }));
991
+ }
879
992
  function syncProjectSpinner(element) {
880
993
  if (element === null) return;
881
994
  for (const animation of element.getAnimations?.({ subtree: true }) ?? []) animation.startTime = 0;
@@ -893,8 +1006,8 @@ window.__ModuleLoader__.load({
893
1006
  h('g', { className: 'dsh-wt-state-motion' }, h('circle', { className: 'dsh-wt-state-track', cx: 12, cy: 12, r: 9.5 }), h('circle', { className: 'dsh-wt-state-arc', cx: 12, cy: 12, r: 9.5 })));
894
1007
  }
895
1008
  // Native workspace-folder artwork copied locally; React is the only runtime import.
896
- function WorkspaceFolderIcon({ expanded = false, className }) {
897
- return h('svg', { width: 16, height: 16, className, viewBox: '0 0 16 16', fill: 'none', strokeWidth: 1, 'aria-hidden': true }, expanded ? [
1009
+ function WorkspaceFolderIcon({ expanded = false, className, size = 16 }) {
1010
+ return h('svg', { width: size, height: size, className, viewBox: '0 0 16 16', fill: 'none', strokeWidth: 1, 'aria-hidden': true }, expanded ? [
898
1011
  h('path', { key: 'tint', d: 'M2.55912 7.93683C2.67584 7.49906 3.0723 7.19446 3.52536 7.19446H13.6491C14.3061 7.19446 14.7846 7.81725 14.6153 8.45209L13.4411 12.856C13.3244 13.2938 12.9279 13.5984 12.4748 13.5984H2.35113C1.69411 13.5984 1.21562 12.9756 1.38489 12.3407L2.55912 7.93683Z', fill: 'currentColor', opacity: 0.16 }),
899
1012
  h('path', { key: 'front', d: 'M13.6491 6.69446C14.6346 6.69453 15.3522 7.62895 15.0983 8.58118L13.9245 12.9845C13.7494 13.6412 13.1539 14.0988 12.4743 14.0988H2.35126C1.36574 14.0988 0.648153 13.1643 0.902044 12.212L2.07587 7.80774C2.25102 7.15128 2.84567 6.69455 3.52509 6.69446H13.6491ZM3.52509 7.69446C3.29865 7.69455 3.10004 7.84674 3.04169 8.06555L1.86786 12.4698C1.78345 12.7872 2.02285 13.0988 2.35126 13.0988H12.4743C12.7007 13.0988 12.8992 12.9463 12.9577 12.7277L14.1325 8.32336C14.2171 8.00598 13.9776 7.69453 13.6491 7.69446H3.52509Z', fill: 'currentColor' }),
900
1013
  h('path', { key: 'back', d: 'M4.7666 1.90137C5.13227 1.90144 5.48571 2.03525 5.75977 2.27734L7.27246 3.61328C7.36379 3.69382 7.48174 3.73828 7.60352 3.73828H12.3994C13.2276 3.73841 13.8993 4.41005 13.8994 5.23828V6.7168C13.8183 6.70327 13.735 6.69436 13.6494 6.69434H12.8994V5.23828C12.8993 4.96233 12.6754 4.73841 12.3994 4.73828H7.60352C7.23781 4.73828 6.88446 4.60438 6.61035 4.3623L5.09766 3.02637C5.00636 2.94576 4.88838 2.90144 4.7666 2.90137H2.0498C1.77366 2.90137 1.5498 3.12523 1.5498 3.40137V9.78223L0.902344 12.2119C0.648452 13.1642 1.36604 14.0986 2.35156 14.0986H2.0498C1.2214 14.0986 0.549838 13.427 0.549805 12.5986V3.40137C0.549805 2.57294 1.22138 1.90137 2.0498 1.90137H4.7666Z', fill: 'currentColor' }),
@@ -975,11 +1088,30 @@ window.__ModuleLoader__.load({
975
1088
  }
976
1089
  function ProjectEditor({ project, runtime: ctx, projects, t, close, returnFocus, fallbackFocus }) {
977
1090
  const [title, setTitle] = React.useState(project?.title || ''), [folders, setFolders] = React.useState(project?.folders.map(folder => folder.path) || []), [path, setPath] = React.useState(''), [mainFolder, setMainFolder] = React.useState(projectMainFolder(project)?.path || '');
978
- const [busy, setBusy] = React.useState(false), [error, setError] = React.useState(''), [browser, setBrowser] = React.useState(false), [addMode, setAddMode] = React.useState('browse'), [confirmRemoval, setConfirmRemoval] = React.useState(false);
979
- const lifetime = React.useRef(true), currentFolders = React.useRef(folders), picker = React.useRef(false), browsing = React.useRef(false), confirming = React.useRef(false), submitter = React.useRef(null), heading = React.useId(), nameInput = React.useRef(null), removeTrigger = React.useRef(null);
1091
+ const [busy, setBusy] = React.useState(false), [error, setError] = React.useState(''), [browser, setBrowser] = React.useState(false), [confirmRemoval, setConfirmRemoval] = React.useState(false);
1092
+ // Optimistic first paint: path entry works everywhere, so the two optional
1093
+ // entries appear only once the session confirms it can serve them.
1094
+ const [picker] = React.useState(() => nativeFolderPicker(ctx));
1095
+ const [capabilities, setCapabilities] = React.useState({ modes: ['path'], ready: false });
1096
+ const [addMode, setAddMode] = React.useState(() => matchingAddMode('path', ['path']));
1097
+ const lifetime = React.useRef(true), currentFolders = React.useRef(folders), picking = React.useRef(false), browsing = React.useRef(false), confirming = React.useRef(false), submitter = React.useRef(null), heading = React.useId(), nameInput = React.useRef(null), removeTrigger = React.useRef(null);
980
1098
  if (!submitter.current) submitter.current = createProjectSubmitter(projects, project?.id, project?.id || crypto.randomUUID());
981
1099
  React.useEffect(() => { lifetime.current = true; return () => { lifetime.current = false; }; }, []);
982
- const dismiss = () => { if (!submitter.current.pending && !picker.current && !confirming.current) close(); };
1100
+ React.useEffect(() => {
1101
+ let live = true;
1102
+ browseSupported(ctx.uiWorkspace).then(browse => { if (live) setCapabilities({ modes: addModesFor({ picker, browse }), ready: true }); });
1103
+ return () => { live = false; };
1104
+ }, [ctx, picker]);
1105
+ // Path entry is the only entry safe before the probe answers, so the probe
1106
+ // promotes its own placeholder to this session's picker — but never over a
1107
+ // choice the user already made.
1108
+ React.useEffect(() => {
1109
+ setAddMode(mode => {
1110
+ if (capabilities.ready && mode === 'path') return preferredAddMode(capabilities.modes);
1111
+ return matchingAddMode(mode, capabilities.modes);
1112
+ });
1113
+ }, [capabilities]);
1114
+ const dismiss = () => { if (!submitter.current.pending && !picking.current && !confirming.current) close(); };
983
1115
  const primary = draftMainFolder(folders, mainFolder);
984
1116
  function closeBrowser() { browsing.current = false; setBrowser(false); }
985
1117
  function add(paths) {
@@ -987,20 +1119,28 @@ window.__ModuleLoader__.load({
987
1119
  try { const next = appendProjectFolders(currentFolders.current, paths); currentFolders.current = next; setFolders(next); setPath(''); closeBrowser(); setError(''); }
988
1120
  catch (failure) { setError(report(t, failure)); }
989
1121
  }
990
- function remove(path) { if (submitter.current.pending || picker.current || browsing.current || confirming.current) return; currentFolders.current = currentFolders.current.filter(value => value !== path); setFolders(currentFolders.current); if (mainFolder === path) setMainFolder(''); setError(''); }
1122
+ function remove(path) { if (submitter.current.pending || picking.current || browsing.current || confirming.current) return; currentFolders.current = currentFolders.current.filter(value => value !== path); setFolders(currentFolders.current); if (mainFolder === path) setMainFolder(''); setError(''); }
991
1123
  async function choose() {
992
- if (submitter.current.pending || picker.current || browsing.current || confirming.current || currentFolders.current.length >= 32) return;
1124
+ if (submitter.current.pending || picking.current || browsing.current || confirming.current || currentFolders.current.length >= 32) return;
993
1125
  setError('');
994
- if (addMode === 'path') { add([path.trim()]); return; }
995
- if (addMode === 'browse') { browsing.current = true; setBrowser(true); return; }
996
- // The optional native chooser returns one folder or null, never a batch.
997
- picker.current = true; setBusy(true);
998
- try { const selected = await ctx.uiWorkspace.pickDirectory(); if (lifetime.current && selected) add([selected]); }
1126
+ // `void choose()` discards the returned promise, so every refusal a user can
1127
+ // trigger has to surface here; a synchronous throw must not vanish.
1128
+ try {
1129
+ if (addMode === 'path') { add([path.trim()]); return; }
1130
+ if (addMode === 'browse') {
1131
+ if (!capabilities.modes.includes('browse')) throw new WorktreeError('browseUnavailable', 'no-browse-capability');
1132
+ browsing.current = true; setBrowser(true); return;
1133
+ }
1134
+ if (typeof picker !== 'function') throw new WorktreeError('folderPickerUnavailable', 'no-directory-picker');
1135
+ // The native chooser returns one folder or null, never a batch.
1136
+ picking.current = true; setBusy(true);
1137
+ try { const selected = await picker(); if (lifetime.current && selected) add([selected]); }
1138
+ finally { picking.current = false; if (lifetime.current) setBusy(false); }
1139
+ }
999
1140
  catch (failure) { if (lifetime.current) setError(report(t, failure)); }
1000
- finally { picker.current = false; if (lifetime.current) setBusy(false); }
1001
1141
  }
1002
1142
  async function save(event) {
1003
- event.preventDefault(); if (submitter.current.pending || picker.current || browsing.current || confirming.current || !title.trim() || !currentFolders.current.length) return;
1143
+ event.preventDefault(); if (submitter.current.pending || picking.current || browsing.current || confirming.current || !title.trim() || !currentFolders.current.length) return;
1004
1144
  if (!draftMainFolder(currentFolders.current, mainFolder)) { setError(t('mainFolderRequired')); return; }
1005
1145
  setBusy(true); setError('');
1006
1146
  try { await submitter.current.submit(title, currentFolders.current, mainFolder); if (lifetime.current) close(); }
@@ -1008,12 +1148,12 @@ window.__ModuleLoader__.load({
1008
1148
  finally { if (lifetime.current) setBusy(false); }
1009
1149
  }
1010
1150
  function beginRemoval() {
1011
- if (!project || submitter.current.pending || picker.current || browsing.current || confirming.current) return;
1151
+ if (!project || submitter.current.pending || picking.current || browsing.current || confirming.current) return;
1012
1152
  confirming.current = true; setError(''); setConfirmRemoval(true);
1013
1153
  }
1014
1154
  function dismissRemoval() { if (!submitter.current.pending) { confirming.current = false; setConfirmRemoval(false); setError(''); } }
1015
1155
  async function removeProject() {
1016
- if (!confirming.current || submitter.current.pending || picker.current || browsing.current) return;
1156
+ if (!confirming.current || submitter.current.pending || picking.current || browsing.current) return;
1017
1157
  setBusy(true); setError('');
1018
1158
  try { await submitter.current.remove(); if (lifetime.current) close(); }
1019
1159
  catch (failure) { if (lifetime.current) setError(report(t, failure)); }
@@ -1023,7 +1163,8 @@ window.__ModuleLoader__.load({
1023
1163
  h('form', { onSubmit: save, 'aria-busy': busy }, h('label', { className: 'dsh-wt-project-name' }, h(FolderIcon), h('input', { required: true, ref: nameInput, maxLength: 120, placeholder: t('projectName'), 'aria-label': t('projectName'), value: title, disabled: busy, onChange: event => setTitle(event.target.value) })),
1024
1164
  h('div', { className: 'dsh-wt-source-heading' }, h('span', { id: heading }, t('sourceFolders')), folders.length > 0 && h('span', { className: 'dsh-wt-source-count', 'aria-live': 'polite' }, folders.length + ' / 32')),
1025
1165
  h('section', { className: 'dsh-wt-source-card', 'aria-labelledby': heading, 'data-filled': !!folders.length }, folders.length > 0 && h(FolderRows, { folders, remove, disabled: busy, t }),
1026
- h('div', { className: 'dsh-wt-source-add' }, h('select', { className: 'dsh-wt-source-select', 'aria-label': t('addFolder'), value: addMode, disabled: busy || folders.length >= 32, onChange: event => { setAddMode(event.target.value); setError(''); } }, h('option', { value: 'browse' }, t('localFolders')), h('option', { value: 'native' }, t('chooseDirectory')), h('option', { value: 'path' }, t('enterFolderPath'))),
1166
+ h('div', { className: 'dsh-wt-source-add' }, h('select', { className: 'dsh-wt-source-select', 'aria-label': t('addFolder'), value: addMode, disabled: !capabilities.ready || busy || folders.length >= 32, onChange: event => { setAddMode(event.target.value); setError(''); } }, capabilities.modes.includes('browse') && h('option', { value: 'browse' }, t('localFolders')), capabilities.modes.includes('native') && h('option', { value: 'native' }, t('chooseDirectory')), capabilities.modes.includes('path') && h('option', { value: 'path' }, t('enterFolderPath'))),
1167
+ !capabilities.ready && h('p', { role: 'status', className: 'dsh-wt-muted' }, t('checkingFolderOptions')),
1027
1168
  addMode === 'path' && h('label', { className: 'dsh-wt-project-path' }, h('input', { value: path, autoFocus: true, placeholder: t('absolutePath'), 'aria-label': t('absolutePath'), disabled: busy || folders.length >= 32, onChange: event => setPath(event.target.value), onKeyDown: event => { if (event.key === 'Enter' && !event.nativeEvent?.isComposing) { event.preventDefault(); void choose(); } } })),
1028
1169
  h(Button, { className: 'dsh-wt-add-pill', disabled: busy || folders.length >= 32 || (addMode === 'path' && !absolutePath(path.trim())), onClick: () => { void choose(); } }, h(FolderIcon, { plus: true }), t('add')))),
1029
1170
  folders.length > 1 && h('label', { className: 'dsh-wt-main-folder' }, t('mainFolder'), h('select', { required: true, value: primary, disabled: busy, title: primary, onChange: event => { setMainFolder(event.target.value); setError(''); } }, h('option', { value: '', disabled: true }, t('chooseMainFolder')), ...folders.map(folder => h('option', { key: folder, value: folder }, projectFolderName(folder) + ' · ' + folder))), h('small', null, t('mainFolderHelp'))),
@@ -1064,9 +1205,8 @@ window.__ModuleLoader__.load({
1064
1205
  (searching || !collapsed) && h('div', { role: 'group' }, ...rows.map(row => h(ThreadRow, { key: row.id, row, selected: row.id === selectedId, runtime, metadata, renderSlot, t, now })),
1065
1206
  !searching && hasOverflow && h(Button, { className: 'dsh-wt-session-overflow', 'aria-expanded': remaining === 0, onClick: () => setLimit(value => remaining === 0 ? 5 : remaining <= 5 ? Infinity : value + 5) }, remaining ? t('showMore', { n: remaining }) : t('showLess'))));
1066
1207
  }
1067
- function SidebarToggle({ sidebarMode, wide, t }) {
1068
- const native = React.useSyncExternalStore(sidebarMode.store.subscribe, sidebarMode.store.getSnapshot);
1069
- return h('span', { className: 'dsh-wt' }, h(Styles), h(Button, { title: t(native ? 'projects' : 'folderView'), 'aria-label': t(native ? 'projects' : 'folderView'), 'aria-pressed': !native, onClick: sidebarMode.toggle }, h(WorkspaceFolderIcon), wide && ' ' + t(native ? 'projects' : 'folderView')));
1208
+ function SidebarToggle({ size }) {
1209
+ return h(WorkspaceFolderIcon, { size });
1070
1210
  }
1071
1211
  function ProjectSidebarToolbar({ query, onQuery, archived, onArchived, onCreate, newProjectTrigger, t }) {
1072
1212
  const [searchOpen, setSearchOpen] = React.useState(!!query), [optionsOpen, setOptionsOpen] = React.useState(false);
@@ -1105,13 +1245,51 @@ window.__ModuleLoader__.load({
1105
1245
  h('div', { className: 'dsh-wt-project-list', role: 'tree', 'aria-label': t('projects') }, ...groups.map(group => h(ProjectGroup, { key: group.project?.id || 'unassigned', group, selectedId, runtime: ctx, metadata, renderSlot, t, searching: !!query.trim(), onManage: openEditor, now }))),
1106
1246
  !metadata.projects.length && !metadata.loading && h('p', { className: 'dsh-wt-muted' }, t('noProjects')), editor && h(ProjectEditor, { key: editor.project?.id || 'new-project', project: editor.project, projects, runtime: ctx, t, close: () => setEditor(null), returnFocus: editorTrigger, fallbackFocus: newProjectTrigger }));
1107
1247
  }
1248
+ function ChevronIcon() { return h('svg', { width: 12, height: 12, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, 'aria-hidden': true }, h('path', { d: 'm4 6 4 4 4-4' })); }
1249
+ function menuKeys(event) {
1250
+ if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
1251
+ const items = [...event.currentTarget.querySelectorAll('[role^=menuitem]:not(:disabled)')]; if (!items.length) return;
1252
+ event.preventDefault(); const at = items.indexOf(event.target);
1253
+ items[event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : (at + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length].focus();
1254
+ }
1255
+ function ChoicePicker({ label, value, items, disabled, onChange, icon }) {
1256
+ const [open, setOpen] = React.useState(false), root = React.useRef(null), trigger = React.useRef(null), id = React.useId();
1257
+ React.useEffect(() => { if (!open) return; const menu = root.current.querySelector('[role=menu]'); (menu?.querySelector('[aria-checked=true]:not(:disabled)') || menu?.querySelector('[role=menuitemradio]:not(:disabled)'))?.focus(); }, [open]);
1258
+ const dismiss = () => { setOpen(false); trigger.current?.focus(); }, current = items.find(item => item.value === value), selectable = items.filter(item => !item.disabled).length > 1;
1259
+ return h('div', { ref: root, className: 'dsh-wt-choice', onBlur: event => { if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false); }, onKeyDown: event => { if (event.key === 'Escape' && open) { event.preventDefault(); event.stopPropagation(); dismiss(); } } },
1260
+ h(Button, { ref: trigger, className: 'dsh-wt-control', disabled, 'aria-label': label, 'aria-haspopup': selectable ? 'menu' : undefined, 'aria-expanded': selectable ? open : undefined, 'aria-controls': open ? id : undefined, title: current?.title, onClick: () => selectable ? setOpen(!open) : onChange(value) }, icon, h('span', { className: 'dsh-wt-control-text' }, current?.label || label), selectable && h(ChevronIcon)),
1261
+ open && h('div', { id, role: 'menu', 'aria-label': label, className: 'dsh-wt-choice-menu', onKeyDown: menuKeys }, ...items.map(item => h(Button, { key: item.value, role: 'menuitemradio', 'aria-checked': value === item.value, disabled: item.disabled, className: 'dsh-wt-choice-item', title: item.title, onClick: () => { onChange(item.value); dismiss(); } }, h('span', { className: 'dsh-wt-choice-check', 'aria-hidden': true }, value === item.value ? '✓' : ''), item.label))));
1262
+ }
1263
+ function BranchPicker({ state, sessionId, flow, disabled, t, currentBranch }) {
1264
+ const [open, setOpen] = React.useState(false), root = React.useRef(null), trigger = React.useRef(null), search = React.useRef(null), id = React.useId();
1265
+ const selected = state.sessionId === sessionId, local = !selected || state.branchSource === 'local', branch = selected ? state.branch || (local ? currentBranch : '') : currentBranch;
1266
+ const label = branch ? (local ? branch : state.remote + '/' + branch) : t('select');
1267
+ const dismiss = () => { setOpen(false); trigger.current?.focus(); };
1268
+ const matches = (selected && state.branches?.items || []).filter(item => item.name.toLocaleLowerCase().includes((state.query || '').toLocaleLowerCase()));
1269
+ React.useEffect(() => { if (open) search.current?.focus(); }, [open]);
1270
+ function refreshBranches() {
1271
+ if (disabled || state.loading || !selected || !state.status) return;
1272
+ // Keep focus on a stable enabled child before Refresh becomes disabled.
1273
+ // Otherwise Chromium can emit blur(null), which is not an outside action.
1274
+ search.current?.focus(); void flow.loadBranches(sessionId, true);
1275
+ }
1276
+ return h('div', { ref: root, className: 'dsh-wt-choice', onBlur: event => { if (event.currentTarget.contains(event.relatedTarget)) return; if (!event.relatedTarget && !disabled && flow.store.getSnapshot().loading) { search.current?.focus(); return; } setOpen(false); }, onKeyDown: event => { if (event.key === 'Escape' && open) { event.preventDefault(); event.stopPropagation(); dismiss(); } } },
1277
+ h(Button, { ref: trigger, className: 'dsh-wt-control', disabled, 'aria-expanded': open, 'aria-haspopup': 'dialog', 'aria-controls': open ? id : undefined, 'aria-label': t('branch') + ': ' + label, title: t(local ? 'localBranchHelp' : 'remoteBranchHelp'), onClick: () => { setOpen(!open); if (!open) void flow.configure(sessionId); } }, h(Icon, { size: 14 }), h('span', { className: 'dsh-wt-control-text' }, label), h(ChevronIcon)),
1278
+ open && h('div', { id, role: 'dialog', 'aria-label': t('branch'), className: 'dsh-wt-base-options' },
1279
+ h('div', { className: 'dsh-wt-row dsh-wt-between' }, h('span', { className: 'dsh-wt-branch-heading' }, t('branch')), h(Button, { className: 'dsh-wt-control', disabled: disabled || state.loading || !selected || !state.status, onClick: refreshBranches }, t('refresh'))),
1280
+ h('div', { role: 'radiogroup', 'aria-label': t('branchSource'), className: 'dsh-wt-branch-sources' }, ...['local', 'remote'].map(source => h(Button, { key: source, role: 'radio', 'aria-checked': local === (source === 'local'), disabled: disabled || !state.status || (source === 'remote' && !state.status.remotes.length), onClick: () => flow.setBranchSource(sessionId, source) }, t(source === 'local' ? 'localBranches' : 'remoteBranches')))),
1281
+ !local && state.status?.remotes.length > 1 && h(ChoicePicker, { label: t('remote'), value: state.remote, disabled, items: state.status.remotes.map(item => ({ value: item.name, label: item.name })), onChange: remote => flow.setRemote(sessionId, remote) }),
1282
+ h('label', { className: 'dsh-wt-branch-search' }, h(SidebarToolbarIcon, { kind: 'search', size: 14 }), h('input', { ref: search, type: 'search', placeholder: t('search'), 'aria-label': t('search'), value: selected ? state.query : '', disabled, onChange: event => flow.setQuery(event.target.value), onKeyDown: event => { if (event.key === 'ArrowDown') { event.preventDefault(); root.current.querySelector('[role=menuitemradio]:not(:disabled)')?.focus(); } } })),
1283
+ h('div', { role: 'menu', 'aria-label': t(local ? 'localBranches' : 'remoteBranches'), className: 'dsh-wt-branch-list', 'aria-busy': state.loading, onKeyDown: menuKeys }, state.loading ? h('div', { role: 'status', className: 'dsh-wt-branch-help' }, t('loading')) : matches.length ? matches.map(item => h(Button, { key: item.name, role: 'menuitemradio', 'aria-checked': branch === item.name, disabled, className: 'dsh-wt-branch-item', title: item.name, onClick: () => { if (flow.setBranch(item.name)) dismiss(); } }, h('span', { className: 'dsh-wt-choice-check', 'aria-hidden': true }, branch === item.name ? '✓' : ''), h('span', { className: 'dsh-wt-branch-name' }, item.name === 'HEAD' && local ? t('detached') + ' · HEAD' : item.name), local && item.name === (state.status?.repository.branch || 'HEAD') && h('span', { className: 'dsh-wt-branch-badge' }, t('currentBranch')))) : h('div', { className: 'dsh-wt-branch-help', role: 'status' }, t('noBranches'))),
1284
+ h('div', { className: 'dsh-wt-branch-help' }, t(local ? 'localBranchHelp' : 'remoteBranchHelp'))));
1285
+ }
1108
1286
  function NewWorktreeControls({ sessionId, flow, projects, repositories, runtime: ctx, t, useSession, useInput, usePanelInfo, useWorkspaces }) {
1109
1287
  const snapshot = useSession(value => value), empty = useInput(inputEmpty), ready = sourceEligible(snapshot), panel = usePanelInfo(value => value.activePanelId), workspaces = useWorkspaces(value => value);
1110
1288
  const state = React.useSyncExternalStore(flow.store.subscribe, flow.store.getSnapshot);
1111
1289
  React.useSyncExternalStore(repositories.store.subscribe, repositories.store.getSnapshot);
1112
1290
  const metadata = React.useSyncExternalStore(projects.store.subscribe, projects.store.getSnapshot), context = projects.context(sessionId);
1113
- const [options, setOptions] = React.useState(false), [folderError, setFolderError] = React.useState('');
1114
- React.useEffect(() => { setOptions(false); if (panel || !snapshot.blank || snapshot.subagent !== null) return; try { flow.observe(sessionId); } catch { /* New mode reports the guarded ABI refusal. */ } void repositories.ensure(sessionId); return () => repositories.withdraw(sessionId); }, [flow, repositories, sessionId, ready, panel, snapshot.blank, snapshot.subagent, snapshot.openState, snapshot.removed, context?.project.id, context?.folder.id, context?.folder.path]);
1291
+ const [folderError, setFolderError] = React.useState('');
1292
+ React.useEffect(() => { if (panel || !snapshot.blank || snapshot.subagent !== null) return; try { flow.observe(sessionId); } catch { /* New mode reports the guarded ABI refusal. */ } void repositories.ensure(sessionId); return () => repositories.withdraw(sessionId); }, [flow, repositories, sessionId, ready, panel, snapshot.blank, snapshot.subagent, snapshot.openState, snapshot.removed, context?.project.id, context?.folder.id, context?.folder.path]);
1115
1293
  if (panel || !snapshot.blank || snapshot.subagent !== null) return null;
1116
1294
  const backing = projectBacking(metadata, workspaces, sessionId), worktree = backing.mode === 'worktree', record = backing.record;
1117
1295
  const selected = state.sessionId === sessionId, busy = selected && state.busy, main = projectMainFolder(context?.project), canWorktree = repositories.enabled(sessionId), blockedNew = selected && state.mode === 'new' && (!canWorktree || state.sourceChanged);
@@ -1119,19 +1297,13 @@ window.__ModuleLoader__.load({
1119
1297
  context?.project.folders.length > 1 && h('select', { className: 'dsh-wt-conversation-folder', 'aria-label': t('conversationFolder'), value: context.folder.id, disabled: busy || !empty || !sourceEligible(snapshot), title: !empty ? t('draftWarning') : context.folder.path,
1120
1298
  onChange: event => { setFolderError(''); try { chooseConversationFolder(ctx, projects, flow, sessionId, event.target.value); } catch (failure) { setFolderError(report(t, failure)); } } }, ...context.project.folders.map(folder => h('option', { key: folder.id, value: folder.id, title: folder.path }, folder.title + (folder.id === main?.id ? ' · ' + t('main') : '')))),
1121
1299
  folderError && h('span', { className: 'dsh-wt-create-error', role: 'alert' }, folderError),
1122
- !blockedNew && (canWorktree || worktree) ? h('select', { 'aria-label': t('threadMode'), value: canWorktree && selected && state.mode === 'new' ? 'new' : worktree ? 'worktree' : 'local', disabled: busy || !sourceEligible(snapshot),
1123
- onChange: event => { setOptions(false); flow.select(sessionId, event.target.value); } },
1124
- h('option', { value: 'local', disabled: worktree && !context?.folder }, t('local')),
1125
- worktree && h('option', { value: 'worktree' }, t('worktreeThread')),
1126
- canWorktree && h('option', { value: 'new' }, t('new'))) : h(Button, { 'aria-label': t(worktree && !context ? 'keepCurrentFolder' : 'local'), 'aria-pressed': !selected || state.mode === 'local', disabled: busy || !sourceEligible(snapshot), onClick: () => flow.select(sessionId, 'local') }, t(worktree && !context ? 'keepCurrentFolder' : 'local')),
1300
+ h(ChoicePicker, { key: sessionId + ':' + (blockedNew ? 'blocked' : canWorktree ? 'git' : 'local'), label: t('threadMode'), value: !blockedNew && canWorktree && selected && state.mode === 'new' ? 'new' : worktree ? 'worktree' : 'local', disabled: busy || !sourceEligible(snapshot), icon: h(FolderIcon, { size: 14 }), items: [
1301
+ { value: 'local', label: t('local'), disabled: worktree && !context?.folder },
1302
+ ...(worktree ? [{ value: 'worktree', label: t('worktreeThread') }] : []),
1303
+ ...(!blockedNew && canWorktree ? [{ value: 'new', label: t('new') }] : []),
1304
+ ], onChange: mode => flow.select(sessionId, mode) }),
1127
1305
  worktree && h('span', { className: 'dsh-wt-muted', title: backing.effectiveCwd }, record?.branch || t('worktreeThread')),
1128
- !blockedNew && canWorktree && !worktree && h(Button, { disabled: busy || !sourceEligible(snapshot), 'aria-expanded': options, 'aria-label': t('branch'), onClick: () => { setOptions(value => !value); if (!options) void flow.configure(sessionId); } }, selected && state.branch ? state.remote + '/' + state.branch : t('select')),
1129
- !blockedNew && canWorktree && !worktree && options && h('fieldset', { className: 'dsh-wt-base-options', onKeyDown: event => { if (event.key === 'Escape') { event.stopPropagation(); setOptions(false); } } }, h('legend', null, t('branch')),
1130
- selected && state.status?.remotes.length > 1 && h('select', { 'aria-label': t('remote'), disabled: busy || state.loading, value: state.remote, onChange: event => flow.setRemote(sessionId, event.target.value) }, ...state.status.remotes.map(item => h('option', { key: item.name, value: item.name }, item.name))),
1131
- h('select', { 'aria-label': t('branch'), value: selected ? state.branch : '', disabled: !selected || state.loading || busy, onChange: event => flow.setBranch(event.target.value) }, h('option', { value: '' }, t(state.loading ? 'loading' : 'select')), ...(selected && state.branches?.items || []).map(item => h('option', { key: item.name, value: item.name }, item.name))),
1132
- h('input', { type: 'search', 'aria-label': t('search'), value: selected ? state.query : '', disabled: !selected || busy || !state.remote, onChange: event => { void flow.loadBranches(sessionId, event.target.value); } }),
1133
- selected && state.branches?.nextCursor && h(Button, { disabled: state.loading || busy, onClick: () => { void flow.loadBranches(sessionId, state.query, true); } }, t('more')),
1134
- h(Button, { disabled: busy || state.loading, onClick: () => { void flow.configure(sessionId); } }, t('refresh')), h(Button, { onClick: () => setOptions(false) }, t('close'))),
1306
+ !blockedNew && canWorktree && !worktree && h(BranchPicker, { key: sessionId + ':' + (context?.folder.id || ''), state, sessionId, flow, disabled: busy || !sourceEligible(snapshot), t, currentBranch: repositories.store.getSnapshot().status?.repository.branch || 'HEAD' }),
1135
1307
  selected && state.error && h('span', { className: 'dsh-wt-create-error', role: 'alert' }, state.error,
1136
1308
  state.op && h(React.Fragment, null, ' · ', h('code', null, state.op.id), h(Button, { disabled: state.loading, onClick: () => { void flow.reconcile(sessionId); } }, t('reconcile')), state.op.result?.sessionId && h(Button, { onClick: () => flow.openTarget(sessionId) }, t('open')))));
1137
1309
  }
@@ -1143,7 +1315,7 @@ window.__ModuleLoader__.load({
1143
1315
  const completed = new Set(state.steps.filter(step => step.stage !== failed).map(step => step.stage));
1144
1316
  return h('section', { className: 'dsh-wt dsh-wt-progress', 'aria-label': t('setupProgress'), 'aria-busy': state.busy }, h(Styles),
1145
1317
  h('div', { className: 'dsh-wt-row dsh-wt-between' }, h('strong', { role: 'status', 'aria-live': 'polite' }, t(labels[active] || 'setupProgress')), state.busy && h(Button, { onClick: flow.cancel }, t('cancelSetup'))),
1146
- h('ol', null, ...['fetching', 'creating', 'naming'].map(stage => h('li', { key: stage, 'data-active': active === stage, 'aria-current': active === stage ? 'step' : undefined }, h('span', { 'aria-hidden': true }, failed === stage ? '× ' : completed.has(stage) && active !== stage ? '✓ ' : active === stage ? '◌ ' : '○ '), t(labels[stage])))),
1318
+ h('ol', null, ...((state.op?.intent.branchSource || state.branchSource) === 'local' ? ['creating', 'naming'] : ['fetching', 'creating', 'naming']).map(stage => h('li', { key: stage, 'data-active': active === stage, 'aria-current': active === stage ? 'step' : undefined }, h('span', { 'aria-hidden': true }, failed === stage ? '× ' : completed.has(stage) && active !== stage ? '✓ ' : active === stage ? '◌ ' : '○ '), t(labels[stage])))),
1147
1319
  state.error && h('p', { role: 'alert', className: 'dsh-wt-error' }, state.error),
1148
1320
  !state.busy && state.op && h('div', { className: 'dsh-wt-row' }, h('code', null, state.op.id), h(Button, { onClick: () => { void flow.reconcile(sessionId); } }, t('reconcile')), state.op.result?.sessionId && h(Button, { onClick: () => flow.openTarget(sessionId) }, t('open'))));
1149
1321
  }
@@ -1385,7 +1557,7 @@ window.__ModuleLoader__.load({
1385
1557
  sidebarMode = createSidebarMode(() => seat('sidebar.workspaces', { name: 'sidebar.workspaces', priority: -50, children: projectChildren(ctx) }, ProjectsSidebar));
1386
1558
  ctx.effect(() => () => sidebarMode.dispose());
1387
1559
  for (const source of projectSlots) mirrorProjectSlot(ctx, source);
1388
- seat('sidebar.footer.action', { name: 'sidebar.footer.action', id: PANEL + '-projects-view', order: 450 }, SidebarToggle);
1560
+ registerSidebarViewSwitch(ctx, sidebarMode, SidebarToggle);
1389
1561
  seat('conversation.input.left', { name: 'conversation.input.left', id: PANEL, order: 15 }, NewWorktreeControls);
1390
1562
  seat('conversation.input.dock', { name: 'conversation.input.dock', id: PANEL + '-setup', order: 5 }, SetupProgress);
1391
1563
  seat('plugins.row.config', { name: 'plugins.row.config', key: '@1yefuwang1/dsh-worktrees#git-worktrees' }, RootFolderSettings);