feat: 自定义窗口两种模式——新建专属对话 / 发送到已有会话(默认当前会话,可下拉选择)
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 } | null = null
|
||||
let sessionBridge: { sessions: any; conversation: any; list: any } | null = null
|
||||
|
||||
/** 自定义窗口任务:新建一个专属会话,注入工作台背景 + 用户需求,并打开该会话 */
|
||||
export async function createCustomSession(projectId: string, projectName: string, requirement: string): Promise<void> {
|
||||
@@ -291,6 +291,25 @@ export async function createCustomSession(projectId: string, projectName: string
|
||||
if (!sent) await b.conversation?.send?.(text)
|
||||
}
|
||||
|
||||
/** 把自定义需求直接发到用户选定的已有会话(默认当前会话) */
|
||||
export async function sendCustomToSession(sessionId: string, projectName: string, requirement: string): Promise<void> {
|
||||
const b = sessionBridge
|
||||
if (!b) throw new Error('bridge unavailable')
|
||||
const text = [
|
||||
'【工作台自定义窗口任务】',
|
||||
'以下内容由 dsh-worktable(工作台)插件发送:用户正在自定义「' + projectName + '」项目中的一个窗口。',
|
||||
'1. dsh-worktable 是侧边栏底部的自建「工作台」插件(官方文档没有它的说明,不了解之处可向用户提问);',
|
||||
'2. 用户需求:' + requirement,
|
||||
'3. 请帮助完成该窗口的自定义设计(内容/页面/实现方案),该窗口位于「' + projectName + '」项目内。',
|
||||
].join('\n')
|
||||
let sent = false
|
||||
if (typeof b.conversation?.sendSession === 'function') {
|
||||
try { await b.conversation.sendSession(sessionId, text); sent = true } catch { sent = false }
|
||||
}
|
||||
try { b.sessions.open?.(sessionId) } catch {}
|
||||
if (!sent) await b.conversation?.send?.(text)
|
||||
}
|
||||
|
||||
/** 会话作用域快照(模块级;apply 里订阅 ctx.sessions.list 写入,组件与引擎只读) */
|
||||
const sessionScopeStore: {
|
||||
snapshot: { sessionId: string; cwd: string; jobs: any[]; subagents: any[] } | null
|
||||
@@ -411,6 +430,19 @@ function WorktableSection(props: any) {
|
||||
]
|
||||
},
|
||||
currentProjectId: () => (splitStore.active && splitStore.spec ? splitStore.spec.id : null),
|
||||
getSessions: () => {
|
||||
try {
|
||||
const snap = sessionBridge?.list?.getSnapshot?.()
|
||||
const items = Array.isArray(snap?.items) ? snap.items : []
|
||||
const current = snap?.current ?? ''
|
||||
return items.map((it: any) => ({
|
||||
id: it?.sessionId ?? it?.id ?? '',
|
||||
title: it?.title ?? it?.name ?? it?.summary?.title ?? (it?.sessionId ?? it?.id ?? ''),
|
||||
isCurrent: (it?.sessionId ?? it?.id) === current,
|
||||
}))
|
||||
} catch { return [] }
|
||||
},
|
||||
sendToSession: (sessionId, projectName, requirement) => sendCustomToSession(sessionId, projectName, requirement),
|
||||
submit: (projectId, projectName, requirement) => createCustomSession(projectId, projectName, requirement),
|
||||
},
|
||||
})
|
||||
@@ -1325,8 +1357,8 @@ function WorktableSection(props: any) {
|
||||
export const inject = ['slots', 'locale', 'sessions', 'conversation']
|
||||
|
||||
export function apply(ctx: any) {
|
||||
// 自定义窗口 → 新建会话桥:保存宿主 sessions/conversation 服务引用(模块级)
|
||||
sessionBridge = { sessions: ctx.sessions ?? null, conversation: ctx.conversation ?? null }
|
||||
// 自定义窗口 → 宿主会话桥:保存 sessions/conversation/list 服务引用(模块级)
|
||||
sessionBridge = { sessions: ctx.sessions ?? null, conversation: ctx.conversation ?? null, list: ctx.sessions?.list ?? null }
|
||||
|
||||
ctx.effect(() => {
|
||||
const style = document.createElement('style')
|
||||
|
||||
@@ -62,7 +62,14 @@ export const zh = {
|
||||
'custom.hint': '发送后将自动新建一个会话,专门负责这个窗口的自定义设计。',
|
||||
'custom.placeholder': '告诉 AI 你想让这个窗口做什么,例如:做一个展示我旅行足迹的时间线页面…',
|
||||
'custom.project': '所属项目',
|
||||
'custom.modeNew': '新建对话',
|
||||
'custom.modeSend': '发送到会话',
|
||||
'custom.session': '选择会话',
|
||||
'custom.sessionCurrent': '(当前)',
|
||||
'custom.hintSend': '内容将直接发送到你选择的会话(默认当前会话),用于讨论这个窗口的自定义设计。',
|
||||
'custom.send': '发送并新建对话',
|
||||
'custom.sendToSession': '发送到该会话',
|
||||
'custom.sent': '已发送到会话',
|
||||
'custom.done': '已新建专属对话',
|
||||
'custom.doneHint': 'AI 正在新会话中为你设计这个窗口。关闭本标签即可返回窗口选择。',
|
||||
'custom.fail': '创建对话失败',
|
||||
@@ -151,7 +158,14 @@ export const en = {
|
||||
'custom.hint': 'Sending will create a new conversation dedicated to designing this pane.',
|
||||
'custom.placeholder': 'Tell the AI what you want this pane to do, e.g. a timeline page of my travels…',
|
||||
'custom.project': 'Project',
|
||||
'custom.modeNew': 'New conversation',
|
||||
'custom.modeSend': 'Send to a conversation',
|
||||
'custom.session': 'Conversation',
|
||||
'custom.sessionCurrent': ' (current)',
|
||||
'custom.hintSend': 'The content will be sent directly to the conversation you pick (current by default).',
|
||||
'custom.send': 'Send & create conversation',
|
||||
'custom.sendToSession': 'Send to this conversation',
|
||||
'custom.sent': 'Sent to the conversation',
|
||||
'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',
|
||||
|
||||
@@ -185,11 +185,13 @@ type SplitEnv = {
|
||||
getScope: () => SplitScope | null
|
||||
getJobs: () => SplitJob[]
|
||||
getSubagents: () => any[]
|
||||
/** 自定义窗口:项目列表 + 当前项目 + 提交创建专属会话 */
|
||||
/** 自定义窗口:项目列表 + 会话列表 + 当前项目 + 新建会话 / 发送到已有会话 */
|
||||
custom?: {
|
||||
getProjects: () => { id: string; name: string }[]
|
||||
currentProjectId: () => string | null
|
||||
getSessions: () => { id: string; title: string; isCurrent: boolean }[]
|
||||
submit: (projectId: string, projectName: string, requirement: string) => Promise<void>
|
||||
sendToSession: (sessionId: string, projectName: string, requirement: string) => Promise<void>
|
||||
}
|
||||
}
|
||||
let splitEnv: SplitEnv | null = null
|
||||
@@ -1243,21 +1245,27 @@ function TextViewer(props: { path: string; fileUrl: string; isMd: boolean }) {
|
||||
)
|
||||
}
|
||||
|
||||
/** 自定义窗口:居中对话框。用户写下需求 + 选择所属项目 → 发送 = 新建专属会话并由 AI 辅助设计。 */
|
||||
/** 自定义窗口:居中对话框。两种模式:新建专属会话 / 发送到已有会话(默认当前会话)。 */
|
||||
function CustomPane() {
|
||||
const custom = splitEnv?.custom
|
||||
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 [sessions, setSessions] = useState<{ id: string; title: string; isCurrent: boolean }[]>(() => custom?.getSessions?.() ?? [])
|
||||
const [sessionId, setSessionId] = useState<string | null>(null)
|
||||
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))
|
||||
const slist = custom?.getSessions?.() ?? []
|
||||
setSessions(slist)
|
||||
setSessionId(slist.find((s) => s.isCurrent)?.id ?? slist[0]?.id ?? null)
|
||||
}, [custom])
|
||||
const submit = async () => {
|
||||
const text = requirement.trim()
|
||||
@@ -1266,7 +1274,12 @@ function CustomPane() {
|
||||
setFail('')
|
||||
try {
|
||||
const proj = projects.find((p) => p.id === projectId)
|
||||
await custom.submit(projectId, proj?.name ?? projectId, text)
|
||||
const pname = proj?.name ?? projectId
|
||||
if (mode === 'new') await custom.submit(projectId, pname, text)
|
||||
else {
|
||||
if (!sessionId) return
|
||||
await custom.sendToSession(sessionId, pname, text)
|
||||
}
|
||||
setDone(true)
|
||||
} catch (e) {
|
||||
setFail(String(e))
|
||||
@@ -1281,7 +1294,7 @@ function CustomPane() {
|
||||
return (
|
||||
<div className="dsh-wt_customBox">
|
||||
<span className="dsh-wt_customDone" aria-hidden>✅</span>
|
||||
<p className="dsh-wt_customDoneText">{T('custom.done')}</p>
|
||||
<p className="dsh-wt_customDoneText">{mode === 'new' ? T('custom.done') : T('custom.sent')}</p>
|
||||
<p className="dsh-wt_customDoneHint">{T('custom.doneHint')}</p>
|
||||
</div>
|
||||
)
|
||||
@@ -1290,7 +1303,11 @@ function CustomPane() {
|
||||
<div className="dsh-wt_customBox">
|
||||
<div className="dsh-wt_customCard">
|
||||
<span className="dsh-wt_customTitle">✨ {T('custom.title')}</span>
|
||||
<p className="dsh-wt_customHint">{T('custom.hint')}</p>
|
||||
<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>
|
||||
</div>
|
||||
<p className="dsh-wt_customHint">{mode === 'new' ? T('custom.hint') : T('custom.hintSend')}</p>
|
||||
<textarea
|
||||
className="dsh-wt_customInput"
|
||||
autoFocus
|
||||
@@ -1298,6 +1315,20 @@ function CustomPane() {
|
||||
value={requirement}
|
||||
onChange={(e) => setRequirement(e.target.value)}
|
||||
/>
|
||||
{mode === 'existing' && (
|
||||
<div className="dsh-wt_customRow">
|
||||
<span className="dsh-wt_customLabel">{T('custom.session')}</span>
|
||||
<select
|
||||
className="dsh-wt_customSelect"
|
||||
value={sessionId ?? ''}
|
||||
onChange={(e) => setSessionId(e.target.value)}
|
||||
>
|
||||
{sessions.map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.title}{s.isCurrent ? ' ' + T('custom.sessionCurrent') : ''}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div className="dsh-wt_customRow">
|
||||
<span className="dsh-wt_customLabel">{T('custom.project')}</span>
|
||||
<select
|
||||
@@ -1313,9 +1344,9 @@ function CustomPane() {
|
||||
<button
|
||||
type="button"
|
||||
className="dsh-wt_customSend"
|
||||
disabled={busy || !requirement.trim() || !projectId}
|
||||
disabled={busy || !requirement.trim() || !projectId || (mode === 'existing' && !sessionId)}
|
||||
onClick={submit}
|
||||
>{busy ? '…' : T('custom.send')}</button>
|
||||
>{busy ? '…' : (mode === 'new' ? T('custom.send') : T('custom.sendToSession'))}</button>
|
||||
{fail && <p className="dsh-wt_customFail">{T('custom.fail')}:{fail}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -194,6 +194,11 @@ export const css = [
|
||||
'.dsh-wt_customCard{display:flex;flex-direction:column;gap:10px;width:100%;max-width:360px;padding:14px;border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:10px;background:var(--dsw-alias-bg-base,#0b0e14);box-shadow:var(--dsw-shadow-lv2,0 8px 24px rgba(0,0,0,.4))}',
|
||||
'.dsh-wt_customTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e8eb)}',
|
||||
'.dsh-wt_customHint{font-size:11.5px;line-height:17px;color:var(--dsw-alias-label-secondary,#9aa4b2);margin:0}',
|
||||
// 自定义对话框:模式切换(新建对话 / 发送到会话)
|
||||
'.dsh-wt_customModes{display:flex;gap:6px}',
|
||||
'.dsh-wt_customModeBtn{flex:1;display:flex;align-items:center;justify-content:center;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,.02));color:var(--dsw-alias-label-secondary,#9aa4b2);font:inherit;font-size:11px;line-height:16px;cursor:pointer}',
|
||||
'.dsh-wt_customModeBtn:hover{color:var(--dsw-alias-label-primary,#e6e8eb);border-color:var(--dsw-alias-border-l2,#3a4150)}',
|
||||
'.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_customRow{display:flex;align-items:center;gap:8px}',
|
||||
|
||||
Reference in New Issue
Block a user