revert: hub.js/server.js/panel.html 回滚到 v1.6.37(用户指令:1.6.37 为最新版本)
- src/hub.js: 89545 → 89332 字节(去除 1.6.70 增强) - src/server.js: 42219 → 41961 字节 - web/panel.html: 89203 → 84523 字节(撤掉 isSessionConnected holding/isHung 修改) - 本地 /Users/karuo/.persistent-chat-local/ 三个核心文件已同步覆盖 + uchg 锁定 - Hub 服务已重启到 1.6.37(API 返回 panelVersion: 1.6.37) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
"env": {
|
||||
"PCHAT_HTTP_PORT": "13458",
|
||||
"PCHAT_NODE": "/usr/local/opt/node@22/bin/node",
|
||||
"PCHAT_HUB_HOST": "192.168.110.101",
|
||||
"PCHAT_HUB_HOST": "127.0.0.1",
|
||||
"NO_PROXY": "192.168.110.101,127.0.0.1,localhost",
|
||||
"no_proxy": "192.168.110.101,127.0.0.1,localhost"
|
||||
},
|
||||
|
||||
@@ -711,9 +711,6 @@ function shouldAlwaysKeepAlive(s) {
|
||||
function buildKeepAliveContinueText(s, review) {
|
||||
const mode = getSessionContinueMode(s);
|
||||
const goal = String((s && s.projectGoal) || (review && review.projectGoal) || '').slice(0, 150);
|
||||
if (review && review.isIdleNext) {
|
||||
return `【挂起】当前会话已保持连接,下一步状态是 wait / 待命。等待下一条消息即可,不要结束对话。`;
|
||||
}
|
||||
if (review && review.nextStep && !review.isIdleNext) {
|
||||
return `【保活续跑】继续执行▶下一步:${review.nextStep.slice(0, 400)}。保持连接勿停。`;
|
||||
}
|
||||
@@ -728,7 +725,7 @@ function buildKeepAliveContinueText(s, review) {
|
||||
return '【保活续跑】保持连接,继续优化流程:总结→检索→补文档→开发,按学习清单推进。';
|
||||
}
|
||||
if (goal) {
|
||||
return `【挂起】目标已记录:${goal}。当前状态维持 wait / 待命,等待下一条消息继续,不要结束对话。`;
|
||||
return `【保活续跑】继续推进:${goal}。按复盘与▶下一步执行,保持连接勿停。`;
|
||||
}
|
||||
return String(
|
||||
hubSettings.keepAliveText
|
||||
|
||||
@@ -34,10 +34,9 @@ const WORKSPACE = (!WORKSPACE_RAW
|
||||
? (process.cwd() || HOME)
|
||||
: WORKSPACE_RAW;
|
||||
const HTTP_PORT = parseInt(process.env.PCHAT_HTTP_PORT || arg('port', '13458'), 10);
|
||||
const HUB_HOST = process.env.PCHAT_HUB_HOST || '127.0.0.1';
|
||||
const HUB = `http://${HUB_HOST}:${HTTP_PORT}`;
|
||||
const HUB = `http://127.0.0.1:${HTTP_PORT}`;
|
||||
const HOST_IP = getHostIp();
|
||||
const HUB_LABEL = `${HUB_HOST}:${HTTP_PORT}`;
|
||||
const HUB_LABEL = `127.0.0.1:${HTTP_PORT}`;
|
||||
/** 卡若AI 能力真源(每轮对话门禁) */
|
||||
const KARUO_AI_ROOT = '/Users/karuo/Documents/个人/卡若AI';
|
||||
|
||||
@@ -656,7 +655,7 @@ const TOOLS = [
|
||||
},
|
||||
{
|
||||
name: 'wait_for_user_input',
|
||||
description: '挂起 Agent,等待远端主链面板 http://192.168.110.101:13458 回复;本机 127.0.0.1 仅兜底。必须使用本线程锁定的 pchat-token。',
|
||||
description: '挂起 Agent,等待本地面板 http://127.0.0.1:13458 回复。必须使用本线程锁定的 pchat-token。',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -1112,12 +1111,9 @@ rl.on('line', async (line) => {
|
||||
|
||||
/** Cursor 可能关闭 MCP stdin;挂起 poll 期间禁止 exit,否则 Hub 仍 waiting 但无 poller */
|
||||
let activeWaitToken = null;
|
||||
let idleKeepAliveTimer = null;
|
||||
process.stdin.on('end', () => {
|
||||
log('stdin end', { activeWait: activeWaitToken });
|
||||
if (activeWaitToken) return;
|
||||
if (!idleKeepAliveTimer) idleKeepAliveTimer = setInterval(() => {}, 60 * 1000);
|
||||
log('stdin end ignored: keep alive while idle to preserve transport');
|
||||
if (!activeWaitToken) setTimeout(() => process.exit(0), 200);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => process.exit(0));
|
||||
|
||||
189
web/panel.html
189
web/panel.html
@@ -81,8 +81,6 @@
|
||||
aside .sess.active { background:rgba(37,99,235,.08); border-color:rgba(37,99,235,.28); box-shadow:inset 3px 0 0 var(--kir-brand); }
|
||||
aside .sess.waiting { background:linear-gradient(90deg,rgba(255,138,0,.1),transparent); }
|
||||
aside .sess.waiting.active { background:linear-gradient(90deg,rgba(255,138,0,.16),rgba(37,99,235,.06)); }
|
||||
aside .sess.abandoned { opacity:.62; }
|
||||
aside .sess.abandoned .badge { color:var(--fg2); }
|
||||
aside .sess .title { font-weight:600; font-size:13px; line-height:1.35; }
|
||||
aside .sess .meta { font-size:11px; color:var(--fg2); margin-top:4px; display:flex; gap:6px; align-items:center; flex-wrap:wrap; }
|
||||
.badge { padding:2px 8px; border-radius:var(--kir-radius-pill); font-size:10px; font-weight:600; letter-spacing:.02em; }
|
||||
@@ -197,16 +195,13 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
<div id="vsixBanner" class="alert-bar" style="display:flex;background:linear-gradient(90deg,#2563eb,#7c3aed);cursor:default">
|
||||
<span>🔌 <strong>VSIX 4.6.0 官方</strong> · 主界面=<strong>Cursor 侧栏</strong>(开 MCP + 持续对话)· Hub <code>http://localhost:13458</code> 排队续跑</span>
|
||||
</div>
|
||||
<div id="alertBar" class="alert-bar" style="display:none" onclick="jumpToWaiting()">
|
||||
<span class="pulse"></span>
|
||||
<span id="alertText">有 1 个会话正在等待你回复</span>
|
||||
<span style="margin-left:auto;font-size:12px;opacity:.85">点击跳转 →</span>
|
||||
</div>
|
||||
<header class="app-bar kir-glass-solid">
|
||||
<h1>卡若AI · VSIX 4.6.0 Hub <span id="panelVer" style="font-size:11px;color:var(--fg2);font-weight:500;-webkit-text-fill-color:var(--fg2)"></span></h1>
|
||||
<h1>卡若AI · 持久对话 <span id="panelVer" style="font-size:11px;color:var(--fg2);font-weight:500;-webkit-text-fill-color:var(--fg2)"></span></h1>
|
||||
<div class="right">
|
||||
<span id="conn" class="conn-pill"><span class="dot warn"></span> 连接中…</span>
|
||||
<div class="more-menu">
|
||||
@@ -231,8 +226,7 @@
|
||||
<div class="empty">
|
||||
<div class="big">还没开始?</div>
|
||||
点 <button type="button" class="copy-btn" data-copy-prompt style="display:inline-block">📋 复制启动提示词</button> 粘贴到 Cursor Chat(Agent 模式),或点左侧 <strong>+ 新建任务</strong>。<br><br>
|
||||
Agent 会 <code>init_conversation</code> 创建会话,再 <code>wait_for_user_input</code> 挂起;之后从本面板回复即可。<br>
|
||||
<small style="color:var(--fg2)">MCP 4 工具:init · select · wait · merge · 与 Cursor 侧栏 4tools 一致</small>
|
||||
Agent 会 <code>init_conversation</code> 创建会话,再 <code>wait_for_user_input</code> 挂起;之后从本面板回复即可。
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@@ -248,31 +242,10 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const PANEL_VERSION = '1.6.70';
|
||||
const MCP_4TOOLS = ['init_conversation', 'select_conversation', 'wait_for_user_input', 'merge_conversation'];
|
||||
let hubMeta = { vsixCompat: '4.6.0', vsixVariant: '4tools', mcpToolCount: 4, panelVersion: PANEL_VERSION };
|
||||
|
||||
function isVsix4Tools() {
|
||||
return hubMeta.vsixVariant === '4tools' || (Number(hubMeta.mcpToolCount) === 4 && hubMeta.vsixVariant !== 'standard');
|
||||
}
|
||||
|
||||
function fetchWithTimeout(url, ms, init = {}) {
|
||||
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
||||
return fetch(url, { ...init, signal: AbortSignal.timeout(ms) });
|
||||
}
|
||||
const ac = new AbortController();
|
||||
const timer = setTimeout(() => ac.abort(), ms);
|
||||
return fetch(url, { ...init, signal: ac.signal }).finally(() => clearTimeout(timer));
|
||||
}
|
||||
const PANEL_VERSION = '1.6.37';
|
||||
const SIDEBAR_ACTIVE_MS = 2 * 60 * 60 * 1000;
|
||||
const PIN_STORAGE_KEY = 'pchat_pinned_token';
|
||||
const PINNED_LIST_KEY = 'pchat_pinned_tokens';
|
||||
/** v1.6.68:/?token=ct_xxx 深链选中并聚焦回复框 */
|
||||
let deepLinkToken = null;
|
||||
try {
|
||||
const _dl = new URLSearchParams(location.search);
|
||||
deepLinkToken = _dl.get('token') || _dl.get('ct');
|
||||
} catch {}
|
||||
const MODE_LABELS = { standard: '复盘', turbo: '直通', optimize: '优化' };
|
||||
const TIMED_REPLY_FIXED = [
|
||||
{ label: '自动', ms: 0 },
|
||||
@@ -332,15 +305,12 @@ function togglePinToken(token) {
|
||||
updateTaskFooter();
|
||||
}
|
||||
|
||||
const STARTUP_PROMPT = `请使用 persistent-chat MCP(VSIX 4.6.0-4tools · 仅 4 工具):
|
||||
const STARTUP_PROMPT = `请使用 persistent-chat MCP(卡若复盘 + 续跑三段式):
|
||||
1. select_conversation / init_conversation(cursorTitle=当前标签名)。
|
||||
2. **新计划必须 newPlan**:无关新任务 → init_conversation(newPlan:true, planLabel=计划名, cursorTitle=标签名)。
|
||||
3. 每轮:执行 → **[卡若复盘]** 🎯📌💡📝▶ → wait_for_user_input 长挂起;**每轮只 wait 一次**。
|
||||
4. **[RENEWAL] 零工**:同 turn 立刻 renewalOnly 再 wait;禁止 RENEWAL 之间夹 Read/Shell。
|
||||
5. 续跑在 **13458 面板** 或 **侧栏** 发下一条;勿在 Cursor 聊天打「继续」。
|
||||
6. 「结束持久对话」才停循环。
|
||||
|
||||
MCP 4 工具:init_conversation · select_conversation · wait_for_user_input · merge_conversation
|
||||
2. **新计划必须 newPlan**:若与旧任务无关的新任务 → init_conversation(newPlan:true, planLabel=计划名, cursorTitle=标签名),禁止挂旧线程。
|
||||
3. 每轮:执行 → **[卡若复盘]** 🎯📌💡📝▶ → wait(\\\`\\\`\\\`text 包裹)。
|
||||
4. 挂起后续跑:①追问→「追问完成」→②WebSearch+GitHub→「检索完成」→③执行▶。
|
||||
5. 「结束持久对话」才停循环。
|
||||
|
||||
第一项任务:`;
|
||||
|
||||
@@ -382,10 +352,10 @@ function sessionLastActiveAt(s) {
|
||||
return Number.isFinite(t) ? t : 0;
|
||||
}
|
||||
|
||||
/** 真正保持连接:挂起 / 续跑 / 活跃 wait(废弃会话不算) */
|
||||
/** 真正保持连接:挂起 / 续跑 / 活跃 wait */
|
||||
function isSessionConnected(s) {
|
||||
if (!s || s.isAbandoned || s.status === 'abandoned') return false;
|
||||
return isAgentSuspended(s) || !!(s.autoRunning || s.isActiveWait || s.connectionHeld || s.isHung || s.status === 'holding');
|
||||
if (!s) return false;
|
||||
return isAgentSuspended(s) || !!(s.autoRunning || s.isActiveWait);
|
||||
}
|
||||
|
||||
/** 2 小时内有实质对话(有消息且最近活跃) */
|
||||
@@ -541,12 +511,10 @@ function sessionMode(s) {
|
||||
}
|
||||
|
||||
function sessionAutoOn(s) {
|
||||
if (isVsix4Tools()) return s.sessionAutoContinue === true;
|
||||
return s.sessionAutoContinue !== false;
|
||||
}
|
||||
|
||||
function sessionTimedOn(s) {
|
||||
if (isVsix4Tools()) return s.sessionTimedReply === true;
|
||||
if (typeof s.sessionTimedReply === 'boolean') return s.sessionTimedReply;
|
||||
return s.timedReplyEnabled !== false;
|
||||
}
|
||||
@@ -702,47 +670,16 @@ function patchDetailDynamic() {
|
||||
});
|
||||
}
|
||||
|
||||
function setConnected(ok, meta) {
|
||||
const connEl = document.getElementById('conn');
|
||||
if (!connEl) return;
|
||||
if (ok) {
|
||||
const variant = (meta && meta.vsixVariant) || hubMeta.vsixVariant || '4tools';
|
||||
const tools = (meta && meta.mcpToolCount) || hubMeta.mcpToolCount || 4;
|
||||
connEl.innerHTML = `<span class="dot"></span> 已连接 · ${variant} · MCP ${tools}`;
|
||||
connEl.className = 'conn-pill ok';
|
||||
} else {
|
||||
connEl.innerHTML = '<span class="dot warn"></span> 离线';
|
||||
connEl.className = 'conn-pill';
|
||||
}
|
||||
}
|
||||
|
||||
async function pingHealth() {
|
||||
try {
|
||||
const r = await fetchWithTimeout('/api/health', 8000, { cache: 'no-store' });
|
||||
if (!r.ok) throw new Error('health ' + r.status);
|
||||
const d = await r.json();
|
||||
hubMeta = { ...hubMeta, ...d };
|
||||
setConnected(true, d);
|
||||
const ver = document.getElementById('panelVer');
|
||||
if (ver) ver.textContent = `v${d.panelVersion || PANEL_VERSION} · MCP ${d.mcpToolCount || 4}`;
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn('pingHealth failed', e);
|
||||
setConnected(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchState(){
|
||||
if (isTyping() && Date.now() < pollPauseUntil + 120000) return;
|
||||
try {
|
||||
const r = await fetchWithTimeout('/api/state?lite=1', 45000, { cache: 'no-store' });
|
||||
if (!r.ok) throw new Error('state ' + r.status);
|
||||
const r = await fetch('/api/state', { cache: 'no-store' });
|
||||
state = await r.json();
|
||||
if (!Array.isArray(state.sessions)) state.sessions = [];
|
||||
if (state.vsixVariant) hubMeta.vsixVariant = state.vsixVariant;
|
||||
if (state.mcpToolCount) hubMeta.mcpToolCount = state.mcpToolCount;
|
||||
setConnected(true, hubMeta);
|
||||
const connEl = document.getElementById('conn');
|
||||
if (connEl && !connEl.classList.contains('ok')) {
|
||||
connEl.innerHTML = '<span class="dot"></span> 已连接';
|
||||
connEl.className = 'conn-pill ok';
|
||||
}
|
||||
|
||||
const waiting = state.sessions.filter(s => s.status === 'waiting' || s.hasPending);
|
||||
const active = state.activeWaitToken;
|
||||
@@ -798,10 +735,12 @@ async function fetchState(){
|
||||
patchTimedPresetUi(getTimedMs(selSess || {}));
|
||||
}
|
||||
updateTaskFooter();
|
||||
applyDeepLinkToken();
|
||||
} catch (e) {
|
||||
console.error('fetchState failed', e);
|
||||
setConnected(false);
|
||||
const connEl = document.getElementById('conn');
|
||||
if (connEl) {
|
||||
connEl.innerHTML = '<span class="dot warn"></span> 离线';
|
||||
connEl.className = 'conn-pill';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -825,21 +764,6 @@ function jumpToWaiting(){
|
||||
}
|
||||
}
|
||||
|
||||
function applyDeepLinkToken() {
|
||||
if (!deepLinkToken) return;
|
||||
const tok = deepLinkToken;
|
||||
deepLinkToken = null;
|
||||
const hit = state.sessions.find((s) => s.token === tok);
|
||||
if (!hit) return;
|
||||
selectSession(tok);
|
||||
userPinnedSelection = true;
|
||||
const ta = document.getElementById('replyBox');
|
||||
if (ta) {
|
||||
ta.focus();
|
||||
ta.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
function selectSession(token) {
|
||||
selected = token;
|
||||
userPinnedSelection = true;
|
||||
@@ -872,14 +796,11 @@ function sessionLiveBadge(s) {
|
||||
return { text: '挂起', cls: 'suspend-waiting', title: 'Agent 已 wait_for_user_input,等待你回复' };
|
||||
}
|
||||
if (s.autoRunning || s.isActiveWait) return { text: '续跑', cls: 'suspend-auto', title: '自动续跑中' };
|
||||
if (isConnectionOnly(s)) return { text: '待wait', cls: 'suspend-auto', title: '仅保活连接,Cursor 尚未调用 wait_for_user_input' };
|
||||
if (isConnectionOnly(s)) return { text: '保活', cls: 'suspend-auto', title: '计划保活中,Cursor 尚未调用 wait' };
|
||||
return null;
|
||||
}
|
||||
|
||||
function sessionBadge(s) {
|
||||
if (s.isAbandoned || s.status === 'abandoned') {
|
||||
return { text: '废弃', cls: 'idle' };
|
||||
}
|
||||
const live = sessionLiveBadge(s);
|
||||
if (live) {
|
||||
const sideCls = live.cls === 'suspend-waiting' ? 'waiting' : (live.text === '续跑' ? 'auto' : 'waiting');
|
||||
@@ -953,7 +874,6 @@ function patchSessionItem(div, s) {
|
||||
if (selected === s.token) cls.push('active');
|
||||
if (isTokenPinned(s.token)) cls.push('pinned');
|
||||
if (s.hasPending || s.status === 'waiting' || s.connectionHeld || s.isHung) cls.push('waiting');
|
||||
if (s.isAbandoned || s.status === 'abandoned') cls.push('abandoned');
|
||||
div.className = cls.join(' ');
|
||||
div.oncontextmenu = (e) => showSessContextMenu(e, s.token);
|
||||
const badge = sessionBadge(s);
|
||||
@@ -983,7 +903,7 @@ function patchSessionItem(div, s) {
|
||||
}
|
||||
const pendEl = div.querySelector('.sess-pending');
|
||||
if (pendEl) {
|
||||
pendEl.textContent = preview ? `挂起: ${preview}` : (s.steerQueueCount ? `排队 ${s.steerQueueCount} 条` : (isAgentSuspended(s) ? '挂起中,可面板回复…' : (isConnectionOnly(s) ? '未挂起,等 Agent 调 wait…' : '')));
|
||||
pendEl.textContent = preview ? `挂起: ${preview}` : (s.steerQueueCount ? `排队 ${s.steerQueueCount} 条` : (isAgentSuspended(s) ? '挂起中,可面板回复…' : (isConnectionOnly(s) ? '保活中,等 Agent wait 接入…' : '')));
|
||||
pendEl.style.display = preview || s.hasPending || s.steerQueueCount || s.connectionHeld ? 'block' : 'none';
|
||||
}
|
||||
const badgeEl = div.querySelector('.badge');
|
||||
@@ -1050,13 +970,13 @@ function renderSidebar(){
|
||||
fold.id = 'sidebarStaleFold';
|
||||
const head = document.createElement('div');
|
||||
head.className = 'sidebar-fold-head';
|
||||
const foldLabel = '▸ 已废弃 · 超过2小时无对话';
|
||||
const foldLabel = '▸ 已归类 · 未连接或超过2小时';
|
||||
head.innerHTML = `<span>${foldLabel}</span><span class="count">${stale.length}</span>`;
|
||||
head.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
sidebarStaleExpanded = !sidebarStaleExpanded;
|
||||
fold.classList.toggle('open', sidebarStaleExpanded);
|
||||
head.querySelector('span').textContent = (sidebarStaleExpanded ? '▾' : '▸') + ' 已废弃 · 超过2小时无对话';
|
||||
head.querySelector('span').textContent = (sidebarStaleExpanded ? '▾' : '▸') + ' 已归类 · 未连接或超过2小时';
|
||||
lastSidebarKey = '';
|
||||
};
|
||||
const body = document.createElement('div');
|
||||
@@ -1064,7 +984,7 @@ function renderSidebar(){
|
||||
fold.appendChild(head);
|
||||
fold.appendChild(body);
|
||||
list.appendChild(fold);
|
||||
if (sidebarStaleExpanded) head.querySelector('span').textContent = '▾ 已废弃 · 超过2小时无对话';
|
||||
if (sidebarStaleExpanded) head.querySelector('span').textContent = '▾ 已归类 · 未连接或超过2小时';
|
||||
for (const s of stale) {
|
||||
keep.add(s.token);
|
||||
ensureSessionRow(list, body, s);
|
||||
@@ -1136,12 +1056,7 @@ function renderDetail(){
|
||||
banner += `<div class="steer-queue"><div class="steer-head">📥 排队方向(${s.steerQueueCount})· wait 后送达 · 可单删 <button type="button" class="ghost" id="steerClearAll">清空</button></div>${steerItems}</div>`;
|
||||
}
|
||||
if (suspended) {
|
||||
const sub = (s.panelSubTask || review.nextStep || '').trim();
|
||||
if (sub && !review.isIdleNext) {
|
||||
banner += `<div class="status-banner ok" style="border-color:rgba(89,211,156,.5)">📋 <strong>子任务已挂起</strong> · 请到本面板点「▶ 点击继续开发」分配执行<br/><span style="opacity:.9;font-size:13px">${esc(sub)}</span><br/><small style="opacity:.75">勿在 Cursor 对话框输入「继续」</small></div>`;
|
||||
} else {
|
||||
banner += `<div class="status-banner ok">⏸ <strong>已挂起</strong> · 请在本面板点「继续开发」或发消息续跑</div>`;
|
||||
}
|
||||
banner += `<div class="status-banner ok">⏸ <strong>已挂起</strong> · Hub 已收到 <code>wait_for_user_input</code>,可从本面板回复或点「继续开发」</div>`;
|
||||
} else if (connOnly) {
|
||||
banner += `<div class="status-banner proc">🔗 <strong>计划保活中(未挂起)</strong> · Cursor 里该任务若还在跑、未调用 wait,面板不会显示挂起;请让 Agent 复盘后 <code>wait_for_user_input</code>,或在本面板发消息排队</div>`;
|
||||
}
|
||||
@@ -1186,27 +1101,6 @@ function renderDetail(){
|
||||
const cfgAutoCls = autoOn ? 'on' : '';
|
||||
const cfgTimedCls = timedOn ? 'on' : '';
|
||||
const timedMs = getTimedMs(s);
|
||||
const autoTimedControls = isVsix4Tools()
|
||||
? `<span class="chip on" title="4tools:仅面板/侧栏手动发令,Hub 不自动续跑">4tools · 手动发令</span>
|
||||
<span class="chip" title="MCP 4 工具与 Cursor 侧栏一致">MCP 4 · init/select/wait/merge</span>`
|
||||
: `<label class="chip ${cfgAutoCls}" id="sessAutoToggle" title="自动续跑">
|
||||
<input type="checkbox" id="sessAutoCb" ${autoOn ? 'checked' : ''} hidden /> 自动续跑
|
||||
</label>
|
||||
<label class="chip ${cfgTimedCls}" id="timedReplyToggle" title="定时保活(Hub 按间隔发继续)">
|
||||
<input type="checkbox" id="timedReplyCb" ${timedOn ? 'checked' : ''} hidden /> 自动定时
|
||||
</label>`;
|
||||
const timedPresetControls = isVsix4Tools()
|
||||
? ''
|
||||
: `${TIMED_REPLY_FIXED.map((p) =>
|
||||
`<button type="button" class="chip timed-preset${timedMs === p.ms ? ' on' : ''}" data-ms="${p.ms}">${p.label}</button>`
|
||||
).join('')}
|
||||
<label class="chip timed-custom${(timedMs > 0 && !TIMED_REPLY_FIXED.some((p) => p.ms === timedMs)) ? ' on' : ''}" title="自定义分钟">
|
||||
<input id="timedCustomMin" type="number" min="1" max="999" placeholder="分" value="${(timedMs > 0 && !TIMED_REPLY_FIXED.some((p) => p.ms === timedMs)) ? Math.max(1, Math.round(timedMs / 60000)) : ''}" />
|
||||
<span>自定义</span>
|
||||
</label>`;
|
||||
const composerLegend = isVsix4Tools()
|
||||
? '4tools:本面板输入 → 排队 → Agent wait 首 poll 送达 Cursor → 自动续跑 ▶(勿开 turbo 自动续跑)'
|
||||
: '定时:自动=10秒保活 · 10秒/30秒/4分/9分 · 排队方向可单删';
|
||||
det.innerHTML = `
|
||||
<div class="header2">
|
||||
<div class="title">${esc(sessionDisplayTitle(s))} ${titleBadge}</div>
|
||||
@@ -1215,13 +1109,24 @@ function renderDetail(){
|
||||
</div>
|
||||
<div class="control-bar">
|
||||
<span class="chip-label">控制</span>
|
||||
${autoTimedControls}
|
||||
<label class="chip ${cfgAutoCls}" id="sessAutoToggle" title="自动续跑">
|
||||
<input type="checkbox" id="sessAutoCb" ${autoOn ? 'checked' : ''} hidden /> 自动续跑
|
||||
</label>
|
||||
<label class="chip ${cfgTimedCls}" id="timedReplyToggle" title="定时保活(Hub 按间隔发继续)">
|
||||
<input type="checkbox" id="timedReplyCb" ${timedOn ? 'checked' : ''} hidden /> 自动定时
|
||||
</label>
|
||||
<div class="mode-switch" id="sessModeSwitch">
|
||||
<button type="button" class="mode-btn${standardOn ? ' active' : ''}" data-mode="standard">复盘</button>
|
||||
<button type="button" class="mode-btn turbo${turboOn ? ' active' : ''}" data-mode="turbo">直通</button>
|
||||
<button type="button" class="mode-btn optimize${optimizeOn ? ' active' : ''}" data-mode="optimize">优化</button>
|
||||
</div>
|
||||
${timedPresetControls}
|
||||
${TIMED_REPLY_FIXED.map((p) =>
|
||||
`<button type="button" class="chip timed-preset${timedMs === p.ms ? ' on' : ''}" data-ms="${p.ms}">${p.label}</button>`
|
||||
).join('')}
|
||||
<label class="chip timed-custom${(timedMs > 0 && !TIMED_REPLY_FIXED.some((p) => p.ms === timedMs)) ? ' on' : ''}" title="自定义分钟">
|
||||
<input id="timedCustomMin" type="number" min="1" max="999" placeholder="分" value="${(timedMs > 0 && !TIMED_REPLY_FIXED.some((p) => p.ms === timedMs)) ? Math.max(1, Math.round(timedMs / 60000)) : ''}" />
|
||||
<span>自定义</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msg" id="msgBox">
|
||||
@@ -1243,7 +1148,7 @@ function renderDetail(){
|
||||
<span class="hint">${composerHint}</span>
|
||||
<button id="sendBtn">${minimalComposer ? '插队发送 →' : (canSend ? '发送回复 →' : '排队发送 →')}</button>
|
||||
</div>
|
||||
<div class="legend">${composerLegend}</div>
|
||||
<div class="legend">定时:自动=10秒保活 · 10秒/30秒/4分/9分 · 排队方向可单删</div>
|
||||
</div>
|
||||
`;
|
||||
{
|
||||
@@ -1458,7 +1363,7 @@ async function manualContinue() {
|
||||
return;
|
||||
}
|
||||
lastSent[selected] = { at: Date.now(), text: (j.text || '继续').slice(0, 80), auto: false };
|
||||
showToast('▶ 子任务已从面板发出 · Cursor 将自动续跑(stop hook)', 'ok', 5500);
|
||||
showToast('▶ 已发送继续 · Cursor 应马上开跑', 'ok', 4500);
|
||||
lastSidebarKey = '';
|
||||
lastDetailKey = '';
|
||||
await fetchState();
|
||||
@@ -1829,11 +1734,9 @@ if ('Notification' in window && Notification.permission === 'default') {
|
||||
}
|
||||
|
||||
setInterval(() => { if (!isTyping()) fetchState(); }, 2000);
|
||||
pingHealth().finally(() => {
|
||||
fetchState().catch((e) => {
|
||||
console.error('fetchState failed', e);
|
||||
showToast('面板加载失败,请 Cmd+Shift+R 硬刷新', 'err', 6000);
|
||||
});
|
||||
fetchState().catch((e) => {
|
||||
console.error('fetchState failed', e);
|
||||
showToast('面板加载失败,请 Cmd+Shift+R 硬刷新', 'err', 6000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user