fix(host): 根因——spawn 的 detached:true 让 PowerShell 起来即退;feat: 三种跟随方式(默认跟随前台)
根因(对照实验):spawn('powershell', [...], {detached:true}) 在 Windows 上 → 子进程 exit 0、
脚本一行没跑(日志里连 start 都没有),表现就是『点打开没反应』;detached:false 完全正常。
现在 spawnShell:绝对路径优先 + 不 detached + 等心跳确认,失败把原因写进 status notes。
跟随方式(用户需求:切到哪个会话就跟哪个,且可配置):
- front(默认)跟随前台:客户端半订阅 uiSession.current,会话一换就上报 /window{action:front}
- latest 跟随最新事件;pinned 固定在 ⇄ 里选中的会话
设置入口:设置→悬浮窗(模式三选一);浮窗 ⇄ 里点会话=切到 pinned
快照新增 follow/front 字段;/window 支持 action=follow|front
This commit is contained in:
+58
-7
@@ -49,17 +49,17 @@ window.__ModuleLoader__.load({
|
||||
})
|
||||
const muted = { opacity: 0.7, fontSize: 12, lineHeight: 1.7 }
|
||||
|
||||
/** 宿主可达性 + 浮窗存活状态 */
|
||||
/** 宿主可达性 + 浮窗存活状态 + 跟随方式 */
|
||||
function useStatus() {
|
||||
const [status, setStatus] = React.useState({ ok: false, alive: false, text: '', session: null })
|
||||
const [status, setStatus] = React.useState({ ok: false, alive: false, text: '', session: null, follow: 'front' })
|
||||
const [busy, setBusy] = React.useState(false)
|
||||
const refresh = React.useCallback(async () => {
|
||||
try {
|
||||
const r = await callHost('status')
|
||||
const s = await readState().catch(() => null)
|
||||
setStatus({ ok: true, alive: !!r.alive, text: r.text || '', session: s })
|
||||
setStatus({ ok: true, alive: !!r.alive, text: r.text || '', session: s, follow: r.follow || (s && s.follow) || 'front' })
|
||||
} catch (e) {
|
||||
setStatus({ ok: false, alive: false, text: String(e && e.message || e), session: null })
|
||||
setStatus({ ok: false, alive: false, text: String(e && e.message || e), session: null, follow: 'front' })
|
||||
}
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
@@ -72,7 +72,18 @@ window.__ModuleLoader__.load({
|
||||
try { await callHost(action) } catch (e) { /* 宿主离线:状态轮询会显示 */ }
|
||||
setTimeout(() => { setBusy(false); refresh() }, 800)
|
||||
}, [refresh])
|
||||
return { status, busy, act, refresh }
|
||||
const setFollow = React.useCallback(async (mode) => {
|
||||
setBusy(true)
|
||||
try {
|
||||
await fetch(`${ROUTE}/window`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'follow', mode }),
|
||||
})
|
||||
} catch (e) { }
|
||||
setTimeout(() => { setBusy(false); refresh() }, 500)
|
||||
}, [refresh])
|
||||
return { status, busy, act, setFollow, refresh }
|
||||
}
|
||||
|
||||
/** 侧边栏底部按钮(设置按钮上方那一排) */
|
||||
@@ -98,9 +109,15 @@ window.__ModuleLoader__.load({
|
||||
|
||||
/** 设置 →「悬浮窗」整页 */
|
||||
function FloatSettingsSection() {
|
||||
const { status, busy, act } = useStatus()
|
||||
const { status, busy, act, setFollow } = useStatus()
|
||||
const s = status.session || {}
|
||||
const m = s.metrics || {}
|
||||
const mode = status.follow || 'front'
|
||||
const modeBtn = (key, label, hint) => h('button', {
|
||||
key, style: btn(mode === key), disabled: busy,
|
||||
title: hint,
|
||||
onClick: () => setFollow(key),
|
||||
}, label)
|
||||
return h('div', { style: { padding: '4px 2px', display: 'flex', flexDirection: 'column', gap: 14, maxWidth: 640 } }, [
|
||||
h('div', { key: 'h', style: { fontSize: 15, fontWeight: 600 } }, '悬浮窗(会话镜像小窗)'),
|
||||
h('div', { key: 'd', style: muted }, '始终置顶、可固定位置/尺寸、抓屏时自动隐身的小窗;把主界面切到后台也能盯着 Agent 干活。'),
|
||||
@@ -119,6 +136,15 @@ window.__ModuleLoader__.load({
|
||||
h('button', { key: 'close', style: btn(false), disabled: busy, onClick: () => act('close') }, '关闭'),
|
||||
]),
|
||||
]),
|
||||
h('div', { key: 'follow', style: card }, [
|
||||
h('div', { key: 't', style: { fontWeight: 600, fontSize: 13 } }, '镜像跟随方式'),
|
||||
h('div', { key: 'row', style: { display: 'flex', gap: 8, flexWrap: 'wrap' } }, [
|
||||
modeBtn('front', '跟随前台(推荐)', '主界面显示哪个会话,浮窗就镜像哪个;切换会话自动跟上'),
|
||||
modeBtn('latest', '跟随最新事件', '哪个会话最新有动静就镜像哪个(多任务并行时会来回跳)'),
|
||||
modeBtn('pinned', '固定某个会话', '固定在浮窗 ⇄ 里选中的那个会话'),
|
||||
]),
|
||||
h('div', { key: 'hint', style: muted }, '当前:' + (mode === 'front' ? '跟随前台当前对话' : mode === 'latest' ? '跟随最新事件' : '固定在选中的会话')),
|
||||
]),
|
||||
h('div', { key: 'st', style: card }, [
|
||||
h('div', { key: 't', style: { fontWeight: 600, fontSize: 13 } }, '当前会话'),
|
||||
h('div', { key: 'n', style: muted }, s.title ? String(s.title) : (s.sessionId || '还没有会话')),
|
||||
@@ -128,7 +154,31 @@ window.__ModuleLoader__.load({
|
||||
])
|
||||
}
|
||||
|
||||
const inject = ['slots']
|
||||
const inject = ['slots', 'uiSession']
|
||||
|
||||
/** 把"当前显示的会话"上报给宿主 —— 「跟随前台」就是靠这个做到"切哪个跟哪个" */
|
||||
function reportFront(ctx) {
|
||||
try {
|
||||
const ui = ctx.get('uiSession')
|
||||
const read = () => {
|
||||
try {
|
||||
const cur = ui?.current
|
||||
const id = cur?.value?.key ?? cur?.value?.sessionId ?? cur?.key ?? null
|
||||
if (!id || id === lastFront) return
|
||||
lastFront = id
|
||||
fetch(`${ROUTE}/window`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'front', sessionId: id }),
|
||||
}).catch(() => { })
|
||||
} catch { }
|
||||
}
|
||||
read()
|
||||
// uiSession.current 是 binding source,订阅它就能在切换会话时立刻跟上
|
||||
try { return ui?.current?.subscribe?.(read) || ui?.subscribe?.(read) || null } catch { return null }
|
||||
} catch (e) { console.warn('[float-window] uiSession unavailable', e); return null }
|
||||
}
|
||||
let lastFront = null
|
||||
|
||||
/** 客户端半接管 UI 后,把宿主注入的备用按钮摘掉,避免出现两个入口 */
|
||||
function claimUi() {
|
||||
@@ -141,6 +191,7 @@ window.__ModuleLoader__.load({
|
||||
|
||||
function apply(ctx) {
|
||||
let registered = 0
|
||||
reportFront(ctx)
|
||||
try {
|
||||
ctx.slots.inject('sidebar.footer.action', () => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user