@virtengine/openfleet 0.25.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.
- package/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
package/ui/tabs/logs.js
ADDED
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
/* ─────────────────────────────────────────────────────────────
|
|
2
|
+
* Tab: Logs — system logs, agent log library, git snapshot
|
|
3
|
+
* ────────────────────────────────────────────────────────────── */
|
|
4
|
+
import { h } from "preact";
|
|
5
|
+
import {
|
|
6
|
+
useState,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useCallback,
|
|
10
|
+
useMemo,
|
|
11
|
+
} from "preact/hooks";
|
|
12
|
+
import htm from "htm";
|
|
13
|
+
|
|
14
|
+
const html = htm.bind(h);
|
|
15
|
+
|
|
16
|
+
import { haptic, showAlert, getTg, openLink } from "../modules/telegram.js";
|
|
17
|
+
import { apiFetch, sendCommandToChat } from "../modules/api.js";
|
|
18
|
+
import {
|
|
19
|
+
logsData,
|
|
20
|
+
logsLines,
|
|
21
|
+
gitDiff,
|
|
22
|
+
gitBranches,
|
|
23
|
+
agentLogFiles,
|
|
24
|
+
agentLogFile,
|
|
25
|
+
agentLogTail,
|
|
26
|
+
agentLogLines,
|
|
27
|
+
agentLogQuery,
|
|
28
|
+
agentContext,
|
|
29
|
+
agentWorkspaceTarget,
|
|
30
|
+
loadLogs,
|
|
31
|
+
loadAgentLogFileList,
|
|
32
|
+
loadAgentLogTailData,
|
|
33
|
+
loadAgentContextData,
|
|
34
|
+
showToast,
|
|
35
|
+
scheduleRefresh,
|
|
36
|
+
} from "../modules/state.js";
|
|
37
|
+
import { navigateTo } from "../modules/router.js";
|
|
38
|
+
import { ICONS } from "../modules/icons.js";
|
|
39
|
+
import { formatBytes } from "../modules/utils.js";
|
|
40
|
+
import { Card, Badge, EmptyState, SkeletonCard, Modal } from "../components/shared.js";
|
|
41
|
+
import { SearchInput } from "../components/forms.js";
|
|
42
|
+
|
|
43
|
+
/* ─── Log level helpers ─── */
|
|
44
|
+
const LOG_LEVELS = [
|
|
45
|
+
{ value: "all", label: "All" },
|
|
46
|
+
{ value: "info", label: "Info" },
|
|
47
|
+
{ value: "warn", label: "Warn" },
|
|
48
|
+
{ value: "error", label: "Error" },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function filterByLevel(text, level) {
|
|
52
|
+
if (!text || level === "all") return text;
|
|
53
|
+
return text
|
|
54
|
+
.split("\n")
|
|
55
|
+
.filter((line) => {
|
|
56
|
+
const lower = line.toLowerCase();
|
|
57
|
+
if (level === "error")
|
|
58
|
+
return (
|
|
59
|
+
lower.includes("error") ||
|
|
60
|
+
lower.includes("err") ||
|
|
61
|
+
lower.includes("fatal")
|
|
62
|
+
);
|
|
63
|
+
if (level === "warn")
|
|
64
|
+
return (
|
|
65
|
+
lower.includes("warn") ||
|
|
66
|
+
lower.includes("warning") ||
|
|
67
|
+
lower.includes("error") ||
|
|
68
|
+
lower.includes("fatal")
|
|
69
|
+
);
|
|
70
|
+
return true;
|
|
71
|
+
})
|
|
72
|
+
.join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ─── Helpers ─── */
|
|
76
|
+
function escapeRegex(str) {
|
|
77
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function highlightLine(text, search, isRegex) {
|
|
81
|
+
if (!search || !search.trim()) return text;
|
|
82
|
+
let regex;
|
|
83
|
+
try {
|
|
84
|
+
regex = isRegex
|
|
85
|
+
? new RegExp(search, "gi")
|
|
86
|
+
: new RegExp(escapeRegex(search), "gi");
|
|
87
|
+
} catch {
|
|
88
|
+
return text;
|
|
89
|
+
}
|
|
90
|
+
const parts = [];
|
|
91
|
+
let lastIndex = 0;
|
|
92
|
+
let match;
|
|
93
|
+
regex.lastIndex = 0;
|
|
94
|
+
while ((match = regex.exec(text)) !== null) {
|
|
95
|
+
if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index));
|
|
96
|
+
parts.push(html`<mark class="log-hl">${match[0]}</mark>`);
|
|
97
|
+
lastIndex = regex.lastIndex;
|
|
98
|
+
if (match[0].length === 0) {
|
|
99
|
+
regex.lastIndex++;
|
|
100
|
+
if (regex.lastIndex > text.length) break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (lastIndex < text.length) parts.push(text.slice(lastIndex));
|
|
104
|
+
return parts.length > 0 ? parts : text;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const LINE_HEIGHT = 20;
|
|
108
|
+
const SCROLL_BUFFER = 20;
|
|
109
|
+
|
|
110
|
+
function isMobileViewport() {
|
|
111
|
+
const tg = getTg?.();
|
|
112
|
+
const platform = String(tg?.platform || "").toLowerCase();
|
|
113
|
+
if (platform === "ios" || platform === "android" || platform === "android_x") {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (typeof globalThis !== "undefined" && globalThis.matchMedia) {
|
|
117
|
+
return globalThis.matchMedia("(max-width: 680px)").matches;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* ─── LogsTab ─── */
|
|
123
|
+
export function LogsTab() {
|
|
124
|
+
const logRef = useRef(null);
|
|
125
|
+
const tailRef = useRef(null);
|
|
126
|
+
const isAtBottomRef = useRef(true);
|
|
127
|
+
|
|
128
|
+
const isMobile = useMemo(() => isMobileViewport(), []);
|
|
129
|
+
const [localLogLines, setLocalLogLines] = useState(() => {
|
|
130
|
+
const base = logsLines?.value ?? 200;
|
|
131
|
+
return isMobile ? Math.min(base, 20) : base;
|
|
132
|
+
});
|
|
133
|
+
const [localAgentLines, setLocalAgentLines] = useState(
|
|
134
|
+
agentLogLines?.value ?? 200,
|
|
135
|
+
);
|
|
136
|
+
const [contextQuery, setContextQuery] = useState("");
|
|
137
|
+
const [logLevel, setLogLevel] = useState(isMobile ? "error" : "all");
|
|
138
|
+
const [logSearch, setLogSearch] = useState("");
|
|
139
|
+
const [autoScroll, setAutoScroll] = useState(true);
|
|
140
|
+
const [regexMode, setRegexMode] = useState(false);
|
|
141
|
+
const [logScrollTop, setLogScrollTop] = useState(0);
|
|
142
|
+
const [containerHeight, setContainerHeight] = useState(400);
|
|
143
|
+
const [branchDetail, setBranchDetail] = useState(null);
|
|
144
|
+
const [branchLoading, setBranchLoading] = useState(false);
|
|
145
|
+
const [branchError, setBranchError] = useState(null);
|
|
146
|
+
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (!isMobile) return;
|
|
149
|
+
if (logsLines) logsLines.value = 20;
|
|
150
|
+
setLocalLogLines(20);
|
|
151
|
+
setLogLevel("error");
|
|
152
|
+
loadLogs();
|
|
153
|
+
}, [isMobile]);
|
|
154
|
+
|
|
155
|
+
const branchFileDetails = useMemo(() => {
|
|
156
|
+
if (!branchDetail) return [];
|
|
157
|
+
if (Array.isArray(branchDetail.filesChanged) && branchDetail.filesChanged.length) {
|
|
158
|
+
return branchDetail.filesChanged;
|
|
159
|
+
}
|
|
160
|
+
if (Array.isArray(branchDetail.filesDetailed) && branchDetail.filesDetailed.length) {
|
|
161
|
+
return branchDetail.filesDetailed;
|
|
162
|
+
}
|
|
163
|
+
if (Array.isArray(branchDetail.files) && branchDetail.files.length) {
|
|
164
|
+
return branchDetail.files.map((file) => ({ file }));
|
|
165
|
+
}
|
|
166
|
+
return [];
|
|
167
|
+
}, [branchDetail]);
|
|
168
|
+
|
|
169
|
+
const branchCommits = useMemo(() => {
|
|
170
|
+
if (!branchDetail) return [];
|
|
171
|
+
if (Array.isArray(branchDetail.commitList) && branchDetail.commitList.length) {
|
|
172
|
+
return branchDetail.commitList;
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(branchDetail.commits) && branchDetail.commits.length) {
|
|
175
|
+
return branchDetail.commits;
|
|
176
|
+
}
|
|
177
|
+
return [];
|
|
178
|
+
}, [branchDetail]);
|
|
179
|
+
|
|
180
|
+
const workspaceLink = useMemo(() => {
|
|
181
|
+
if (!branchDetail) return null;
|
|
182
|
+
return branchDetail.workspaceLink || branchDetail.workspaceTarget || null;
|
|
183
|
+
}, [branchDetail]);
|
|
184
|
+
|
|
185
|
+
/* Raw log text */
|
|
186
|
+
const rawLogText = logsData?.value?.lines
|
|
187
|
+
? logsData.value.lines.join("\n")
|
|
188
|
+
: "No logs yet.";
|
|
189
|
+
|
|
190
|
+
const rawTailText = agentLogTail?.value?.lines
|
|
191
|
+
? agentLogTail.value.lines.join("\n")
|
|
192
|
+
: "Select a log file.";
|
|
193
|
+
|
|
194
|
+
/* Filtered log lines (memoized) */
|
|
195
|
+
const { filteredLines, matchCount } = useMemo(() => {
|
|
196
|
+
const leveled = filterByLevel(rawLogText, logLevel);
|
|
197
|
+
const allLines = leveled.split("\n");
|
|
198
|
+
if (!logSearch.trim()) {
|
|
199
|
+
return { filteredLines: allLines, matchCount: 0 };
|
|
200
|
+
}
|
|
201
|
+
let testFn;
|
|
202
|
+
if (regexMode) {
|
|
203
|
+
try {
|
|
204
|
+
const re = new RegExp(logSearch, "i");
|
|
205
|
+
testFn = (line) => re.test(line);
|
|
206
|
+
} catch {
|
|
207
|
+
testFn = (line) =>
|
|
208
|
+
line.toLowerCase().includes(logSearch.toLowerCase());
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
const q = logSearch.toLowerCase();
|
|
212
|
+
testFn = (line) => line.toLowerCase().includes(q);
|
|
213
|
+
}
|
|
214
|
+
const matched = allLines.filter(testFn);
|
|
215
|
+
if (matched.length === 0) {
|
|
216
|
+
return { filteredLines: ["No matching lines."], matchCount: 0 };
|
|
217
|
+
}
|
|
218
|
+
return { filteredLines: matched, matchCount: matched.length };
|
|
219
|
+
}, [rawLogText, logLevel, logSearch, regexMode]);
|
|
220
|
+
|
|
221
|
+
const filteredLogText = filteredLines.join("\n");
|
|
222
|
+
|
|
223
|
+
/* Virtual scroll calculations */
|
|
224
|
+
const totalLines = filteredLines.length;
|
|
225
|
+
const firstVisible = Math.floor(logScrollTop / LINE_HEIGHT);
|
|
226
|
+
const startIdx = Math.max(0, firstVisible - SCROLL_BUFFER);
|
|
227
|
+
const visibleCount = Math.ceil(containerHeight / LINE_HEIGHT);
|
|
228
|
+
const endIdx = Math.min(totalLines, firstVisible + visibleCount + SCROLL_BUFFER);
|
|
229
|
+
const topSpacer = startIdx * LINE_HEIGHT;
|
|
230
|
+
const bottomSpacer = Math.max(0, (totalLines - endIdx) * LINE_HEIGHT);
|
|
231
|
+
const visibleLines = filteredLines.slice(startIdx, endIdx);
|
|
232
|
+
|
|
233
|
+
/* Scroll handler */
|
|
234
|
+
const handleLogScroll = useCallback((e) => {
|
|
235
|
+
const el = e.target;
|
|
236
|
+
setLogScrollTop(el.scrollTop);
|
|
237
|
+
isAtBottomRef.current =
|
|
238
|
+
el.scrollTop + el.clientHeight >= el.scrollHeight - 30;
|
|
239
|
+
}, []);
|
|
240
|
+
|
|
241
|
+
/* Container height measurement */
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
const el = logRef.current;
|
|
244
|
+
if (!el) return;
|
|
245
|
+
setContainerHeight(el.clientHeight);
|
|
246
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
247
|
+
const ro = new ResizeObserver((entries) => {
|
|
248
|
+
for (const entry of entries)
|
|
249
|
+
setContainerHeight(entry.contentRect.height);
|
|
250
|
+
});
|
|
251
|
+
ro.observe(el);
|
|
252
|
+
return () => ro.disconnect();
|
|
253
|
+
}
|
|
254
|
+
}, []);
|
|
255
|
+
|
|
256
|
+
/* Auto-scroll */
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
if (autoScroll && logRef.current) {
|
|
259
|
+
logRef.current.scrollTop = logRef.current.scrollHeight;
|
|
260
|
+
}
|
|
261
|
+
}, [filteredLines, autoScroll]);
|
|
262
|
+
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
if (autoScroll && tailRef.current) {
|
|
265
|
+
tailRef.current.scrollTop = tailRef.current.scrollHeight;
|
|
266
|
+
}
|
|
267
|
+
}, [rawTailText, autoScroll]);
|
|
268
|
+
|
|
269
|
+
/* ── System log handlers ── */
|
|
270
|
+
const handleLogLinesChange = async (value) => {
|
|
271
|
+
setLocalLogLines(value);
|
|
272
|
+
if (logsLines) logsLines.value = value;
|
|
273
|
+
await loadLogs();
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/* ── Agent log handlers ── */
|
|
277
|
+
const handleAgentSearch = async () => {
|
|
278
|
+
if (agentLogFile) agentLogFile.value = "";
|
|
279
|
+
await loadAgentLogFileList();
|
|
280
|
+
await loadAgentLogTailData();
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const normalizeBranchLine = (line) => {
|
|
284
|
+
if (!line) return null;
|
|
285
|
+
const cleaned = line.replace(/^\*\s*/, "").trim();
|
|
286
|
+
const noRemote = cleaned.replace(/^remotes\//, "");
|
|
287
|
+
const short = noRemote.replace(/^origin\//, "");
|
|
288
|
+
return { raw: line, name: noRemote, short };
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const openBranchDetail = async (line) => {
|
|
292
|
+
const parsed = normalizeBranchLine(line);
|
|
293
|
+
if (!parsed?.name) return;
|
|
294
|
+
setBranchError(null);
|
|
295
|
+
setBranchLoading(true);
|
|
296
|
+
setBranchDetail({ branch: parsed.name });
|
|
297
|
+
try {
|
|
298
|
+
const res = await apiFetch(
|
|
299
|
+
`/api/git/branch-detail?branch=${encodeURIComponent(parsed.name)}`,
|
|
300
|
+
);
|
|
301
|
+
setBranchDetail(res.data || null);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
setBranchError(err.message || "Failed to load branch detail");
|
|
304
|
+
} finally {
|
|
305
|
+
setBranchLoading(false);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const openWorkspace = (detail) => {
|
|
310
|
+
if (!detail) return;
|
|
311
|
+
const target =
|
|
312
|
+
detail?.workspaceTarget ||
|
|
313
|
+
{
|
|
314
|
+
taskId: detail?.activeSlot?.taskId || detail?.worktree?.taskKey || null,
|
|
315
|
+
taskTitle: detail?.activeSlot?.taskTitle || detail?.branch || "Workspace",
|
|
316
|
+
branch: detail?.branch || null,
|
|
317
|
+
};
|
|
318
|
+
agentWorkspaceTarget.value = {
|
|
319
|
+
taskId: target.taskId || null,
|
|
320
|
+
taskTitle: target.taskTitle || detail?.branch || "Workspace",
|
|
321
|
+
branch: target.branch || detail?.branch || null,
|
|
322
|
+
};
|
|
323
|
+
navigateTo("agents");
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const handleAgentOpen = async (name) => {
|
|
327
|
+
haptic();
|
|
328
|
+
if (agentLogFile) agentLogFile.value = name;
|
|
329
|
+
await loadAgentLogTailData();
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const handleAgentLinesChange = async (value) => {
|
|
333
|
+
setLocalAgentLines(value);
|
|
334
|
+
if (agentLogLines) agentLogLines.value = value;
|
|
335
|
+
await loadAgentLogTailData();
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/* ── Context handler ── */
|
|
339
|
+
const handleContextLoad = async () => {
|
|
340
|
+
haptic();
|
|
341
|
+
await loadAgentContextData(contextQuery.trim());
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/* ── Git handler ── */
|
|
345
|
+
const handleGitRefresh = async () => {
|
|
346
|
+
haptic();
|
|
347
|
+
const [branches, diff] = await Promise.all([
|
|
348
|
+
apiFetch("/api/git/branches", { _silent: true }).catch(() => ({
|
|
349
|
+
data: [],
|
|
350
|
+
})),
|
|
351
|
+
apiFetch("/api/git/diff", { _silent: true }).catch(() => ({ data: "" })),
|
|
352
|
+
]);
|
|
353
|
+
if (gitBranches) gitBranches.value = branches.data || [];
|
|
354
|
+
if (gitDiff) gitDiff.value = diff.data || "";
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
/* ── Copy to clipboard ── */
|
|
358
|
+
const copyToClipboard = async (text, label) => {
|
|
359
|
+
haptic();
|
|
360
|
+
try {
|
|
361
|
+
if (navigator.clipboard) {
|
|
362
|
+
await navigator.clipboard.writeText(text);
|
|
363
|
+
} else {
|
|
364
|
+
const ta = document.createElement("textarea");
|
|
365
|
+
ta.value = text;
|
|
366
|
+
ta.style.position = "fixed";
|
|
367
|
+
ta.style.left = "-9999px";
|
|
368
|
+
document.body.appendChild(ta);
|
|
369
|
+
ta.select();
|
|
370
|
+
document.execCommand("copy");
|
|
371
|
+
document.body.removeChild(ta);
|
|
372
|
+
}
|
|
373
|
+
showToast(`${label} copied`, "success");
|
|
374
|
+
} catch {
|
|
375
|
+
showToast("Copy failed", "error");
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/* ── Download logs ── */
|
|
380
|
+
const downloadLogs = useCallback(() => {
|
|
381
|
+
haptic();
|
|
382
|
+
const blob = new Blob([filteredLogText], { type: "text/plain" });
|
|
383
|
+
const url = URL.createObjectURL(blob);
|
|
384
|
+
const a = document.createElement("a");
|
|
385
|
+
const d = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
386
|
+
a.href = url;
|
|
387
|
+
a.download = `openfleet-logs-${d}.log`;
|
|
388
|
+
document.body.appendChild(a);
|
|
389
|
+
a.click();
|
|
390
|
+
document.body.removeChild(a);
|
|
391
|
+
URL.revokeObjectURL(url);
|
|
392
|
+
showToast("Log file downloaded", "success");
|
|
393
|
+
}, [filteredLogText]);
|
|
394
|
+
|
|
395
|
+
return html`
|
|
396
|
+
<style>
|
|
397
|
+
.log-line { display: flex; }
|
|
398
|
+
.log-ln { min-width: 3.5em; text-align: right; padding-right: 8px; opacity: 0.35; user-select: none; font-size: 0.85em; }
|
|
399
|
+
.log-lt { flex: 1; white-space: pre-wrap; word-break: break-all; }
|
|
400
|
+
.log-hl { background: rgba(250,204,21,0.3); border-radius: 2px; padding: 0 1px; }
|
|
401
|
+
</style>
|
|
402
|
+
<!-- Loading skeleton -->
|
|
403
|
+
${!logsData?.value && !agentLogFiles?.value && html`<${Card} title="Loading Logs…"><${SkeletonCard} /><//>`}
|
|
404
|
+
|
|
405
|
+
<!-- ── System Logs ── -->
|
|
406
|
+
<${Card} title="System Logs">
|
|
407
|
+
<div class="range-row mb-sm">
|
|
408
|
+
<input
|
|
409
|
+
type="range"
|
|
410
|
+
min="20"
|
|
411
|
+
max="800"
|
|
412
|
+
step="20"
|
|
413
|
+
value=${localLogLines}
|
|
414
|
+
onInput=${(e) => setLocalLogLines(Number(e.target.value))}
|
|
415
|
+
onChange=${(e) => handleLogLinesChange(Number(e.target.value))}
|
|
416
|
+
/>
|
|
417
|
+
<span class="pill">${localLogLines} lines</span>
|
|
418
|
+
</div>
|
|
419
|
+
<div class="chip-group mb-sm">
|
|
420
|
+
${[50, 200, 500].map(
|
|
421
|
+
(n) => html`
|
|
422
|
+
<button
|
|
423
|
+
key=${n}
|
|
424
|
+
class="chip ${(logsLines?.value ?? localLogLines) === n
|
|
425
|
+
? "active"
|
|
426
|
+
: ""}"
|
|
427
|
+
onClick=${() => handleLogLinesChange(n)}
|
|
428
|
+
>
|
|
429
|
+
${n}
|
|
430
|
+
</button>
|
|
431
|
+
`,
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
434
|
+
<div class="chip-group mb-sm">
|
|
435
|
+
${LOG_LEVELS.map(
|
|
436
|
+
(l) => html`
|
|
437
|
+
<button
|
|
438
|
+
key=${l.value}
|
|
439
|
+
class="chip chip-outline ${logLevel === l.value ? "active" : ""}"
|
|
440
|
+
onClick=${() => {
|
|
441
|
+
haptic();
|
|
442
|
+
setLogLevel(l.value);
|
|
443
|
+
}}
|
|
444
|
+
>
|
|
445
|
+
${l.label}
|
|
446
|
+
</button>
|
|
447
|
+
`,
|
|
448
|
+
)}
|
|
449
|
+
</div>
|
|
450
|
+
<div class="input-row mb-sm">
|
|
451
|
+
<input
|
|
452
|
+
class="input"
|
|
453
|
+
placeholder=${regexMode ? "Regex pattern…" : "Search/grep logs…"}
|
|
454
|
+
value=${logSearch}
|
|
455
|
+
onInput=${(e) => setLogSearch(e.target.value)}
|
|
456
|
+
/>
|
|
457
|
+
<button
|
|
458
|
+
class="btn btn-ghost btn-sm"
|
|
459
|
+
style="font-family:monospace;min-width:2.2em;padding:2px 6px;${regexMode ? "background:var(--accent);color:#fff;" : ""}"
|
|
460
|
+
onClick=${() => { setRegexMode(!regexMode); haptic(); }}
|
|
461
|
+
title="Toggle regex mode"
|
|
462
|
+
>.*</button>
|
|
463
|
+
${logSearch.trim() && matchCount > 0 && html`<span class="pill">${matchCount} matches</span>`}
|
|
464
|
+
<label
|
|
465
|
+
class="meta-text toggle-label"
|
|
466
|
+
style="white-space:nowrap"
|
|
467
|
+
onClick=${() => {
|
|
468
|
+
setAutoScroll(!autoScroll);
|
|
469
|
+
haptic();
|
|
470
|
+
}}
|
|
471
|
+
>
|
|
472
|
+
<input
|
|
473
|
+
type="checkbox"
|
|
474
|
+
checked=${autoScroll}
|
|
475
|
+
style="accent-color:var(--accent)"
|
|
476
|
+
/>
|
|
477
|
+
Auto-scroll
|
|
478
|
+
</label>
|
|
479
|
+
</div>
|
|
480
|
+
<div ref=${logRef} class="log-box" onScroll=${handleLogScroll} style="overflow-y:auto">
|
|
481
|
+
<div style="height:${topSpacer}px"></div>
|
|
482
|
+
${visibleLines.map((line, i) => {
|
|
483
|
+
const lineNum = startIdx + i + 1;
|
|
484
|
+
return html`<div class="log-line" key=${lineNum} style="height:${LINE_HEIGHT}px">
|
|
485
|
+
<span class="log-ln">${lineNum}</span>
|
|
486
|
+
<span class="log-lt">${logSearch.trim() ? highlightLine(line, logSearch, regexMode) : line}</span>
|
|
487
|
+
</div>`;
|
|
488
|
+
})}
|
|
489
|
+
<div style="height:${bottomSpacer}px"></div>
|
|
490
|
+
</div>
|
|
491
|
+
<div class="btn-row mt-sm">
|
|
492
|
+
<button
|
|
493
|
+
class="btn btn-ghost btn-sm"
|
|
494
|
+
onClick=${() =>
|
|
495
|
+
sendCommandToChat(`/logs ${logsLines?.value ?? localLogLines}`)}
|
|
496
|
+
>
|
|
497
|
+
/logs to chat
|
|
498
|
+
</button>
|
|
499
|
+
<button
|
|
500
|
+
class="btn btn-ghost btn-sm"
|
|
501
|
+
onClick=${() => copyToClipboard(filteredLogText, "Logs")}
|
|
502
|
+
>
|
|
503
|
+
📋 Copy
|
|
504
|
+
</button>
|
|
505
|
+
<button
|
|
506
|
+
class="btn btn-ghost btn-sm"
|
|
507
|
+
onClick=${downloadLogs}
|
|
508
|
+
>
|
|
509
|
+
💾 Download
|
|
510
|
+
</button>
|
|
511
|
+
</div>
|
|
512
|
+
<//>
|
|
513
|
+
|
|
514
|
+
<!-- ── Agent Log Library ── -->
|
|
515
|
+
<${Card} title="Agent Log Library">
|
|
516
|
+
<div class="input-row mb-sm">
|
|
517
|
+
<input
|
|
518
|
+
class="input"
|
|
519
|
+
placeholder="Search log files"
|
|
520
|
+
value=${agentLogQuery?.value ?? ""}
|
|
521
|
+
onInput=${(e) => {
|
|
522
|
+
if (agentLogQuery) agentLogQuery.value = e.target.value;
|
|
523
|
+
}}
|
|
524
|
+
/>
|
|
525
|
+
<button class="btn btn-secondary btn-sm" onClick=${handleAgentSearch}>
|
|
526
|
+
🔍 Search
|
|
527
|
+
</button>
|
|
528
|
+
</div>
|
|
529
|
+
<div class="range-row mb-md">
|
|
530
|
+
<input
|
|
531
|
+
type="range"
|
|
532
|
+
min="50"
|
|
533
|
+
max="800"
|
|
534
|
+
step="50"
|
|
535
|
+
value=${localAgentLines}
|
|
536
|
+
onInput=${(e) => setLocalAgentLines(Number(e.target.value))}
|
|
537
|
+
onChange=${(e) => handleAgentLinesChange(Number(e.target.value))}
|
|
538
|
+
/>
|
|
539
|
+
<span class="pill">${localAgentLines} lines</span>
|
|
540
|
+
</div>
|
|
541
|
+
<//>
|
|
542
|
+
|
|
543
|
+
<!-- ── Log Files list ── -->
|
|
544
|
+
<${Card} title="Log Files">
|
|
545
|
+
${(agentLogFiles?.value || []).length
|
|
546
|
+
? (agentLogFiles.value || []).map(
|
|
547
|
+
(file) => html`
|
|
548
|
+
<div
|
|
549
|
+
key=${file.name}
|
|
550
|
+
class="task-card"
|
|
551
|
+
style="cursor:pointer"
|
|
552
|
+
onClick=${() => handleAgentOpen(file.name)}
|
|
553
|
+
>
|
|
554
|
+
<div class="task-card-header">
|
|
555
|
+
<div>
|
|
556
|
+
<div class="task-card-title">${file.name}</div>
|
|
557
|
+
<div class="task-card-meta">
|
|
558
|
+
${formatBytes
|
|
559
|
+
? formatBytes(file.size)
|
|
560
|
+
: Math.round(file.size / 1024) + "kb"}
|
|
561
|
+
· ${new Date(file.mtime).toLocaleString()}
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
<${Badge} status="log" text="log" />
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
`,
|
|
568
|
+
)
|
|
569
|
+
: html`<${EmptyState} message="No log files found." />`}
|
|
570
|
+
<//>
|
|
571
|
+
|
|
572
|
+
<!-- ── Log Tail viewer ── -->
|
|
573
|
+
<${Card} title=${agentLogFile?.value || "Log Tail"}>
|
|
574
|
+
${agentLogTail?.value?.truncated &&
|
|
575
|
+
html`<span class="pill mb-sm">Tail clipped</span>`}
|
|
576
|
+
<div ref=${tailRef} class="log-box">${rawTailText}</div>
|
|
577
|
+
<div class="btn-row mt-sm">
|
|
578
|
+
<button
|
|
579
|
+
class="btn btn-ghost btn-sm"
|
|
580
|
+
onClick=${() => copyToClipboard(rawTailText, "Log tail")}
|
|
581
|
+
>
|
|
582
|
+
📋 Copy
|
|
583
|
+
</button>
|
|
584
|
+
</div>
|
|
585
|
+
<//>
|
|
586
|
+
|
|
587
|
+
<!-- ── Worktree Context ── -->
|
|
588
|
+
<${Card} title="Worktree Context">
|
|
589
|
+
<div class="input-row mb-sm">
|
|
590
|
+
<input
|
|
591
|
+
class="input"
|
|
592
|
+
placeholder="Branch fragment"
|
|
593
|
+
value=${contextQuery}
|
|
594
|
+
onInput=${(e) => setContextQuery(e.target.value)}
|
|
595
|
+
onKeyDown=${(e) => {
|
|
596
|
+
if (e.key === "Enter") handleContextLoad();
|
|
597
|
+
}}
|
|
598
|
+
/>
|
|
599
|
+
<button class="btn btn-secondary btn-sm" onClick=${handleContextLoad}>
|
|
600
|
+
📂 Load
|
|
601
|
+
</button>
|
|
602
|
+
</div>
|
|
603
|
+
<div class="log-box">
|
|
604
|
+
${agentContext?.value
|
|
605
|
+
? [
|
|
606
|
+
"Worktree: " + (agentContext.value.name || "?"),
|
|
607
|
+
"",
|
|
608
|
+
agentContext.value.gitLog || "No git log.",
|
|
609
|
+
"",
|
|
610
|
+
agentContext.value.gitStatus || "Clean worktree.",
|
|
611
|
+
"",
|
|
612
|
+
agentContext.value.diffStat || "No diff stat.",
|
|
613
|
+
].join("\n")
|
|
614
|
+
: "Load a worktree context to view git log/status."}
|
|
615
|
+
</div>
|
|
616
|
+
${agentContext?.value &&
|
|
617
|
+
html`
|
|
618
|
+
<div class="btn-row mt-sm">
|
|
619
|
+
<button
|
|
620
|
+
class="btn btn-ghost btn-sm"
|
|
621
|
+
onClick=${() =>
|
|
622
|
+
copyToClipboard(
|
|
623
|
+
[
|
|
624
|
+
agentContext.value.gitLog,
|
|
625
|
+
agentContext.value.gitStatus,
|
|
626
|
+
agentContext.value.diffStat,
|
|
627
|
+
]
|
|
628
|
+
.filter(Boolean)
|
|
629
|
+
.join("\n\n"),
|
|
630
|
+
"Context",
|
|
631
|
+
)}
|
|
632
|
+
>
|
|
633
|
+
📋 Copy
|
|
634
|
+
</button>
|
|
635
|
+
</div>
|
|
636
|
+
`}
|
|
637
|
+
<//>
|
|
638
|
+
|
|
639
|
+
<!-- ── Git Snapshot ── -->
|
|
640
|
+
<${Card} title="Git Snapshot">
|
|
641
|
+
<div class="btn-row mb-sm">
|
|
642
|
+
<button class="btn btn-secondary btn-sm" onClick=${handleGitRefresh}>
|
|
643
|
+
${ICONS.refresh} Refresh
|
|
644
|
+
</button>
|
|
645
|
+
<button
|
|
646
|
+
class="btn btn-ghost btn-sm"
|
|
647
|
+
onClick=${() => sendCommandToChat("/diff")}
|
|
648
|
+
>
|
|
649
|
+
/diff
|
|
650
|
+
</button>
|
|
651
|
+
<button
|
|
652
|
+
class="btn btn-ghost btn-sm"
|
|
653
|
+
onClick=${() => copyToClipboard(gitDiff?.value || "", "Diff")}
|
|
654
|
+
>
|
|
655
|
+
📋 Copy
|
|
656
|
+
</button>
|
|
657
|
+
</div>
|
|
658
|
+
<div class="log-box mb-md">
|
|
659
|
+
${gitDiff?.value || "Clean working tree."}
|
|
660
|
+
</div>
|
|
661
|
+
<div class="card-subtitle">Recent Branches</div>
|
|
662
|
+
${(gitBranches?.value || []).length
|
|
663
|
+
? (gitBranches.value || []).map(
|
|
664
|
+
(line, i) => {
|
|
665
|
+
const parsed = normalizeBranchLine(line);
|
|
666
|
+
return html`
|
|
667
|
+
<button
|
|
668
|
+
key=${i}
|
|
669
|
+
class="branch-row"
|
|
670
|
+
onClick=${() => openBranchDetail(line)}
|
|
671
|
+
>
|
|
672
|
+
<span class="branch-name">${parsed?.short || line}</span>
|
|
673
|
+
<span class="branch-raw">${line}</span>
|
|
674
|
+
</button>
|
|
675
|
+
`;
|
|
676
|
+
},
|
|
677
|
+
)
|
|
678
|
+
: html`<div class="meta-text">No branches found.</div>`}
|
|
679
|
+
<//>
|
|
680
|
+
|
|
681
|
+
${branchDetail &&
|
|
682
|
+
html`
|
|
683
|
+
<${Modal} title="Branch Detail" onClose=${() => setBranchDetail(null)}>
|
|
684
|
+
${branchLoading && html`<${SkeletonCard} height="80px" />`}
|
|
685
|
+
${branchError && html`<div class="meta-text" style="color:var(--color-error)">${branchError}</div>`}
|
|
686
|
+
${!branchLoading &&
|
|
687
|
+
!branchError &&
|
|
688
|
+
html`
|
|
689
|
+
<div class="meta-text mb-sm">
|
|
690
|
+
Branch: <span class="mono">${branchDetail.branch}</span>
|
|
691
|
+
</div>
|
|
692
|
+
${branchDetail.base &&
|
|
693
|
+
html`<div class="meta-text mb-sm">Base: ${branchDetail.base}</div>`}
|
|
694
|
+
${branchDetail.activeSlot &&
|
|
695
|
+
html`<div class="meta-text mb-sm">Active Agent: ${branchDetail.activeSlot.taskTitle || branchDetail.activeSlot.taskId}</div>`}
|
|
696
|
+
${branchDetail.worktree?.path &&
|
|
697
|
+
html`<div class="meta-text mb-sm">Worktree: <span class="mono">${branchDetail.worktree.path}</span></div>`}
|
|
698
|
+
<div class="btn-row mb-sm">
|
|
699
|
+
${(branchDetail.workspaceTarget || branchDetail.activeSlot || branchDetail.worktree) &&
|
|
700
|
+
html`<button class="btn btn-primary btn-sm" onClick=${() => openWorkspace(branchDetail)}>
|
|
701
|
+
🔍 Open Workspace Viewer
|
|
702
|
+
</button>`}
|
|
703
|
+
${branchDetail.workspaceLink?.url &&
|
|
704
|
+
html`<button
|
|
705
|
+
class="btn btn-secondary btn-sm"
|
|
706
|
+
onClick=${() => openLink(branchDetail.workspaceLink.url)}
|
|
707
|
+
>
|
|
708
|
+
🔗 Open Workspace Link
|
|
709
|
+
</button>`}
|
|
710
|
+
<button
|
|
711
|
+
class="btn btn-ghost btn-sm"
|
|
712
|
+
onClick=${() => copyToClipboard(branchDetail.diffStat || "", "Diff")}
|
|
713
|
+
>📋 Copy Diff</button>
|
|
714
|
+
</div>
|
|
715
|
+
${workspaceLink &&
|
|
716
|
+
html`
|
|
717
|
+
<div class="meta-text mb-sm">
|
|
718
|
+
Workspace: ${workspaceLink.label || workspaceLink.taskTitle || workspaceLink.branch || "Active"}
|
|
719
|
+
${(workspaceLink.target?.workspacePath || workspaceLink.workspacePath)
|
|
720
|
+
? html`<span class="mono"> · ${workspaceLink.target?.workspacePath || workspaceLink.workspacePath}</span>`
|
|
721
|
+
: ""}
|
|
722
|
+
</div>
|
|
723
|
+
`}
|
|
724
|
+
${branchDetail.diffSummary &&
|
|
725
|
+
html`
|
|
726
|
+
<div class="meta-text mb-sm">
|
|
727
|
+
Diff: ${branchDetail.diffSummary.totalFiles || 0} files ·
|
|
728
|
+
+${branchDetail.diffSummary.totalAdditions || 0} ·
|
|
729
|
+
-${branchDetail.diffSummary.totalDeletions || 0}
|
|
730
|
+
${branchDetail.diffSummary.binaryFiles ? `· ${branchDetail.diffSummary.binaryFiles} binary` : ""}
|
|
731
|
+
</div>
|
|
732
|
+
`}
|
|
733
|
+
${branchCommits.length > 0 &&
|
|
734
|
+
html`
|
|
735
|
+
<div class="card mb-sm">
|
|
736
|
+
<div class="card-title">Commits</div>
|
|
737
|
+
${branchCommits.map((cm) => {
|
|
738
|
+
const subject = cm.subject || cm.message || "";
|
|
739
|
+
const author =
|
|
740
|
+
cm.author ||
|
|
741
|
+
(cm.authorName && cm.authorEmail
|
|
742
|
+
? `${cm.authorName} <${cm.authorEmail}>`
|
|
743
|
+
: cm.authorName || cm.authorEmail || "");
|
|
744
|
+
const dateVal = cm.authorDate || cm.date || cm.time;
|
|
745
|
+
return html`
|
|
746
|
+
<div class="meta-text" key=${cm.hash}>
|
|
747
|
+
<span class="mono">${cm.hash}</span> ${subject}
|
|
748
|
+
${author ? `· ${author}` : ""}
|
|
749
|
+
${dateVal ? `· ${new Date(dateVal).toLocaleString()}` : ""}
|
|
750
|
+
</div>
|
|
751
|
+
`;
|
|
752
|
+
})}
|
|
753
|
+
</div>
|
|
754
|
+
`}
|
|
755
|
+
<div class="card mb-sm">
|
|
756
|
+
<div class="card-title">Files Changed</div>
|
|
757
|
+
${branchFileDetails.length
|
|
758
|
+
? branchFileDetails.map(
|
|
759
|
+
(f) => html`
|
|
760
|
+
<div class="meta-text" key=${f.file}>
|
|
761
|
+
<span class="mono">${f.file}</span>
|
|
762
|
+
${typeof f.additions === "number" &&
|
|
763
|
+
html`<span class="pill" style="margin-left:6px">+${f.additions}</span>`}
|
|
764
|
+
${typeof f.deletions === "number" &&
|
|
765
|
+
html`<span class="pill" style="margin-left:6px">-${f.deletions}</span>`}
|
|
766
|
+
${f.binary && html`<span class="pill" style="margin-left:6px">binary</span>`}
|
|
767
|
+
</div>
|
|
768
|
+
`,
|
|
769
|
+
)
|
|
770
|
+
: html`<div class="meta-text">No diff against base.</div>`}
|
|
771
|
+
</div>
|
|
772
|
+
${branchDetail.diffStat &&
|
|
773
|
+
html`
|
|
774
|
+
<div class="card">
|
|
775
|
+
<div class="card-title">Diff Summary</div>
|
|
776
|
+
<pre class="workspace-diff">${branchDetail.diffStat}</pre>
|
|
777
|
+
</div>
|
|
778
|
+
`}
|
|
779
|
+
`}
|
|
780
|
+
<//>
|
|
781
|
+
`}
|
|
782
|
+
`;
|
|
783
|
+
}
|