feat: group option in new-conversation pane (join existing / create new workspace via mkdir route)
This commit is contained in:
@@ -263,7 +263,7 @@ function findSidebar(start: HTMLElement | null): HTMLElement | null {
|
||||
const registryStore: { ids: string[]; listeners: Set<() => void> } = { ids: [], listeners: new Set() }
|
||||
|
||||
/** 自定义窗口 → 宿主会话桥(apply 时注入;不可用时 CustomPane 降级提示) */
|
||||
let sessionBridge: { sessions: any; conversation: any; list: any } | null = null
|
||||
let sessionBridge: { sessions: any; conversation: any; list: any; workspaces: any } | null = null
|
||||
|
||||
/** 把文本送入指定会话:宿主同款寻址(binding(id).session.prompt / sendSession(会话面)),
|
||||
* 插件是根级上下文,无作用域的 conversation.send 会报 requires a session scope,不可用。 */
|
||||
@@ -299,8 +299,11 @@ async function promptIntoSession(sessionId: string, text: string): Promise<void>
|
||||
throw new Error('no send path: session face unavailable')
|
||||
}
|
||||
|
||||
/** 自定义窗口任务:新建一个专属会话,注入工作台背景 + 用户需求,并打开该会话 */
|
||||
export async function createCustomSession(projectId: string, projectName: string, requirement: string): Promise<void> {
|
||||
/** 新建会话的分组选择:未分组 / 加入现有分组 / 新建一个分组(父目录 + 名称) */
|
||||
export type NewSessionGroup = { kind: 'none' } | { kind: 'existing'; workspaceId: string } | { kind: 'new'; parent: string; name: string }
|
||||
|
||||
/** 自定义窗口任务:新建一个专属会话(可选指定分组),注入工作台背景 + 用户需求,并打开该会话 */
|
||||
export async function createCustomSession(projectId: string, projectName: string, requirement: string, group?: NewSessionGroup): Promise<void> {
|
||||
const b = sessionBridge
|
||||
if (!b || typeof b.sessions?.create !== 'function') throw new Error('sessions unavailable')
|
||||
const text = [
|
||||
@@ -316,7 +319,30 @@ export async function createCustomSession(projectId: string, projectName: string
|
||||
'4. 请基于以上背景,帮助用户完成这个窗口的自定义设计(例如产出可直接放入窗口的内容、页面或实现方案);',
|
||||
' 该窗口位于「' + projectName + '」项目内,如需要也可协助该项目后续的其他自定义工作。',
|
||||
].join('\n')
|
||||
const sessionId = await b.sessions.create({})
|
||||
// 分组解析:existing → 直接带 workspaceId 建会话;new → 先建目录并注册为工作区
|
||||
let workspaceId: string | null = null
|
||||
const g = group ?? { kind: 'none' as const }
|
||||
if (g.kind === 'existing') {
|
||||
workspaceId = g.workspaceId
|
||||
} else if (g.kind === 'new') {
|
||||
const ws = b.workspaces as any
|
||||
if (!ws || typeof ws.create !== 'function') throw new Error('workspaces unavailable')
|
||||
const parent = g.parent.replace(/[\\/]+$/, '')
|
||||
const name = g.name.replace(/^[\\/]+/, '').replace(/[\\/]+$/, '')
|
||||
if (!parent || !name) throw new Error('invalid group path')
|
||||
const full = parent + '\\' + name
|
||||
try { await ws.createDirectory?.(parent, name) } catch { /* 宿主 browse 能力本机为 native 时不可用 */ }
|
||||
try {
|
||||
// 兜底:插件服务端 mkdir(父目录必须已存在,避免误建深层目录)
|
||||
const r = await fetch('/api/worktable/mkdir', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path: full }) })
|
||||
const d = await r.json()
|
||||
if (!r.ok || !d?.ok) throw new Error(d?.error ?? 'mkdir failed')
|
||||
} catch { /* 目录建不出来的错误最终由 workspace.create 暴露 */ }
|
||||
const view = await ws.create({ path: full })
|
||||
workspaceId = view?.workspaceId ?? view?.id ?? null
|
||||
if (!workspaceId) throw new Error('workspace create failed')
|
||||
}
|
||||
const sessionId = await b.sessions.create(workspaceId ? { workspaceId } : {})
|
||||
try { await b.sessions.open?.(sessionId) } catch {}
|
||||
await promptIntoSession(sessionId, text)
|
||||
}
|
||||
@@ -499,7 +525,22 @@ function WorktableSection(props: any) {
|
||||
} catch { return { groups: [], current: '' } }
|
||||
},
|
||||
sendToSession: (sessionId, projectName, requirement) => sendCustomToSession(sessionId, projectName, requirement),
|
||||
submit: (projectId, projectName, requirement) => createCustomSession(projectId, projectName, requirement),
|
||||
submit: (projectId, projectName, requirement, group) => createCustomSession(projectId, projectName, requirement, group),
|
||||
// 分组(宿主工作区)数据源:读 ctx.workspaces 快照,供新建对话选分组
|
||||
getWorkspaces: () => {
|
||||
try {
|
||||
const snap = sessionBridge?.workspaces?.list?.getSnapshot?.()
|
||||
const items = snap?.items ?? []
|
||||
return items.map((w: any) => ({
|
||||
id: w.workspaceId ?? w.id,
|
||||
title: w.title ?? '',
|
||||
path: w.path ?? '',
|
||||
sessionIds: Array.isArray(w.sessionIds) ? w.sessionIds : [],
|
||||
}))
|
||||
} catch { return [] }
|
||||
},
|
||||
createWorkspace: (path: string) => sessionBridge?.workspaces?.create?.({ path }),
|
||||
createWorkspaceDir: (parent: string, name: string) => sessionBridge?.workspaces?.createDirectory?.(parent, name),
|
||||
},
|
||||
})
|
||||
return () => setSplitEnv(null)
|
||||
@@ -1410,12 +1451,12 @@ function WorktableSection(props: any) {
|
||||
)
|
||||
}
|
||||
|
||||
export const inject = ['slots', 'locale', 'sessions', 'conversation']
|
||||
export const inject = ['slots', 'locale', 'sessions', 'conversation', 'workspaces']
|
||||
|
||||
export function apply(ctx: any) {
|
||||
// 自定义窗口 → 宿主会话桥:保存 sessions/conversation/list 服务引用(模块级)
|
||||
sessionBridge = { sessions: ctx.sessions ?? null, conversation: ctx.conversation ?? null, list: ctx.sessions?.list ?? null }
|
||||
try { (window as any).__dshOpenSession = (id: string) => ctx.sessions?.open?.(id); (window as any).__dshSessions = ctx.sessions; (window as any).__dshPromptIntoSession = (id: string, text: string) => promptIntoSession(id, text) } catch {}
|
||||
sessionBridge = { sessions: ctx.sessions ?? null, conversation: ctx.conversation ?? null, list: ctx.sessions?.list ?? null, workspaces: ctx.workspaces ?? null }
|
||||
try { (window as any).__dshOpenSession = (id: string) => ctx.sessions?.open?.(id); (window as any).__dshSessions = ctx.sessions; (window as any).__dshPromptIntoSession = (id: string, text: string) => promptIntoSession(id, text); (window as any).__dshWorkspaces = ctx.workspaces } catch {}
|
||||
|
||||
|
||||
ctx.effect(() => {
|
||||
|
||||
@@ -73,6 +73,14 @@ export const zh = {
|
||||
'custom.done': '已新建专属对话',
|
||||
'custom.doneHint': 'AI 正在新会话中为你设计这个窗口。关闭本标签即可返回窗口选择。',
|
||||
'custom.fail': '创建对话失败',
|
||||
'custom.group': '分组(工作区)',
|
||||
'custom.groupNone': '未分组',
|
||||
'custom.groupNew': '+ 新建一个分组…',
|
||||
'custom.groupNewParent': '分组父目录',
|
||||
'custom.groupNewName': '新分组名',
|
||||
'custom.groupNewParentPh': '例如 E:\\AI_Workspace\\DeepseekHarness',
|
||||
'custom.groupNewNamePh': '例如 MyProject(将新建此文件夹并注册为分组)',
|
||||
'custom.groupNeedPath': '请填写分组父目录和新分组名',
|
||||
'pane.wip': '开发中(后续版本接入)',
|
||||
'pane.explorerNoCwd': '当前会话没有工作目录',
|
||||
'pane.openLater': '该类型暂不支持预览',
|
||||
@@ -169,6 +177,14 @@ export const en = {
|
||||
'custom.done': 'Conversation created',
|
||||
'custom.doneHint': 'The AI is designing your pane in the new conversation. Close this tab to return to the pane picker.',
|
||||
'custom.fail': 'Failed to create conversation',
|
||||
'custom.group': 'Group (workspace)',
|
||||
'custom.groupNone': 'Ungrouped',
|
||||
'custom.groupNew': '+ Create a new group…',
|
||||
'custom.groupNewParent': 'Group parent folder',
|
||||
'custom.groupNewName': 'New group name',
|
||||
'custom.groupNewParentPh': 'e.g. E:\\AI_Workspace\\DeepseekHarness',
|
||||
'custom.groupNewNamePh': 'e.g. MyProject (folder will be created and registered)',
|
||||
'custom.groupNeedPath': 'Fill in the parent folder and new group name',
|
||||
'pane.wip': 'Coming in a later version',
|
||||
'pane.explorerNoCwd': 'No working directory for the current session',
|
||||
'pane.openLater': 'Preview not supported for this type',
|
||||
|
||||
@@ -1299,19 +1299,29 @@ function CustomPane() {
|
||||
const [mode, setMode] = useState<'new' | 'existing'>('new')
|
||||
const [sessionGroups, setSessionGroups] = useState<{ title: string; sessions: { id: string; title: string; isCurrent: boolean }[] }[]>([])
|
||||
const [sessionId, setSessionId] = useState<string | null>(null)
|
||||
// 分组(宿主工作区)三态:未分组 / 现有组 / 新建组
|
||||
const [wsGroups, setWsGroups] = useState<{ id: string; title: string; path: string }[]>([])
|
||||
const [groupMode, setGroupMode] = useState<'none' | 'existing' | 'new'>('none')
|
||||
const [groupId, setGroupId] = useState<string | null>(null)
|
||||
const [newGroupParent, setNewGroupParent] = useState('')
|
||||
const [newGroupName, setNewGroupName] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [done, setDone] = useState(false)
|
||||
const [fail, setFail] = useState('')
|
||||
// 默认项目 = 当前工作区所属项目;默认会话 = 当前会话
|
||||
// 默认项目 = 当前工作区所属项目;默认会话 = 当前会话;默认分组 = 当前会话所在工作区
|
||||
useEffect(() => {
|
||||
const list = custom?.getProjects?.() ?? []
|
||||
setProjects(list)
|
||||
const cur = custom?.currentProjectId?.() ?? null
|
||||
setProjectId(cur && list.some((p) => p.id === cur) ? cur : (list[0]?.id ?? null))
|
||||
setWsGroups((custom?.getWorkspaces?.() ?? []).map((w) => ({ id: w.id, title: w.title, path: w.path })))
|
||||
custom?.getSessions?.().then((res) => {
|
||||
setSessionGroups(res.groups)
|
||||
const flat = res.groups.flatMap((g) => g.sessions)
|
||||
setSessionId(flat.find((s) => s.isCurrent)?.id ?? flat[0]?.id ?? null)
|
||||
const curId = flat.find((s) => s.isCurrent)?.id
|
||||
const home = curId ? (custom?.getWorkspaces?.() ?? []).find((w) => (w.sessionIds ?? []).includes(curId)) : null
|
||||
if (home) { setGroupMode('existing'); setGroupId(home.id) }
|
||||
}).catch(() => { setSessionGroups([]) })
|
||||
}, [custom])
|
||||
const submit = async () => {
|
||||
@@ -1322,8 +1332,15 @@ function CustomPane() {
|
||||
try {
|
||||
const proj = projects.find((p) => p.id === projectId)
|
||||
const pname = proj?.name ?? projectId
|
||||
if (mode === 'new') await custom.submit(projectId, pname, text)
|
||||
else {
|
||||
if (mode === 'new') {
|
||||
let group: any = { kind: 'none' }
|
||||
if (groupMode === 'existing' && groupId) group = { kind: 'existing', workspaceId: groupId }
|
||||
else if (groupMode === 'new') {
|
||||
if (!newGroupParent.trim() || !newGroupName.trim()) { setFail(T('custom.groupNeedPath')); return }
|
||||
group = { kind: 'new', parent: newGroupParent.trim(), name: newGroupName.trim() }
|
||||
}
|
||||
await custom.submit(projectId, pname, text, group)
|
||||
} else {
|
||||
if (!sessionId) return
|
||||
await custom.sendToSession(sessionId, pname, text)
|
||||
}
|
||||
@@ -1385,10 +1402,54 @@ function CustomPane() {
|
||||
onChange={setProjectId}
|
||||
/>
|
||||
</div>
|
||||
{mode === 'new' && (
|
||||
<div className="dsh-wt_customRow">
|
||||
<span className="dsh-wt_customLabel">{T('custom.group')}</span>
|
||||
<SelectPop
|
||||
value={groupMode === 'none' ? '__none' : groupMode === 'new' ? '__new' : groupId}
|
||||
groups={[{
|
||||
title: '',
|
||||
items: [
|
||||
{ id: '__none', label: T('custom.groupNone') },
|
||||
...wsGroups.map((w) => ({ id: w.id, label: w.title })),
|
||||
{ id: '__new', label: T('custom.groupNew') },
|
||||
],
|
||||
}]}
|
||||
placeholder={T('custom.group')}
|
||||
onChange={(id) => {
|
||||
if (id === '__none') setGroupMode('none')
|
||||
else if (id === '__new') setGroupMode('new')
|
||||
else { setGroupMode('existing'); setGroupId(id) }
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mode === 'new' && groupMode === 'new' && (
|
||||
<>
|
||||
<div className="dsh-wt_customRow">
|
||||
<span className="dsh-wt_customLabel">{T('custom.groupNewParent')}</span>
|
||||
<input
|
||||
className="dsh-wt_customPathInput"
|
||||
placeholder={T('custom.groupNewParentPh')}
|
||||
value={newGroupParent}
|
||||
onChange={(e) => setNewGroupParent(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="dsh-wt_customRow">
|
||||
<span className="dsh-wt_customLabel">{T('custom.groupNewName')}</span>
|
||||
<input
|
||||
className="dsh-wt_customPathInput"
|
||||
placeholder={T('custom.groupNewNamePh')}
|
||||
value={newGroupName}
|
||||
onChange={(e) => setNewGroupName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="dsh-wt_customSend"
|
||||
disabled={busy || !requirement.trim() || !projectId || (mode === 'existing' && !sessionId)}
|
||||
disabled={busy || !requirement.trim() || !projectId || (mode === 'existing' && !sessionId) || (mode === 'new' && groupMode === 'new' && (!newGroupParent.trim() || !newGroupName.trim()))}
|
||||
onClick={submit}
|
||||
>{busy ? '…' : (mode === 'new' ? T('custom.send') : T('custom.sendToSession'))}</button>
|
||||
{fail && <p className="dsh-wt_customFail">{T('custom.fail')}:{fail}</p>}
|
||||
|
||||
@@ -201,6 +201,9 @@ export const css = [
|
||||
'.dsh-wt_customModeBtnOn{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7);color:var(--dsw-alias-state-accent-primary,#4f8ef7);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}',
|
||||
'.dsh-wt_customInput{width:100%;box-sizing:border-box;min-height:96px;padding:8px 10px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:18px;outline:none;resize:vertical}',
|
||||
'.dsh-wt_customInput:focus{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
// 新建分组:父目录 / 名称单行输入
|
||||
'.dsh-wt_customPathInput{flex:1;min-width:0;box-sizing:border-box;padding:5px 8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:18px;outline:none}',
|
||||
'.dsh-wt_customPathInput:focus{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
'.dsh-wt_customRow{display:flex;align-items:center;gap:8px}',
|
||||
'.dsh-wt_customLabel{flex:none;font-size:11.5px;color:var(--dsw-alias-label-secondary,#9aa4b2)}',
|
||||
'.dsh-wt_customSelect{flex:1;min-width:0;padding:5px 8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;outline:none}',
|
||||
|
||||
Reference in New Issue
Block a user