feat: custom pane defaults to send-to-session mode (order swapped) + native folder-picker for project folder

This commit is contained in:
Aisland
2026-08-19 21:48:32 +08:00
parent ce449c6620
commit 04b984a246
8 changed files with 249 additions and 98 deletions
+33 -65
View File
@@ -586,20 +586,15 @@ function WorktableSection(props: any) {
const [wsPreset, setWsPreset] = useState<string>('2h')
const [wsName, setWsName] = useState('')
const [wsError, setWsError] = useState(false)
// 新建项目强制工作文件夹:父目录(必填)+ 名称(留空 = 用项目名)
// 新建项目强制工作文件夹:经「选择位置…」弹窗选定(系统资源管理器式选择窗)
const [wsFolderParent, setWsFolderParent] = useState('')
const [wsFolderName, setWsFolderName] = useState('')
const [wsFolderError, setWsFolderError] = useState(false)
/** 图标选择器:kind + 目标 id + 弹窗锚点坐标(fixed 定位) */
const [iconPick, setIconPick] = useState<{ kind: 'layout' | 'shortcut' | 'project'; id: string; x: number; y: number } | null>(null)
/** 对话绑定弹窗:项目 id + 锚点坐标;bindGroups = 打开时抓取的会话分组 */
const [bindPick, setBindPick] = useState<{ id: string; x: number; y: number } | null>(null)
const [bindGroups, setBindGroups] = useState<{ title: string; sessions: { id: string; title: string; isCurrent: boolean }[] }[]>([])
/** 绑定弹窗内的项目文件夹编辑区 */
const [bindFolderEdit, setBindFolderEdit] = useState(false)
const [bindFolderParent, setBindFolderParent] = useState('')
const [bindFolderName, setBindFolderName] = useState('')
const [bindFolderError, setBindFolderError] = useState(false)
/** 自定义布局弹窗(预设网格末尾的 + 磁贴):描述 → 复制提示词到剪贴板 */
const [customOpen, setCustomOpen] = useState(false)
const [customLayoutText, setCustomLayoutText] = useState('')
@@ -912,26 +907,26 @@ function buildCustomLayoutPrompt(req: string): string {
const y = clamp(Math.round(r.top), 8, window.innerHeight - 420)
setBindPick({ id, x, y })
setBindGroups([])
setBindFolderEdit(false)
setBindFolderError(false)
fetchSessionGroups().then((res) => setBindGroups(res.groups)).catch(() => setBindGroups([]))
}, [])
/** 保存绑定弹窗里的项目文件夹(建目录 + 持久化) */
const saveBindFolder = async () => {
if (!bindPick) return
const parent = bindFolderParent.trim()
const name = bindFolderName.trim()
if (!parent || !name) { setBindFolderError(true); return }
const folderPath = parent.replace(/[\\/]+$/, '') + '\\' + name.replace(/[\\/]+/g, '')
/** 弹出系统文件夹选择窗(宿主 pickDirectory);选中后回调 */
const pickFolder = async (apply: (p: string) => void) => {
try {
const r = await fetch('/api/worktable/mkdir', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path: folderPath }) })
const d = await r.json()
if (!r.ok || !d?.ok) { setBindFolderError(true); return }
} catch { setBindFolderError(true); return }
persistProjects((prev) => ({ ...prev, folders: { ...prev.folders, [bindPick.id]: folderPath } }))
setBindFolderEdit(false)
setBindFolderError(false)
const ws = sessionBridge?.workspaces as any
if (ws && typeof ws.pickDirectory === 'function') {
const p = await ws.pickDirectory()
if (p && typeof p === 'string') apply(p)
}
} catch { /* 取消或不可用 */ }
}
/** 绑定弹窗「更改」:弹窗选择文件夹,直接作为项目文件夹 */
const changeBindFolder = () => {
if (!bindPick) return
pickFolder((p) => {
persistProjects((prev) => ({ ...prev, folders: { ...prev.folders, [bindPick.id]: p } }))
})
}
/** 绑定 / 解绑会话 */
@@ -1114,22 +1109,19 @@ function buildCustomLayoutPrompt(req: string): string {
persistProjects((prev) => ({ ...prev, shortcuts: prev.shortcuts.filter((s) => s.id !== id) }))
}
// ── 布局(新建工作区) ── 强制项目文件夹:父目录必填,文件夹名留空 = 用项目名;保存时建目录
// ── 布局(新建工作区) ── 强制项目文件夹:必须经「选择位置…」选定;路径即为项目文件夹
const saveLayout = async () => {
const name = wsName.trim()
if (!name) { setWsError(true); return }
const parent = wsFolderParent.trim()
if (!parent) { setWsFolderError(true); return }
const folderName = (wsFolderName.trim() || name).replace(/[\\/]+/g, '')
const folderPath = parent.replace(/[\\/]+$/, '') + '\\' + folderName
const folderPath = wsFolderParent.trim()
if (!folderPath) { setWsFolderError(true); return }
try {
const r = await fetch('/api/worktable/mkdir', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path: folderPath }) })
const d = await r.json()
if (!r.ok || !d?.ok) { setWsFolderError(true); return }
} catch { setWsFolderError(true); return }
// 轻量兜底:选中的路径理应存在,mkdir 幂等无害
await fetch('/api/worktable/mkdir', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path: folderPath }) })
} catch { /* 非致命 */ }
const layout = buildLayout(wsPreset, name)
persistProjects((prev) => ({ ...prev, layouts: [...prev.layouts, layout], folders: { ...prev.folders, [layout.id]: folderPath } }))
setWsName(''); setWsFolderParent(''); setWsFolderName(''); setWsError(false); setWsFolderError(false)
setWsName(''); setWsFolderParent(''); setWsError(false); setWsFolderError(false)
setAddOpen(false)
openSplit(layout)
reportUsed(layout.id)
@@ -1550,13 +1542,10 @@ function buildCustomLayoutPrompt(req: string): string {
onChange={(e) => { setWsName(e.target.value); setWsError(false) }} />
<div className="dsh-wt_addFolderRow">
<span className="dsh-wt_customLabel">{t('add.folderParent')}</span>
<input className="dsh-wt_customPathInput" type="text" placeholder={t('add.folderParentPh')} value={wsFolderParent}
onChange={(e) => { setWsFolderParent(e.target.value); setWsFolderError(false) }} />
</div>
<div className="dsh-wt_addFolderRow">
<span className="dsh-wt_customLabel">{t('add.folderName')}</span>
<input className="dsh-wt_customPathInput" type="text" placeholder={t('add.folderNamePh')} value={wsFolderName}
onChange={(e) => { setWsFolderName(e.target.value); setWsFolderError(false) }} />
<span className={'dsh-wt_addFolderPath' + (wsFolderParent ? '' : ' dsh-wt_addFolderPathNone')} title={wsFolderParent || ''}>
{wsFolderParent || t('add.folderNone')}
</span>
<button type="button" className="dsh-wt_bindFolderChange" onClick={() => { pickFolder((p) => { setWsFolderParent(p); setWsFolderError(false) }); setWsError(false) }}>{t('add.folderPick')}</button>
</div>
<button type="button" className="dsh-wt_addBtn" onClick={saveLayout}>{t('add.layoutSave')}</button>
</div>
@@ -1728,32 +1717,11 @@ function buildCustomLayoutPrompt(req: string): string {
<div className="dsh-wt_bindFolderBox">
<div className="dsh-wt_bindFolderRow">
<span className="dsh-wt_bindFolderLabel">📁 {t('bind.folder')}</span>
<button
type="button"
className="dsh-wt_bindFolderChange"
onClick={() => {
const cur = projects.folders[bindPick.id] ?? ''
setBindFolderParent(cur.replace(/[\\/][^\\/]*$/, ''))
setBindFolderName(cur.split(/[\\/]/).pop() ?? '')
setBindFolderEdit((v) => !v)
setBindFolderError(false)
}}
>{t('bind.folderChange')}</button>
<button type="button" className="dsh-wt_bindFolderChange" onClick={changeBindFolder}>{t('bind.folderChange')}</button>
</div>
<div className={'dsh-wt_bindFolderPath' + (projects.folders[bindPick.id] ? '' : ' dsh-wt_bindFolderPathNone')} title={projects.folders[bindPick.id] ?? ''}>
{projects.folders[bindPick.id] ?? t('bind.folderNone')}
</div>
{bindFolderEdit ? (
<div className="dsh-wt_bindFolderForm">
<input className="dsh-wt_customPathInput" type="text" placeholder={t('add.folderParentPh')} value={bindFolderParent}
onChange={(e) => { setBindFolderParent(e.target.value); setBindFolderError(false) }} />
<input className="dsh-wt_customPathInput" type="text" placeholder={t('add.folderNamePh')} value={bindFolderName}
onChange={(e) => { setBindFolderName(e.target.value); setBindFolderError(false) }} />
<button type="button" className="dsh-wt_customLayoutBtn" onClick={saveBindFolder}>{t('add.layoutSave')}</button>
{bindFolderError && <p className="dsh-wt_addError">{t('add.folderRequired')}</p>}
</div>
) : (
<div className={'dsh-wt_bindFolderPath' + (projects.folders[bindPick.id] ? '' : ' dsh-wt_bindFolderPathNone')} title={projects.folders[bindPick.id] ?? ''}>
{projects.folders[bindPick.id] ?? t('bind.folderNone')}
</div>
)}
</div>
{(() => {
const sid = projects.bindings[bindPick.id]
+8 -10
View File
@@ -40,11 +40,10 @@ export const zh = {
'shortcut.badge': '本地',
'add.chooseLayout': '选择布局模式',
'add.layoutNamePh': '布局名称',
'add.folderParent': '项目文件夹(父目录)',
'add.folderParentPh': '例如 E:\\AI_Workspace\\DeepseekHarness\\Projects',
'add.folderName': '文件夹名',
'add.folderNamePh': '留空 = 用项目名',
'add.folderRequired': '请填写项目文件夹的父目录(保存时将创建「父目录\项目名」)',
'add.folderParent': '项目文件夹',
'add.folderPick': '选择位置…',
'add.folderNone': '未选择(点右侧「选择位置…」在资源管理器窗口里挑一个文件夹)',
'add.folderRequired': '请先选择项目文件夹的位置',
'add.layoutSave': '进入工作区',
'add.layoutInvalid': '请填写布局名称',
'manage.deleteLayout': '删除布局',
@@ -171,11 +170,10 @@ export const en = {
'shortcut.badge': 'Local',
'add.chooseLayout': 'Choose a layout',
'add.layoutNamePh': 'Layout name',
'add.folderParent': 'Project folder (parent dir)',
'add.folderParentPh': 'e.g. E:\\AI_Workspace\\DeepseekHarness\\Projects',
'add.folderName': 'Folder name',
'add.folderNamePh': 'Blank = use project name',
'add.folderRequired': 'Fill in the project folder parent (the folder parent\\project-name will be created)',
'add.folderParent': 'Project folder',
'add.folderPick': 'Choose…',
'add.folderNone': 'Not chosen (click Choose… to pick a folder in the explorer window)',
'add.folderRequired': 'Pick the project folder location first',
'add.layoutSave': 'Enter workspace',
'add.layoutInvalid': 'Enter a layout name',
'manage.deleteLayout': 'Delete layout',
+2 -2
View File
@@ -1330,7 +1330,7 @@ function CustomPane(props: { paneTitle?: string }) {
const [requirement, setRequirement] = useState('')
const [projectId, setProjectId] = useState<string | null>(null)
const [projects, setProjects] = useState<{ id: string; name: string }[]>(() => custom?.getProjects?.() ?? [])
const [mode, setMode] = useState<'new' | 'existing'>('new')
const [mode, setMode] = useState<'new' | 'existing'>('existing')
const [sessionGroups, setSessionGroups] = useState<{ title: string; sessions: { id: string; title: string; isCurrent: boolean }[] }[]>([])
const [sessionId, setSessionId] = useState<string | null>(null)
// 分组(宿主工作区)三态:未分组 / 现有组 / 新建组
@@ -1408,8 +1408,8 @@ function CustomPane(props: { paneTitle?: string }) {
<div className="dsh-wt_customCard">
<span className="dsh-wt_customTitle">✨ {T('custom.title')}</span>
<div className="dsh-wt_customModes">
<button type="button" className={'dsh-wt_customModeBtn' + (mode === 'new' ? ' dsh-wt_customModeBtnOn' : '')} onClick={() => setMode('new')}>{T('custom.modeNew')}</button>
<button type="button" className={'dsh-wt_customModeBtn' + (mode === 'existing' ? ' dsh-wt_customModeBtnOn' : '')} onClick={() => setMode('existing')}>{T('custom.modeSend')}</button>
<button type="button" className={'dsh-wt_customModeBtn' + (mode === 'new' ? ' dsh-wt_customModeBtnOn' : '')} onClick={() => setMode('new')}>{T('custom.modeNew')}</button>
</div>
<p className="dsh-wt_customHint">{mode === 'new' ? T('custom.hint') : T('custom.hintSend')}</p>
<textarea
+2
View File
@@ -227,6 +227,8 @@ export const css = [
'.dsh-wt_customPathInput:focus{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
// 新建项目强制文件夹:两行(父目录/名称)
'.dsh-wt_addFolderRow{display:flex;align-items:center;gap:8px;margin-top:6px}',
'.dsh-wt_addFolderPath{flex:1;min-width:0;padding:4px 8px;border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));font-size:11.5px;line-height:17px;color:var(--dsw-alias-label-primary,#e6e8eb);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
'.dsh-wt_addFolderPathNone{color:var(--dsw-alias-label-tertiary,#6b7280);border-style:dashed}',
'.dsh-wt_addFolderRow .dsh-wt_customLabel{width:86px;text-align:right}',
// 绑定弹窗内的项目文件夹区
'.dsh-wt_bindFolderBox{margin-top:8px;padding:6px 8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px}',