feat: anim pane type + bind button redesign (aligned double-circle icon, status popup) + auto-bind rule + workspaces route BOM tolerance
This commit is contained in:
@@ -382,7 +382,7 @@ async function promptIntoSession(sessionId: string, text: 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> {
|
||||
export async function createCustomSession(projectId: string, projectName: string, requirement: string, group?: NewSessionGroup): Promise<string> {
|
||||
const b = sessionBridge
|
||||
if (!b || typeof b.sessions?.create !== 'function') throw new Error('sessions unavailable')
|
||||
const text = [
|
||||
@@ -424,6 +424,7 @@ export async function createCustomSession(projectId: string, projectName: string
|
||||
const sessionId = await b.sessions.create(workspaceId ? { workspaceId } : {})
|
||||
try { await b.sessions.open?.(sessionId) } catch {}
|
||||
await promptIntoSession(sessionId, text)
|
||||
return sessionId
|
||||
}
|
||||
|
||||
/** 把自定义需求直接发到用户选定的已有会话(默认当前会话) */
|
||||
@@ -553,7 +554,7 @@ function WorktableSection(props: any) {
|
||||
// 注意:不走 props.useSessions hook(其宿主包装在部分版本会触发 useSyncExternalStore
|
||||
// 崩溃),改为 apply 里订阅 ctx.sessions.list 后写入模块级 store,此处直接读取。
|
||||
useEffect(() => {
|
||||
setSplitEnv({
|
||||
const env = {
|
||||
getScope: () => {
|
||||
const s = sessionScopeStore.snapshot
|
||||
return s ? { sessionId: s.sessionId, cwd: s.cwd } : null
|
||||
@@ -587,9 +588,20 @@ function WorktableSection(props: any) {
|
||||
},
|
||||
createWorkspace: (path: string) => sessionBridge?.workspaces?.create?.({ path }),
|
||||
createWorkspaceDir: (parent: string, name: string) => sessionBridge?.workspaces?.createDirectory?.(parent, name),
|
||||
// 自动绑定规则:当前项目未绑定 → 绑定到刚新建/发送的会话;已绑定 → 不改(返回 'kept')
|
||||
autoBind: (sessionId: string): 'auto' | 'kept' | 'none' => {
|
||||
const pid = splitStore.active && splitStore.spec ? splitStore.spec.id : null
|
||||
if (!pid || typeof sessionId !== 'string' || !sessionId) return 'none'
|
||||
const already = projectsRef.current.projects.bindings[pid]
|
||||
if (already) return 'kept'
|
||||
persistProjects((prev) => (prev.bindings[pid] ? prev : { ...prev, bindings: { ...prev.bindings, [pid]: sessionId } }))
|
||||
return 'auto'
|
||||
},
|
||||
},
|
||||
})
|
||||
return () => setSplitEnv(null)
|
||||
}
|
||||
setSplitEnv(env)
|
||||
try { (window as any).__dshCustomEnv = env } catch {}
|
||||
return () => { setSplitEnv(null); try { (window as any).__dshCustomEnv = null } catch {} }
|
||||
}, [])
|
||||
const floatRef = useRef<FloatRect | null>(null)
|
||||
|
||||
@@ -742,6 +754,15 @@ function WorktableSection(props: any) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
/** 绑定会话信息:文件夹(工作区)+ 会话标题(供绑定弹窗状态行展示) */
|
||||
function bindInfoOf(groups: { title: string; sessions: { id: string; title: string; isCurrent: boolean }[] }[], sid: string): { folder: string; title: string } {
|
||||
for (const g of groups) {
|
||||
const s = g.sessions.find((x) => x.id === sid)
|
||||
if (s) return { folder: g.title || '未分组', title: s.title }
|
||||
}
|
||||
return { folder: '', title: sid }
|
||||
}
|
||||
|
||||
/** 自定义布局提示词模板(复制到剪贴板,可发给任意 dsh 会话让 agent 实现) */
|
||||
function buildCustomLayoutPrompt(req: string): string {
|
||||
return [
|
||||
@@ -1102,7 +1123,10 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
bindBtn = document.createElement('span')
|
||||
bindBtn.className = 'dsh-wt_bindBtn'
|
||||
bindBtn.setAttribute('role', 'button')
|
||||
bindBtn.textContent = '🔗'
|
||||
const circles = document.createElement('span')
|
||||
circles.className = 'dsh-wt_bindCircles'
|
||||
circles.setAttribute('aria-hidden', 'true')
|
||||
bindBtn.appendChild(circles)
|
||||
el.appendChild(bindBtn)
|
||||
const cs = getComputedStyle(el)
|
||||
if (cs.position === 'static') el.style.position = 'relative'
|
||||
@@ -1110,7 +1134,7 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
bindBtn.setAttribute('data-wt-bind', id)
|
||||
const bound = projects.bindings[id]
|
||||
bindBtn.setAttribute('data-bound', bound ? 'true' : 'false')
|
||||
bindBtn.title = t('bind.title')
|
||||
bindBtn.title = bound ? t('bind.titleBound') : t('bind.titleUnbound')
|
||||
const icon = el.children[0] as HTMLElement | null
|
||||
if (icon) {
|
||||
const ovr = projects.iconOverrides[id]
|
||||
@@ -1542,7 +1566,25 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
{bindPick && (
|
||||
<div className="dsh-wt_menu dsh-wt_pop dsh-wt_bindPop" style={{ position: 'fixed', left: bindPick.x, top: bindPick.y, width: 280, zIndex: 84 }}>
|
||||
<span className="dsh-wt_menuLabel">{t('bind.title')}</span>
|
||||
<div className="dsh-wt_bindProjName">{projects.nameOverrides[bindPick.id] ?? metas[bindPick.id]?.name ?? projects.layouts.find((l) => l.id === bindPick.id)?.title ?? bindPick.id}</div>
|
||||
{(() => {
|
||||
const sid = projects.bindings[bindPick.id]
|
||||
if (!sid) {
|
||||
return (
|
||||
<div className="dsh-wt_bindStatus dsh-wt_bindStatusNone">
|
||||
<span className="dsh-wt_bindMini" aria-hidden><span className="dsh-wt_bindCircles" /></span>
|
||||
<span className="dsh-wt_bindNoneText">{t('bind.unbound')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const info = bindInfoOf(bindGroups, sid)
|
||||
return (
|
||||
<div className="dsh-wt_bindStatus">
|
||||
<span className="dsh-wt_bindFolder" title={info.folder}>📂 {info.folder}</span>
|
||||
<span className="dsh-wt_bindSep" aria-hidden>|</span>
|
||||
<span className="dsh-wt_bindConv" title={info.title}>{info.title}</span>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
<p className="dsh-wt_bindHint">{t('bind.hint')}</p>
|
||||
{projects.bindings[bindPick.id] && (
|
||||
<button type="button" className="dsh-wt_bindUnbind" onClick={() => setProjectBinding(bindPick.id, null)}>{t('bind.unbind')} ✕</button>
|
||||
@@ -1622,7 +1664,7 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
type="button"
|
||||
className="dsh-wt_layout"
|
||||
data-on={activeSplitId === l.id ? 'true' : 'false'}
|
||||
style={{ order: effectiveOrder.indexOf(l.id) + 1000 }}
|
||||
style={{ order: effectiveOrder.indexOf(l.id) + 1000, position: 'relative' }}
|
||||
onClick={() => { openSplit(l); reportUsed(l.id) }}
|
||||
>
|
||||
<span
|
||||
@@ -1640,9 +1682,9 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
data-bound={projects.bindings[l.id] ? 'true' : 'false'}
|
||||
title={t('bind.title')}
|
||||
title={projects.bindings[l.id] ? t('bind.titleBound') : t('bind.titleUnbound')}
|
||||
onClick={(e) => { e.stopPropagation(); openBindPick(l.id, e.currentTarget as HTMLElement) }}
|
||||
>🔗</span>
|
||||
><span className="dsh-wt_bindCircles" aria-hidden /></span>
|
||||
<span className="dsh-wt_layoutArrow" aria-hidden>›</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -58,6 +58,8 @@ export const zh = {
|
||||
'pane.tasks': '任务管理',
|
||||
'pane.terminal': '终端',
|
||||
'pane.custom': '自定义',
|
||||
'pane.anim': '动画',
|
||||
'pane.animUrlPh': '粘贴动画站点地址后回车',
|
||||
'custom.title': '自定义这个窗口',
|
||||
'custom.hint': '发送后将自动新建一个会话,专门负责这个窗口的自定义设计。',
|
||||
'custom.placeholder': '告诉 AI 你想让这个窗口做什么,例如:做一个展示我旅行足迹的时间线页面…',
|
||||
@@ -83,7 +85,10 @@ export const zh = {
|
||||
'custom.groupNeedPath': '请填写分组父目录和新分组名',
|
||||
'bind.title': '绑定对话',
|
||||
'bind.unbind': '解绑',
|
||||
'bind.hint': '选择后,打开该项目时右侧对话窗自动切换到这个会话;解绑后不再切换。',
|
||||
'bind.unbound': '未绑定对话',
|
||||
'bind.titleUnbound': '绑定对话(未绑定 ○○):点击选择要跟随的对话',
|
||||
'bind.titleBound': '已绑定对话(●●):点击更换或解绑',
|
||||
'bind.hint': '一个项目只能绑定一个对话。选择后,打开该项目时右侧对话窗会自动切换过去;已绑定的项目不会因新建/发送对话而自动改绑。',
|
||||
'customLayout.add': '自定义',
|
||||
'customLayout.addTitle': '自定义一个布局(生成提示词)',
|
||||
'customLayout.title': '自定义布局',
|
||||
@@ -91,6 +96,8 @@ export const zh = {
|
||||
'customLayout.copy': '复制提示词到剪贴板',
|
||||
'customLayout.copied': '提示词已复制到剪贴板。去任意一个 DSH 对话里粘贴发送,对方的 dsh 就知道要做什么了。',
|
||||
'customLayout.copyFail': '复制失败,请手动全选输入框内容复制。',
|
||||
'custom.autoBound': '已自动把这个项目绑定到这个对话(卡片上的 ●● 按钮可更换或解绑)。',
|
||||
'custom.keptBinding': '该项目已有绑定的对话,按规则未自动更改。',
|
||||
'pane.wip': '开发中(后续版本接入)',
|
||||
'pane.explorerNoCwd': '当前会话没有工作目录',
|
||||
'pane.openLater': '该类型暂不支持预览',
|
||||
@@ -172,6 +179,8 @@ export const en = {
|
||||
'pane.tasks': 'Tasks',
|
||||
'pane.terminal': 'Terminal',
|
||||
'pane.custom': 'Custom',
|
||||
'pane.anim': 'Animation',
|
||||
'pane.animUrlPh': 'Paste the animation site URL and press Enter',
|
||||
'custom.title': 'Customize this pane',
|
||||
'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…',
|
||||
@@ -197,7 +206,10 @@ export const en = {
|
||||
'custom.groupNeedPath': 'Fill in the parent folder and new group name',
|
||||
'bind.title': 'Bind conversation',
|
||||
'bind.unbind': 'Unbind',
|
||||
'bind.hint': 'When bound, opening this project switches the right chat pane to this conversation. Unbind to stop switching.',
|
||||
'bind.unbound': 'No conversation bound',
|
||||
'bind.titleUnbound': 'Bind conversation (unbound ○○): pick the conversation to follow',
|
||||
'bind.titleBound': 'Conversation bound (●●): click to change or unbind',
|
||||
'bind.hint': 'A project can bind exactly one conversation. When bound, opening the project switches the right chat pane to it; an existing binding is never auto-changed by creating/sending conversations.',
|
||||
'customLayout.add': 'Custom',
|
||||
'customLayout.addTitle': 'Customize a layout (generate a prompt)',
|
||||
'customLayout.title': 'Custom layout',
|
||||
@@ -205,6 +217,8 @@ export const en = {
|
||||
'customLayout.copy': 'Copy prompt to clipboard',
|
||||
'customLayout.copied': 'Prompt copied to clipboard. Paste it into any DSH conversation and send — that dsh will know what to do.',
|
||||
'customLayout.copyFail': 'Copy failed — select the text above and copy it manually.',
|
||||
'custom.autoBound': 'This project was auto-bound to this conversation (use the ●● button on its card to change or unbind).',
|
||||
'custom.keptBinding': 'This project already has a bound conversation, so it was not auto-changed.',
|
||||
'pane.wip': 'Coming in a later version',
|
||||
'pane.explorerNoCwd': 'No working directory for the current session',
|
||||
'pane.openLater': 'Preview not supported for this type',
|
||||
|
||||
@@ -23,7 +23,7 @@ hljs.registerLanguage('json', hljsJson)
|
||||
* 内容与 chatSide 的变更经 onSpecMutated 回调交给工作台持久化(布局条目)。
|
||||
*/
|
||||
|
||||
export type BuiltinType = 'browser' | 'explorer' | 'scm' | 'tasks' | 'terminal' | 'custom'
|
||||
export type BuiltinType = 'browser' | 'anim' | 'explorer' | 'scm' | 'tasks' | 'terminal' | 'custom'
|
||||
|
||||
export type SplitContent =
|
||||
| { kind: 'iframe'; url: string; title?: string }
|
||||
@@ -123,6 +123,7 @@ const PERSIST_KEY = 'dsh.worktable.split.v2'
|
||||
/** 内置内容窗图标 */
|
||||
const BUILTIN_ICONS: Record<BuiltinType, string> = {
|
||||
browser: '🌐',
|
||||
anim: '🎬',
|
||||
explorer: '📁',
|
||||
scm: '🔀',
|
||||
tasks: '✅',
|
||||
@@ -132,6 +133,7 @@ const BUILTIN_ICONS: Record<BuiltinType, string> = {
|
||||
|
||||
const BUILTIN_LABEL_KEYS: Record<BuiltinType, string> = {
|
||||
browser: 'pane.browser',
|
||||
anim: 'pane.anim',
|
||||
explorer: 'pane.explorer',
|
||||
scm: 'pane.scm',
|
||||
tasks: 'pane.tasks',
|
||||
@@ -861,6 +863,31 @@ function BrowserPane() {
|
||||
)
|
||||
}
|
||||
|
||||
/** 动画播放窗:iframe 壳 + 地址栏(站内自带项目/场景列表、播放、画幅切换、导出等全部控件) */
|
||||
function AnimPane() {
|
||||
const [url, setUrl] = useState('')
|
||||
const [src, setSrc] = useState('about:blank')
|
||||
const go = () => {
|
||||
const u = url.trim()
|
||||
setSrc(/^(\/|https?:\/\/)/i.test(u) ? u : 'about:blank')
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="dsh-wt_browserBar">
|
||||
<input
|
||||
className="dsh-wt_browserInput"
|
||||
value={url}
|
||||
placeholder={T('pane.animUrlPh')}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') go() }}
|
||||
/>
|
||||
<button type="button" className="dsh-wt_browserGo" onClick={go}>↗</button>
|
||||
</div>
|
||||
<iframe className="dsh-wt_paneFrame" src={src} title="anim" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** 文件夹图标(重绘 SVG,与 better-sidebar 同款风格) */
|
||||
function FolderIcon() {
|
||||
return (
|
||||
@@ -1313,6 +1340,7 @@ function CustomPane() {
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [done, setDone] = useState(false)
|
||||
const [fail, setFail] = useState('')
|
||||
const [bindNote, setBindNote] = useState<'auto' | 'kept' | 'none'>('none')
|
||||
// 默认项目 = 当前工作区所属项目;默认会话 = 当前会话;默认分组 = 当前会话所在工作区
|
||||
useEffect(() => {
|
||||
const list = custom?.getProjects?.() ?? []
|
||||
@@ -1344,10 +1372,12 @@ function CustomPane() {
|
||||
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)
|
||||
const sid = await custom.submit(projectId, pname, text, group)
|
||||
setBindNote((custom.autoBind?.(sid) ?? 'none') as any)
|
||||
} else {
|
||||
if (!sessionId) return
|
||||
await custom.sendToSession(sessionId, pname, text)
|
||||
setBindNote((custom.autoBind?.(sessionId) ?? 'none') as any)
|
||||
}
|
||||
setDone(true)
|
||||
} catch (e) {
|
||||
@@ -1365,6 +1395,9 @@ function CustomPane() {
|
||||
<span className="dsh-wt_customDone" aria-hidden>✅</span>
|
||||
<p className="dsh-wt_customDoneText">{mode === 'new' ? T('custom.done') : T('custom.sent')}</p>
|
||||
<p className="dsh-wt_customDoneHint">{T('custom.doneHint')}</p>
|
||||
{bindNote !== 'none' && (
|
||||
<p className="dsh-wt_customDoneBind">{bindNote === 'auto' ? T('custom.autoBound') : T('custom.keptBinding')}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1473,6 +1506,7 @@ function PaneTabBody(props: { tab: PaneTab; row: PaneRow; index: number }) {
|
||||
return <FileViewer path={content.path} />
|
||||
}
|
||||
if (content.type === 'browser') return <BrowserPane />
|
||||
if (content.type === 'anim') return <AnimPane />
|
||||
if (content.type === 'explorer') return <ExplorerPane row={props.row} index={props.index} />
|
||||
if (content.type === 'scm') return <GitPane />
|
||||
if (content.type === 'tasks') return <JobsPane />
|
||||
@@ -1547,6 +1581,9 @@ function PanePicker(props: { row: PaneRow; index: number }) {
|
||||
<button type="button" className="dsh-wt_panePick" onClick={() => pick({ kind: 'builtin', type: 'browser' })}>
|
||||
<span aria-hidden>🌐</span>{T('pane.browser')}
|
||||
</button>
|
||||
<button type="button" className="dsh-wt_panePick" onClick={() => pick({ kind: 'builtin', type: 'anim' })}>
|
||||
<span aria-hidden>🎬</span>{T('pane.anim')}
|
||||
</button>
|
||||
<button type="button" className="dsh-wt_panePick" onClick={() => pick({ kind: 'builtin', type: 'explorer' })}>
|
||||
<span aria-hidden>📁</span>{T('pane.explorer')}
|
||||
</button>
|
||||
|
||||
@@ -153,11 +153,16 @@ export const css = [
|
||||
'.dsh-wt_presetAdd{border-style:dashed;color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
'.dsh-wt_presetAddIcon{font-size:18px;line-height:20px}',
|
||||
'.dsh-wt_presetAddText{font-size:10px;line-height:13px}',
|
||||
// 项目卡片上的对话绑定按钮(布局卡内联 / 入驻卡由桥注入,绝对定位于中间偏右)
|
||||
'.dsh-wt_bindBtn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;margin-left:auto;border-radius:5px;font-size:12px;line-height:1;color:var(--dsw-alias-label-tertiary,#6b7280);cursor:pointer;user-select:none}',
|
||||
'.dsh-wt_bindBtn:hover{background:var(--dsw-alias-fill-l2,rgba(255,255,255,.06));color:var(--dsw-alias-label-primary,#e6e8eb)}',
|
||||
// 项目卡片上的对话绑定按钮(○○ 空心 = 未绑定 / ●● 实心 = 已绑定;CSS 双圆单色绘制;
|
||||
// 布局卡与入驻卡统一绝对定位于卡片中间偏右、垂直居中,保证所有卡片竖向对齐)
|
||||
'.dsh-wt_bindBtn{position:absolute;right:26px;top:50%;transform:translateY(-50%);width:18px;height:14px;margin:0;display:block;color:var(--dsw-alias-label-tertiary,#6b7280);cursor:pointer;user-select:none}',
|
||||
'.dsh-wt_bindBtn:hover{color:var(--dsw-alias-label-primary,#e6e8eb)}',
|
||||
'.dsh-wt_bindBtn[data-bound=true]{color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
'button[data-wt-id] > .dsh-wt_bindBtn{position:absolute;right:26px;top:50%;transform:translateY(-50%);margin:0}',
|
||||
'.dsh-wt_bindCircles{position:absolute;left:0;top:50%;width:18px;height:9px;transform:translateY(-50%)}',
|
||||
'.dsh-wt_bindCircles::before,.dsh-wt_bindCircles::after{content:\'\';position:absolute;top:50%;width:8px;height:8px;margin-top:-4px;border-radius:50%;box-sizing:border-box;border:1.4px solid currentColor;background:transparent}',
|
||||
'.dsh-wt_bindCircles::before{left:0}',
|
||||
'.dsh-wt_bindCircles::after{left:9px}',
|
||||
'.dsh-wt_bindBtn[data-bound=true] .dsh-wt_bindCircles::before,.dsh-wt_bindBtn[data-bound=true] .dsh-wt_bindCircles::after{background:currentColor}',
|
||||
|
||||
// 布局条目卡片
|
||||
'.dsh-wt_layout{display:flex;align-items:center;gap:7px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.02));color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:18px;cursor:pointer;text-align:left}',
|
||||
@@ -221,9 +226,17 @@ export const css = [
|
||||
'.dsh-wt_customLayoutBtn:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}',
|
||||
'.dsh-wt_customLayoutToast{margin:8px 0 0;font-size:11.5px;line-height:17px;color:var(--dsw-alias-state-success,#3fb950)}',
|
||||
'.dsh-wt_customLayoutToastFail{color:var(--dsw-alias-state-danger,#f85149)}',
|
||||
// 对话绑定弹窗
|
||||
'.dsh-wt_bindProjName{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e8eb);margin-top:6px}',
|
||||
'.dsh-wt_bindHint{margin:4px 0 6px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary,#9aa4b2)}',
|
||||
// 对话绑定弹窗:状态行(未绑定 / 📂文件夹 | [对话框])
|
||||
'.dsh-wt_bindStatus{display:flex;align-items:center;gap:6px;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}',
|
||||
'.dsh-wt_bindStatusNone{color:var(--dsw-alias-label-secondary,#9aa4b2)}',
|
||||
'.dsh-wt_bindMini{position:relative;display:inline-block;width:18px;height:14px;flex:none}',
|
||||
'.dsh-wt_bindNoneText{font-size:12px;line-height:18px}',
|
||||
'.dsh-wt_bindFolder{flex:none;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#9aa4b2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:40%}',
|
||||
'.dsh-wt_bindSep{flex:none;color:var(--dsw-alias-border-l2,#3a4150);font-size:11px}',
|
||||
'.dsh-wt_bindConv{flex:1;min-width:0;padding:2px 8px;border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:6px;background:var(--dsw-alias-fill-l2,rgba(255,255,255,.05));font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary,#e6e8eb);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||
'.dsh-wt_bindHint{margin:6px 0 6px;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary,#9aa4b2)}',
|
||||
// 自定义窗完成页:自动绑定提示
|
||||
'.dsh-wt_customDoneBind{margin:6px 0 0;font-size:11px;line-height:16px;color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
'.dsh-wt_bindUnbind{display:inline-flex;align-items:center;gap:4px;margin-bottom:6px;padding:3px 8px;border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary,#9aa4b2);font:inherit;font-size:11px;line-height:16px;cursor:pointer}',
|
||||
'.dsh-wt_bindUnbind:hover{color:var(--dsw-alias-state-danger,#f85149);border-color:var(--dsw-alias-state-danger,#f85149)}',
|
||||
'.dsh-wt_bindList{max-height:280px;overflow:auto;position:static}',
|
||||
|
||||
@@ -292,7 +292,8 @@ export function apply(ctx: Context) {
|
||||
try {
|
||||
const file = pathResolve(homedir(), '.dsh', 'storages', 'workspace.json')
|
||||
const raw = await readFile(file, 'utf8')
|
||||
json(res, 200, JSON.parse(raw))
|
||||
// 容忍 BOM(外部工具改写可能带 EF BB BF,JSON.parse 会抛错)
|
||||
json(res, 200, JSON.parse(raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw))
|
||||
} catch (err) {
|
||||
json(res, 404, { error: String(err) })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user