feat: 自定义窗口两种模式——新建专属对话 / 发送到已有会话(默认当前会话,可下拉选择)

This commit is contained in:
Aisland
2026-08-18 02:22:09 +08:00
parent 42645f2f51
commit 02d4f20020
6 changed files with 111 additions and 30 deletions
+35 -3
View File
@@ -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')
+14
View File
@@ -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',
+39 -8
View File
@@ -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>
+5
View File
@@ -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}',
+11 -19
View File
@@ -639,23 +639,15 @@
原生视图内部是浏览器扩展页,插件注入不了按钮、拦不到鼠标,也驱动不了内部滚动——
向用户如实说明后按指示回退);② 视图选项改名「设置」,点开直接内嵌「排序方式 + 管理项目
展开列表」,不再二次点击;③ 管理项目里给现有项目加「变更视图」:换预设拓扑,
## 补记(自定义窗口:需求对话框 + 新建专属会话 + 项目选择 + 上下文注入)
## 补记(自定义窗口增加「发送到已有会话」模式)
- 用户需求:点「自定义」→ 新开标签(关标签可回退)→ 居中对话框:写需求 + 选所属项目
(默认 = 刚在用的项目)+ 提示发送后将新建一个对话 → 发送 = 自动新建会话,
会话需知道:worktable 是什么(自建工具,官方文档没有)、本对话是解决一个窗口的自定义、
用户需求原文、窗口位于哪个项目(便于窗口级与项目级后续自定义)。
- 实现:
① BuiltinType 新增 custom;PanePicker ✨ 直接 openTab 自定义标签(旧内联 URL 表单移除);
② CustomPane:居中卡片对话框(需求 textarea + 项目 select + 提示文案 + 发送按钮),
默认项目 = splitStore.spec.id(当前工作区所属项目),不在列表时回退第一个;发送成功后 ✓ 态;
③ 会话创建桥:inject 增加 'conversation'(探针实测 sessions.create/open、conversation.send/
sendSession 可用;api 不可注入已排除);createCustomSession:sessions.create({}) → 优先
conversation.sendSession(sessionId, text) 否则 open 后 send;注入上下文消息:
「worktable 是什么 / 本对话用途 / 用户需求 / 所在项目」四段式;
④ WorktableSection 经 projectsRef 快照向引擎注入 custom.getProjects/currentProjectId/submit;
locale custom.* 八键 + 对话框 CSS。
- 验证(functional-diag STEP20,仅 UI 不点发送防真实建会话):tabType=custom、对话框/
textarea/提示语/项目下拉/发送禁用(空需求)全部正确;全 20 STEP ERROR_COUNT: 0。
真实建会话闭环需用户在 GUI 实测。
- 备注:纯客户端改动,F5 即生效。checkpoint tag: checkpoint-2026-08-17-pre-customdialog。
- 用户需求:不一定每次都新建对话;可加入当前对话/选择已有会话,把内容直接发过去。
- 处理:CustomPane 顶部加模式切换(新建对话 | 发送到会话);发送到会话模式出现会话下拉
(默认当前会话,标注「(当前)」),项目选择保留(上下文仍需项目信息);
sendCustomToSession:优先 conversation.sendSession(sessionId, text) 定向发送,否则 open 后 send,
发送后打开该会话;消息为精简版上下文(worktable 是什么/需求/所在项目);
env.custom 增 getSessions(读 ctx.sessions.list 快照 items+current)与 sendToSession;
完成态文案区分「已新建专属对话 / 已发送到会话」。locale custom.* 增 7 键。
- 验证(functional-diag STEP20 扩展):切换模式 → 会话下拉出现;新建模式各项回归不变;
全 20 STEP ERROR_COUNT: 0。真实发送闭环待用户在 GUI 实测(新建/发送两种模式各试一次)。
- 备注:纯客户端改动,F5 即生效。
+7
View File
@@ -659,6 +659,13 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
out.defaultIsCurrent=sel?sel.value==='t-custom':false;
var send=vis('.dsh-wt_customSend');
out.sendDisabled=send?send.disabled:null;
// 切到「发送到会话」模式:出现会话下拉,默认当前会话
var modeBtns=[].slice.call(document.querySelectorAll('.dsh-wt_customModeBtn')).filter(function(el){return (getComputedStyle(el).visibility!=='hidden');});
if(modeBtns[1]){ modeBtns[1].click(); }
await new Promise(function(r){setTimeout(r,250)});
var ssel=[].slice.call(document.querySelectorAll('.dsh-wt_customSelect')).filter(function(el){return (getComputedStyle(el).visibility!=='hidden');})[0];
out.sessionSelect=!!ssel;
out.sessionHasCurrent=ssel?String(ssel.textContent).indexOf('\u5f53\u524d')>=0||String(ssel.textContent).indexOf('current')>=0:false;
st.close();
}catch(err){ out.err=String(err) }
return JSON.stringify(out);