helloloop 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +230 -506
  3. package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
  4. package/hosts/gemini/extension/gemini-extension.json +1 -1
  5. package/native/windows-hidden-shell-proxy/HelloLoopHiddenShellProxy.csproj +11 -0
  6. package/native/windows-hidden-shell-proxy/Program.cs +498 -0
  7. package/package.json +4 -2
  8. package/src/activity_projection.mjs +294 -0
  9. package/src/analyze_confirmation.mjs +3 -1
  10. package/src/analyzer.mjs +2 -1
  11. package/src/auto_execution_options.mjs +13 -0
  12. package/src/background_launch.mjs +73 -0
  13. package/src/cli.mjs +49 -1
  14. package/src/cli_analyze_command.mjs +9 -5
  15. package/src/cli_args.mjs +102 -37
  16. package/src/cli_command_handlers.mjs +44 -4
  17. package/src/cli_support.mjs +2 -0
  18. package/src/dashboard_command.mjs +371 -0
  19. package/src/dashboard_tui.mjs +289 -0
  20. package/src/dashboard_web.mjs +351 -0
  21. package/src/dashboard_web_client.mjs +167 -0
  22. package/src/dashboard_web_page.mjs +49 -0
  23. package/src/engine_event_parser_codex.mjs +167 -0
  24. package/src/engine_process_support.mjs +1 -0
  25. package/src/engine_selection.mjs +24 -0
  26. package/src/engine_selection_probe.mjs +10 -6
  27. package/src/engine_selection_settings.mjs +12 -19
  28. package/src/execution_interactivity.mjs +12 -0
  29. package/src/host_continuation.mjs +305 -0
  30. package/src/install_codex.mjs +20 -8
  31. package/src/install_shared.mjs +9 -0
  32. package/src/node_process_launch.mjs +28 -0
  33. package/src/process.mjs +2 -0
  34. package/src/runner_execute_task.mjs +4 -0
  35. package/src/runner_execution_support.mjs +69 -3
  36. package/src/runner_once.mjs +4 -0
  37. package/src/runner_status.mjs +63 -7
  38. package/src/runtime_engine_support.mjs +41 -4
  39. package/src/runtime_engine_task.mjs +7 -0
  40. package/src/runtime_settings.mjs +105 -0
  41. package/src/runtime_settings_loader.mjs +19 -0
  42. package/src/shell_invocation.mjs +227 -9
  43. package/src/supervisor_cli_support.mjs +3 -2
  44. package/src/supervisor_guardian.mjs +307 -0
  45. package/src/supervisor_runtime.mjs +138 -82
  46. package/src/supervisor_state.mjs +64 -0
  47. package/src/supervisor_watch.mjs +92 -48
  48. package/src/terminal_session_limits.mjs +1 -21
  49. package/src/windows_hidden_shell_proxy.mjs +405 -0
  50. package/src/workspace_registry.mjs +155 -0
@@ -0,0 +1,167 @@
1
+ export const DASHBOARD_WEB_CSS = `
2
+ :root {
3
+ color-scheme: dark;
4
+ --bg: #081122;
5
+ --panel: #0f1a2f;
6
+ --panel-2: #13213c;
7
+ --border: rgba(148, 163, 184, 0.2);
8
+ --text: #e5eefc;
9
+ --muted: #9fb2d1;
10
+ --accent: #5eead4;
11
+ --warn: #fbbf24;
12
+ --danger: #f87171;
13
+ --ok: #34d399;
14
+ --shadow: 0 16px 40px rgba(0, 0, 0, 0.28);
15
+ font-family: Inter, "Segoe UI", system-ui, sans-serif;
16
+ }
17
+ * { box-sizing: border-box; }
18
+ body { margin: 0; background: radial-gradient(circle at top, #13284a 0%, var(--bg) 40%, #050b16 100%); color: var(--text); }
19
+ header { position: sticky; top: 0; z-index: 10; backdrop-filter: blur(14px); background: rgba(8, 17, 34, 0.86); border-bottom: 1px solid var(--border); padding: 20px 24px 16px; }
20
+ .title-row, .stats-row, .repo-header-top, .repo-meta { display: flex; gap: 12px; align-items: center; justify-content: space-between; flex-wrap: wrap; }
21
+ .title h1 { margin: 0; font-size: 24px; font-weight: 700; }
22
+ .title p { margin: 6px 0 0; color: var(--muted); font-size: 14px; }
23
+ .pill, .badge, .drawer-close { border-radius: 999px; border: 1px solid var(--border); background: rgba(19, 33, 60, 0.9); color: var(--text); }
24
+ .pill { display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; font-size: 13px; }
25
+ .stats-row { margin-top: 16px; }
26
+ .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 12px; width: min(100%, 760px); }
27
+ .stat, .repo-board { background: rgba(15, 26, 47, 0.9); border: 1px solid var(--border); box-shadow: var(--shadow); }
28
+ .stat { border-radius: 16px; padding: 14px; }
29
+ .stat-label, .column-count, .repo-meta, .empty, .card-meta, .drawer-section h4 { color: var(--muted); font-size: 12px; }
30
+ .stat-value { margin-top: 6px; font-size: 22px; font-weight: 700; }
31
+ main { padding: 20px 24px 80px; display: grid; gap: 20px; }
32
+ .repo-board { border-radius: 22px; overflow: hidden; }
33
+ .repo-header { padding: 18px 20px 14px; border-bottom: 1px solid var(--border); background: linear-gradient(180deg, rgba(19, 33, 60, 0.96) 0%, rgba(15, 26, 47, 0.96) 100%); }
34
+ .repo-header h2 { margin: 0; font-size: 20px; }
35
+ .repo-meta { margin-top: 12px; font-size: 13px; }
36
+ .columns { display: grid; grid-template-columns: repeat(5, minmax(220px, 1fr)); gap: 16px; padding: 18px; overflow-x: auto; }
37
+ .column { min-height: 180px; background: rgba(9, 16, 29, 0.72); border: 1px solid rgba(148, 163, 184, 0.14); border-radius: 18px; padding: 14px; display: flex; flex-direction: column; gap: 12px; }
38
+ .column h3, .drawer h3 { margin: 0; font-size: 15px; }
39
+ .cards { display: grid; gap: 10px; align-content: start; }
40
+ .card { width: 100%; text-align: left; background: rgba(19, 33, 60, 0.96); border: 1px solid rgba(148, 163, 184, 0.16); border-radius: 14px; padding: 12px; cursor: pointer; color: inherit; }
41
+ .card:hover { border-color: rgba(94, 234, 212, 0.42); transform: translateY(-1px); }
42
+ .card-title { font-size: 14px; font-weight: 600; line-height: 1.45; }
43
+ .badges, .repo-badges { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
44
+ .badge { padding: 4px 10px; font-size: 11px; border-color: transparent; background: rgba(148, 163, 184, 0.14); }
45
+ .badge.ok { background: rgba(52, 211, 153, 0.15); color: var(--ok); }
46
+ .badge.warn { background: rgba(251, 191, 36, 0.14); color: var(--warn); }
47
+ .badge.danger { background: rgba(248, 113, 113, 0.14); color: var(--danger); }
48
+ .badge.accent { background: rgba(94, 234, 212, 0.12); color: var(--accent); }
49
+ .card-meta { margin-top: 10px; line-height: 1.5; }
50
+ aside.drawer { position: fixed; top: 0; right: 0; width: min(480px, 100vw); height: 100vh; background: rgba(6, 13, 26, 0.98); border-left: 1px solid var(--border); box-shadow: var(--shadow); transform: translateX(100%); transition: transform 180ms ease; padding: 24px; overflow-y: auto; z-index: 20; }
51
+ aside.drawer.open { transform: translateX(0); }
52
+ .drawer h3 { font-size: 20px; }
53
+ .drawer-section { margin-top: 18px; }
54
+ .drawer-section h4 { margin: 0 0 8px; text-transform: uppercase; }
55
+ .drawer-list { margin: 0; padding-left: 18px; line-height: 1.6; }
56
+ .drawer-close { position: sticky; top: 0; float: right; padding: 6px 12px; cursor: pointer; }
57
+ @media (max-width: 1100px) { .columns { grid-template-columns: repeat(2, minmax(240px, 1fr)); } }
58
+ @media (max-width: 760px) { header, main { padding-left: 14px; padding-right: 14px; } .columns { grid-template-columns: 1fr; } }
59
+ `;
60
+
61
+ export const DASHBOARD_WEB_JS = `
62
+ const STATUS_COLUMNS = [
63
+ { key: "pending", label: "待处理" },
64
+ { key: "in_progress", label: "进行中" },
65
+ { key: "done", label: "已完成" },
66
+ { key: "blocked", label: "阻塞" },
67
+ { key: "failed", label: "失败" },
68
+ ];
69
+ const boardEl = document.getElementById("board");
70
+ const statsEl = document.getElementById("stats");
71
+ const sessionPillEl = document.getElementById("session-pill");
72
+ const updatePillEl = document.getElementById("update-pill");
73
+ const drawerEl = document.getElementById("drawer");
74
+ const drawerContentEl = document.getElementById("drawer-content");
75
+ const drawerCloseEl = document.getElementById("drawer-close");
76
+ let currentSnapshot = window.__HELLOLOOP_INITIAL_SNAPSHOT__;
77
+
78
+ function badgeClass(kind) {
79
+ if (kind === "done" || kind === "running" || kind === "ready") return "ok";
80
+ if (kind === "blocked" || kind === "failed") return "danger";
81
+ if (kind === "retry_waiting" || kind === "watchdog_waiting") return "warn";
82
+ return "accent";
83
+ }
84
+
85
+ function formatRuntime(session) {
86
+ const runtime = session.runtime || {};
87
+ const bits = [runtime.status || "idle"];
88
+ if (Number.isFinite(Number(runtime.recoveryCount)) && Number(runtime.recoveryCount) > 0) bits.push("recovery=" + runtime.recoveryCount);
89
+ if (Number.isFinite(Number(runtime?.heartbeat?.idleSeconds)) && Number(runtime.heartbeat.idleSeconds) > 0) bits.push("idle=" + runtime.heartbeat.idleSeconds + "s");
90
+ return bits.join(" | ");
91
+ }
92
+
93
+ function groupTasks(tasks) {
94
+ const grouped = Object.fromEntries(STATUS_COLUMNS.map((column) => [column.key, []]));
95
+ for (const task of Array.isArray(tasks) ? tasks : []) {
96
+ const key = task.status || "pending";
97
+ if (!grouped[key]) grouped[key] = [];
98
+ grouped[key].push(task);
99
+ }
100
+ return grouped;
101
+ }
102
+
103
+ function renderStats(snapshot) {
104
+ const totals = snapshot.taskTotals || {};
105
+ const items = [["仓库总数", snapshot.repoCount || 0], ["活跃会话", snapshot.activeCount || 0], ["任务总计", totals.total || 0], ["待处理", totals.pending || 0], ["进行中", totals.inProgress || 0], ["已完成", totals.done || 0], ["阻塞", totals.blocked || 0], ["失败", totals.failed || 0]];
106
+ statsEl.innerHTML = items.map(([label, value]) => '<div class="stat"><div class="stat-label">' + label + '</div><div class="stat-value">' + value + '</div></div>').join("");
107
+ sessionPillEl.textContent = "仓库 " + (snapshot.repoCount || 0) + " · 活跃会话 " + (snapshot.activeCount || 0);
108
+ updatePillEl.textContent = "最近刷新 " + (snapshot.generatedAt || "unknown");
109
+ }
110
+
111
+ function renderTaskCard(task, session) {
112
+ const isCurrent = task.id && task.id === session.latestStatus?.taskId;
113
+ const docsCount = Array.isArray(task.docs) ? task.docs.length : 0;
114
+ const pathsCount = Array.isArray(task.paths) ? task.paths.length : 0;
115
+ return '<button class="card" data-session="' + encodeURIComponent(session.sessionId || "") + '" data-task="' + encodeURIComponent(task.id || "") + '"><div class="card-title">' + (task.title || task.id || "未命名任务") + '</div><div class="badges"><span class="badge accent">' + (task.priority || "P2") + '</span><span class="badge ' + badgeClass(task.status || "pending") + '">' + (task.status || "pending") + '</span><span class="badge ' + badgeClass(task.risk || "low") + '">' + (task.risk || "low") + '</span>' + (isCurrent ? '<span class="badge warn">当前执行</span>' : "") + '</div><div class="card-meta">docs ' + docsCount + ' · paths ' + pathsCount + '</div></button>';
116
+ }
117
+
118
+ function renderSessionBoard(session) {
119
+ const grouped = groupTasks(session.tasks || []);
120
+ const repoBadges = ['<span class="badge ' + badgeClass(session.supervisor?.status || "running") + '">supervisor ' + (session.supervisor?.status || "unknown") + '</span>', '<span class="badge ' + badgeClass(session.runtime?.status || "idle") + '">runtime ' + formatRuntime(session) + '</span>', (session.latestStatus?.taskTitle ? '<span class="badge accent">当前任务 ' + session.latestStatus.taskTitle + '</span>' : "")].filter(Boolean).join("");
121
+ const columnsHtml = STATUS_COLUMNS.map((column) => {
122
+ const tasks = grouped[column.key] || [];
123
+ return '<section class="column"><div><h3>' + column.label + '</h3><div class="column-count">' + tasks.length + ' 个任务</div></div><div class="cards">' + (tasks.length ? tasks.map((task) => renderTaskCard(task, session)).join("") : '<div class="empty">当前列为空</div>') + '</div></section>';
124
+ }).join("");
125
+ return '<section class="repo-board"><div class="repo-header"><div class="repo-header-top"><div><h2>' + session.repoName + '</h2><div class="repo-meta"><span>仓库:' + session.repoRoot + '</span><span>会话:' + session.sessionId + '</span></div></div><div class="repo-badges">' + repoBadges + '</div></div><div class="repo-meta"><span>当前动作:' + (session.activity?.current?.label || session.latestStatus?.message || session.runtime?.failureReason || "等待新事件") + '</span><span>宿主续跑:' + (session.hostResume?.issue?.label || (session.hostResume?.supervisorActive ? "后台仍在运行,可直接接续观察" : "需要按续跑提示继续")) + '</span></div></div><div class="columns">' + columnsHtml + '</div></section>';
126
+ }
127
+
128
+ function renderSnapshot(snapshot) {
129
+ currentSnapshot = snapshot;
130
+ renderStats(snapshot);
131
+ if (!Array.isArray(snapshot.sessions) || !snapshot.sessions.length) {
132
+ boardEl.innerHTML = '<section class="repo-board"><div class="repo-header"><h2>当前没有已登记仓库或后台会话</h2></div></section>';
133
+ return;
134
+ }
135
+ boardEl.innerHTML = snapshot.sessions.map(renderSessionBoard).join("");
136
+ }
137
+
138
+ function openDrawer(task, session) {
139
+ const docs = Array.isArray(task.docs) ? task.docs : [];
140
+ const paths = Array.isArray(task.paths) ? task.paths : [];
141
+ const acceptance = Array.isArray(task.acceptance) ? task.acceptance : [];
142
+ drawerContentEl.innerHTML = '<h3>' + (task.title || task.id || "未命名任务") + '</h3><div class="badges"><span class="badge accent">仓库 ' + session.repoName + '</span><span class="badge accent">优先级 ' + (task.priority || "P2") + '</span><span class="badge ' + badgeClass(task.status || "pending") + '">状态 ' + (task.status || "pending") + '</span><span class="badge ' + badgeClass(task.risk || "low") + '">风险 ' + (task.risk || "low") + '</span></div><div class="drawer-section"><h4>目标</h4><div>' + (task.goal || "无") + '</div></div><div class="drawer-section"><h4>文档</h4><ul class="drawer-list">' + (docs.length ? docs.map((item) => '<li>' + item + '</li>').join("") : "<li>无</li>") + '</ul></div><div class="drawer-section"><h4>路径</h4><ul class="drawer-list">' + (paths.length ? paths.map((item) => '<li>' + item + '</li>').join("") : "<li>无</li>") + '</ul></div><div class="drawer-section"><h4>验收</h4><ul class="drawer-list">' + (acceptance.length ? acceptance.map((item) => '<li>' + item + '</li>').join("") : "<li>无</li>") + '</ul></div>';
143
+ drawerEl.classList.add("open");
144
+ }
145
+
146
+ boardEl.addEventListener("click", (event) => {
147
+ const button = event.target.closest(".card");
148
+ if (!button) return;
149
+ const sessionId = decodeURIComponent(button.dataset.session || "");
150
+ const taskId = decodeURIComponent(button.dataset.task || "");
151
+ const session = (currentSnapshot.sessions || []).find((item) => item.sessionId === sessionId);
152
+ const task = (session?.tasks || []).find((item) => item.id === taskId);
153
+ if (session && task) openDrawer(task, session);
154
+ });
155
+
156
+ drawerCloseEl.addEventListener("click", () => drawerEl.classList.remove("open"));
157
+ window.addEventListener("keydown", (event) => { if (event.key === "Escape") drawerEl.classList.remove("open"); });
158
+
159
+ function connectEvents() {
160
+ const source = new EventSource("/events");
161
+ source.onmessage = (event) => { try { renderSnapshot(JSON.parse(event.data)); } catch (error) { console.error("snapshot parse failed", error); } };
162
+ source.onerror = () => { source.close(); setTimeout(connectEvents, 1500); };
163
+ }
164
+
165
+ renderSnapshot(currentSnapshot);
166
+ connectEvents();
167
+ `;
@@ -0,0 +1,49 @@
1
+ import { DASHBOARD_WEB_CSS, DASHBOARD_WEB_JS } from "./dashboard_web_client.mjs";
2
+
3
+ function escapeInlineJson(value) {
4
+ return JSON.stringify(value)
5
+ .replace(/</gu, "\\u003c")
6
+ .replace(/>/gu, "\\u003e")
7
+ .replace(/&/gu, "\\u0026");
8
+ }
9
+
10
+ export function renderDashboardWebHtml(options = {}) {
11
+ const initialSnapshot = options.initialSnapshot || {
12
+ generatedAt: "",
13
+ activeCount: 0,
14
+ taskTotals: {},
15
+ sessions: [],
16
+ };
17
+
18
+ return `<!doctype html>
19
+ <html lang="zh-CN">
20
+ <head>
21
+ <meta charset="utf-8" />
22
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
23
+ <title>HelloLoop Dashboard</title>
24
+ <style>${DASHBOARD_WEB_CSS}</style>
25
+ </head>
26
+ <body>
27
+ <header>
28
+ <div class="title-row">
29
+ <div class="title">
30
+ <h1>HelloLoop Dashboard</h1>
31
+ <p>本地实时多仓开发看板。页面会持续订阅后台会话状态,不依赖宿主聊天流刷新。</p>
32
+ </div>
33
+ <div class="pill" id="update-pill">等待首帧</div>
34
+ </div>
35
+ <div class="stats-row">
36
+ <div class="stats" id="stats"></div>
37
+ <div class="pill" id="session-pill">仓库 0 · 活跃会话 0</div>
38
+ </div>
39
+ </header>
40
+ <main id="board"></main>
41
+ <aside class="drawer" id="drawer">
42
+ <button class="drawer-close" id="drawer-close">关闭</button>
43
+ <div id="drawer-content"></div>
44
+ </aside>
45
+ <script>window.__HELLOLOOP_INITIAL_SNAPSHOT__ = ${escapeInlineJson(initialSnapshot)};</script>
46
+ <script>${DASHBOARD_WEB_JS}</script>
47
+ </body>
48
+ </html>`;
49
+ }
@@ -0,0 +1,167 @@
1
+ function normalizeWhitespace(value) {
2
+ return String(value || "").replace(/\s+/gu, " ").trim();
3
+ }
4
+
5
+ function stripMarkdown(value) {
6
+ return normalizeWhitespace(String(value || "")
7
+ .replace(/\*\*(.*?)\*\*/gu, "$1")
8
+ .replace(/`([^`]+)`/gu, "$1")
9
+ .replace(/^#+\s*/gmu, ""));
10
+ }
11
+
12
+ function shorten(value, maxLength = 160) {
13
+ const text = normalizeWhitespace(value);
14
+ if (!text) {
15
+ return "";
16
+ }
17
+ return text.length <= maxLength ? text : `${text.slice(0, Math.max(0, maxLength - 1))}…`;
18
+ }
19
+
20
+ function summarizeText(value, maxLength = 160) {
21
+ const sections = String(value || "")
22
+ .replace(/\r\n/gu, "\n")
23
+ .split(/\n\s*\n/gu)
24
+ .map((item) => stripMarkdown(item))
25
+ .filter(Boolean);
26
+ return shorten(sections[0] || stripMarkdown(value), maxLength);
27
+ }
28
+
29
+ function normalizeExitCode(value) {
30
+ const code = Number(value);
31
+ return Number.isFinite(code) ? code : null;
32
+ }
33
+
34
+ function normalizeTodoItems(items) {
35
+ const normalized = Array.isArray(items)
36
+ ? items.map((item) => ({
37
+ text: shorten(item?.text || "", 140),
38
+ completed: item?.completed === true,
39
+ }))
40
+ : [];
41
+ const completed = normalized.filter((item) => item.completed).length;
42
+ return {
43
+ items: normalized,
44
+ total: normalized.length,
45
+ completed,
46
+ pending: Math.max(0, normalized.length - completed),
47
+ };
48
+ }
49
+
50
+ function normalizeChanges(changes) {
51
+ return Array.isArray(changes)
52
+ ? changes.map((change) => ({
53
+ path: String(change?.path || "").trim(),
54
+ kind: String(change?.kind || "update").trim() || "update",
55
+ }))
56
+ : [];
57
+ }
58
+
59
+ function buildFileChangeLabel(changes) {
60
+ if (!changes.length) {
61
+ return "文件变更";
62
+ }
63
+ if (changes.length === 1) {
64
+ const change = changes[0];
65
+ return `${change.kind} ${shorten(change.path, 100)}`;
66
+ }
67
+ return `${changes.length} 个文件变更`;
68
+ }
69
+
70
+ export function parseCodexJsonlEventLine(line) {
71
+ const payload = JSON.parse(line);
72
+ const type = String(payload?.type || "").trim();
73
+
74
+ if (type === "thread.started") {
75
+ return {
76
+ kind: "thread",
77
+ status: "started",
78
+ label: "线程已启动",
79
+ threadId: String(payload.thread_id || "").trim(),
80
+ };
81
+ }
82
+
83
+ if (type === "turn.started") {
84
+ return {
85
+ kind: "turn",
86
+ status: "started",
87
+ label: "轮次开始",
88
+ };
89
+ }
90
+
91
+ if (type === "turn.completed") {
92
+ return {
93
+ kind: "turn",
94
+ status: "completed",
95
+ label: "轮次完成",
96
+ };
97
+ }
98
+
99
+ if (!type.startsWith("item.")) {
100
+ return {
101
+ kind: "event",
102
+ status: "info",
103
+ label: shorten(type, 120) || "事件更新",
104
+ };
105
+ }
106
+
107
+ const item = payload.item || {};
108
+ const itemId = String(item.id || "").trim();
109
+ const itemStatus = type === "item.started"
110
+ ? "in_progress"
111
+ : (String(item.status || "").trim() || "completed");
112
+
113
+ if (item.type === "reasoning") {
114
+ const summary = summarizeText(item.text || "", 180);
115
+ return {
116
+ kind: "reasoning",
117
+ itemId,
118
+ status: itemStatus,
119
+ label: summary || "推理更新",
120
+ summary,
121
+ };
122
+ }
123
+
124
+ if (item.type === "todo_list") {
125
+ const todo = normalizeTodoItems(item.items);
126
+ return {
127
+ kind: "todo",
128
+ itemId,
129
+ status: itemStatus,
130
+ label: todo.total > 0
131
+ ? `待办 ${todo.completed}/${todo.total}`
132
+ : "待办清单更新",
133
+ todo,
134
+ };
135
+ }
136
+
137
+ if (item.type === "command_execution") {
138
+ const command = shorten(item.command || "", 220);
139
+ return {
140
+ kind: "command",
141
+ itemId,
142
+ status: itemStatus,
143
+ label: command || "命令执行",
144
+ command,
145
+ exitCode: normalizeExitCode(item.exit_code),
146
+ outputSummary: summarizeText(item.aggregated_output || "", 160),
147
+ };
148
+ }
149
+
150
+ if (item.type === "file_change") {
151
+ const changes = normalizeChanges(item.changes);
152
+ return {
153
+ kind: "file_change",
154
+ itemId,
155
+ status: itemStatus,
156
+ label: buildFileChangeLabel(changes),
157
+ changes,
158
+ };
159
+ }
160
+
161
+ return {
162
+ kind: String(item.type || "item").trim() || "item",
163
+ itemId,
164
+ status: itemStatus,
165
+ label: summarizeText(item.text || item.command || item.type || type, 160) || "活动更新",
166
+ };
167
+ }
@@ -26,6 +26,7 @@ export function runChild(command, args, options = {}) {
26
26
  },
27
27
  stdio: ["pipe", "pipe", "pipe"],
28
28
  shell: Boolean(options.shell),
29
+ windowsHide: process.platform === "win32",
29
30
  });
30
31
 
31
32
  let stdout = "";
@@ -273,6 +273,30 @@ export async function resolveEngineSelection({
273
273
  };
274
274
  }
275
275
 
276
+ const recommendedEngine = recommendEngine({
277
+ hostContext,
278
+ availableEngines,
279
+ projectConfig,
280
+ userSettings,
281
+ });
282
+ if (!interactive && recommendedEngine) {
283
+ return buildResolution({
284
+ engine: recommendedEngine,
285
+ source: "remembered_default",
286
+ basis: [
287
+ buildRecommendationBasis({
288
+ hostContext,
289
+ projectConfig,
290
+ userSettings,
291
+ recommendedEngine,
292
+ }) || `继续沿用已确认过的默认执行引擎:${getEngineDisplayName(recommendedEngine)}。`,
293
+ "当前为无人值守续跑场景,HelloLoop 直接复用之前确认过的可用引擎。",
294
+ ],
295
+ hostContext,
296
+ probes,
297
+ });
298
+ }
299
+
276
300
  if (!interactive) {
277
301
  return {
278
302
  ok: false,
@@ -1,7 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
 
3
3
  import { getEngineDisplayName, getEngineMetadata, listKnownEngines, normalizeEngineName } from "./engine_metadata.mjs";
4
- import { resolveCliInvocation } from "./shell_invocation.mjs";
4
+ import { resolveCliInvocation, resolveCodexInvocation } from "./shell_invocation.mjs";
5
5
 
6
6
  function resolveExecutableOverride(policy = {}, engine) {
7
7
  const envExecutable = String(process.env[`HELLOLOOP_${String(engine || "").toUpperCase()}_EXECUTABLE`] || "").trim();
@@ -13,11 +13,14 @@ function resolveExecutableOverride(policy = {}, engine) {
13
13
 
14
14
  function probeEngineAvailability(engine, policy = {}) {
15
15
  const meta = getEngineMetadata(engine);
16
- const invocation = resolveCliInvocation({
17
- commandName: meta.commandName,
18
- toolDisplayName: meta.displayName,
19
- explicitExecutable: resolveExecutableOverride(policy, engine),
20
- });
16
+ const explicitExecutable = resolveExecutableOverride(policy, engine);
17
+ const invocation = engine === "codex"
18
+ ? resolveCodexInvocation({ explicitExecutable })
19
+ : resolveCliInvocation({
20
+ commandName: meta.commandName,
21
+ toolDisplayName: meta.displayName,
22
+ explicitExecutable,
23
+ });
21
24
 
22
25
  if (invocation.error) {
23
26
  return {
@@ -30,6 +33,7 @@ function probeEngineAvailability(engine, policy = {}) {
30
33
  const result = spawnSync(invocation.command, [...invocation.argsPrefix, "--version"], {
31
34
  encoding: "utf8",
32
35
  shell: invocation.shell,
36
+ windowsHide: process.platform === "win32",
33
37
  });
34
38
  const ok = result.status === 0;
35
39
  return {
@@ -3,6 +3,14 @@ import path from "node:path";
3
3
 
4
4
  import { fileExists, readJson, readText, timestampForFile, writeJson, writeText } from "./common.mjs";
5
5
  import { normalizeEngineName } from "./engine_metadata.mjs";
6
+ import {
7
+ defaultObserverRetrySettings,
8
+ defaultSupervisorKeepAliveSettings,
9
+ defaultTerminalConcurrencySettings,
10
+ normalizeObserverRetrySettings,
11
+ normalizeSupervisorKeepAliveSettings,
12
+ normalizeTerminalConcurrencySettings,
13
+ } from "./runtime_settings.mjs";
6
14
 
7
15
  function defaultEmailNotificationSettings() {
8
16
  return {
@@ -24,15 +32,6 @@ function defaultEmailNotificationSettings() {
24
32
  };
25
33
  }
26
34
 
27
- function defaultTerminalConcurrencySettings() {
28
- return {
29
- enabled: true,
30
- visibleMax: 8,
31
- backgroundMax: 8,
32
- totalMax: 8,
33
- };
34
- }
35
-
36
35
  function defaultUserSettings() {
37
36
  return {
38
37
  defaultEngine: "",
@@ -42,6 +41,8 @@ function defaultUserSettings() {
42
41
  },
43
42
  runtime: {
44
43
  terminalConcurrency: defaultTerminalConcurrencySettings(),
44
+ observerRetry: defaultObserverRetrySettings(),
45
+ supervisorKeepAlive: defaultSupervisorKeepAliveSettings(),
45
46
  },
46
47
  };
47
48
  }
@@ -66,16 +67,6 @@ function normalizePositiveInteger(value, fallback, minimum = 1) {
66
67
  return numericValue;
67
68
  }
68
69
 
69
- function normalizeTerminalConcurrencySettings(settings = {}) {
70
- const defaults = defaultTerminalConcurrencySettings();
71
- return {
72
- enabled: normalizeBoolean(settings?.enabled, defaults.enabled),
73
- visibleMax: normalizePositiveInteger(settings?.visibleMax, defaults.visibleMax, 0),
74
- backgroundMax: normalizePositiveInteger(settings?.backgroundMax, defaults.backgroundMax, 0),
75
- totalMax: normalizePositiveInteger(settings?.totalMax, defaults.totalMax, 0),
76
- };
77
- }
78
-
79
70
  function mergeValueBySchema(schemaValue, baseValue, patchValue) {
80
71
  if (!isPlainObject(schemaValue)) {
81
72
  return patchValue === undefined ? baseValue : patchValue;
@@ -141,6 +132,8 @@ export function syncUserSettingsShape(settings = {}) {
141
132
  },
142
133
  runtime: {
143
134
  terminalConcurrency: normalizeTerminalConcurrencySettings(settings?.runtime?.terminalConcurrency || {}),
135
+ observerRetry: normalizeObserverRetrySettings(settings?.runtime?.observerRetry || {}),
136
+ supervisorKeepAlive: normalizeSupervisorKeepAliveSettings(settings?.runtime?.supervisorKeepAlive || {}),
144
137
  },
145
138
  };
146
139
  }
@@ -0,0 +1,12 @@
1
+ export function shouldPromptForEngineSelection(options = {}) {
2
+ if (options.yes) {
3
+ return false;
4
+ }
5
+ if (options.detach) {
6
+ return false;
7
+ }
8
+ if (process.env.HELLOLOOP_SUPERVISOR_ACTIVE === "1") {
9
+ return false;
10
+ }
11
+ return Boolean(process.stdout?.isTTY);
12
+ }