From 04b984a246dd3554e955db9ef4bdb94cab8e216a Mon Sep 17 00:00:00 2001 From: Aisland Date: Wed, 19 Aug 2026 21:48:32 +0800 Subject: [PATCH] feat: custom pane defaults to send-to-session mode (order swapped) + native folder-picker for project folder --- 01_content/src/client/index.tsx | 98 ++++++---------- 01_content/src/client/locales.ts | 18 ++- 01_content/src/client/split.tsx | 4 +- 01_content/src/client/styles.ts | 2 + .../worklogs/2026-08-16_worktable-v2.md | 16 +++ 04_test/functional-diag.cjs | 41 ++++--- 04_test/pickdir-probe.cjs | 59 ++++++++++ 04_test/probe-batch7.cjs | 109 ++++++++++++++++++ 8 files changed, 249 insertions(+), 98 deletions(-) create mode 100644 04_test/pickdir-probe.cjs create mode 100644 04_test/probe-batch7.cjs diff --git a/01_content/src/client/index.tsx b/01_content/src/client/index.tsx index 18bcc58..dd46baa 100644 --- a/01_content/src/client/index.tsx +++ b/01_content/src/client/index.tsx @@ -586,20 +586,15 @@ function WorktableSection(props: any) { const [wsPreset, setWsPreset] = useState('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) }} />
{t('add.folderParent')} - { setWsFolderParent(e.target.value); setWsFolderError(false) }} /> -
-
- {t('add.folderName')} - { setWsFolderName(e.target.value); setWsFolderError(false) }} /> + + {wsFolderParent || t('add.folderNone')} + +
@@ -1728,32 +1717,11 @@ function buildCustomLayoutPrompt(req: string): string {
📁 {t('bind.folder')} - + +
+
+ {projects.folders[bindPick.id] ?? t('bind.folderNone')}
- {bindFolderEdit ? ( -
- { setBindFolderParent(e.target.value); setBindFolderError(false) }} /> - { setBindFolderName(e.target.value); setBindFolderError(false) }} /> - - {bindFolderError &&

{t('add.folderRequired')}

} -
- ) : ( -
- {projects.folders[bindPick.id] ?? t('bind.folderNone')} -
- )}
{(() => { const sid = projects.bindings[bindPick.id] diff --git a/01_content/src/client/locales.ts b/01_content/src/client/locales.ts index 7222072..5f2720d 100644 --- a/01_content/src/client/locales.ts +++ b/01_content/src/client/locales.ts @@ -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', diff --git a/01_content/src/client/split.tsx b/01_content/src/client/split.tsx index 0a18f8b..f5123f0 100644 --- a/01_content/src/client/split.tsx +++ b/01_content/src/client/split.tsx @@ -1330,7 +1330,7 @@ function CustomPane(props: { paneTitle?: string }) { const [requirement, setRequirement] = useState('') const [projectId, setProjectId] = useState(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(null) // 分组(宿主工作区)三态:未分组 / 现有组 / 新建组 @@ -1408,8 +1408,8 @@ function CustomPane(props: { paneTitle?: string }) {
✨ {T('custom.title')}
- +

{mode === 'new' ? T('custom.hint') : T('custom.hintSend')}