@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/CHANGELOG.md +112 -0
- package/README.md +54 -24
- package/client.js +259 -87
- package/dist/context.js +6 -2
- package/dist/git.d.ts +4 -1
- package/dist/git.js +123 -19
- package/dist/index.js +1 -1
- package/dist/project-store.d.ts +2 -2
- package/dist/schema.d.ts +6 -0
- package/dist/schema.js +19 -4
- package/dist/service.d.ts +1 -1
- package/dist/service.js +45 -13
- package/dist/store.d.ts +2 -0
- package/dist/store.js +1 -0
- package/dist/types.d.ts +31 -5
- package/package.json +7 -2
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
|
|
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: '
|
|
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: '
|
|
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
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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:
|
|
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
|
-
|
|
231
|
-
|
|
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:
|
|
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
|
-
|
|
716
|
-
if (
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
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,
|
|
783
|
-
if (disposed || state.busy || state.sessionId !== sessionId || !state.status ||
|
|
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,
|
|
787
|
-
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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 });
|
|
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
|
-
|
|
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]) => [
|
|
864
|
-
const component = children ? props => h(entry.component, { ...props, renderSlot: (key, owner, options) => props.renderSlot(
|
|
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:
|
|
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), [
|
|
979
|
-
|
|
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
|
-
|
|
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 ||
|
|
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 ||
|
|
1124
|
+
if (submitter.current.pending || picking.current || browsing.current || confirming.current || currentFolders.current.length >= 32) return;
|
|
993
1125
|
setError('');
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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({
|
|
1068
|
-
|
|
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 [
|
|
1114
|
-
React.useEffect(() => {
|
|
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
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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(
|
|
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
|
-
|
|
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);
|