@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.
Files changed (120) hide show
  1. package/.env.example +914 -0
  2. package/LICENSE +190 -0
  3. package/README.md +500 -0
  4. package/agent-endpoint.mjs +918 -0
  5. package/agent-hook-bridge.mjs +230 -0
  6. package/agent-hooks.mjs +1188 -0
  7. package/agent-pool.mjs +2403 -0
  8. package/agent-prompts.mjs +689 -0
  9. package/agent-sdk.mjs +141 -0
  10. package/anomaly-detector.mjs +1195 -0
  11. package/autofix.mjs +1294 -0
  12. package/claude-shell.mjs +708 -0
  13. package/cli.mjs +906 -0
  14. package/codex-config.mjs +1274 -0
  15. package/codex-model-profiles.mjs +135 -0
  16. package/codex-shell.mjs +762 -0
  17. package/config-doctor.mjs +613 -0
  18. package/config.mjs +1720 -0
  19. package/conflict-resolver.mjs +248 -0
  20. package/container-runner.mjs +450 -0
  21. package/copilot-shell.mjs +827 -0
  22. package/daemon-restart-policy.mjs +56 -0
  23. package/diff-stats.mjs +282 -0
  24. package/error-detector.mjs +829 -0
  25. package/fetch-runtime.mjs +34 -0
  26. package/fleet-coordinator.mjs +838 -0
  27. package/get-telegram-chat-id.mjs +71 -0
  28. package/git-safety.mjs +170 -0
  29. package/github-reconciler.mjs +403 -0
  30. package/hook-profiles.mjs +651 -0
  31. package/kanban-adapter.mjs +4491 -0
  32. package/lib/logger.mjs +645 -0
  33. package/maintenance.mjs +828 -0
  34. package/merge-strategy.mjs +1171 -0
  35. package/monitor.mjs +12207 -0
  36. package/openfleet.config.example.json +115 -0
  37. package/openfleet.schema.json +465 -0
  38. package/package.json +203 -0
  39. package/postinstall.mjs +187 -0
  40. package/pr-cleanup-daemon.mjs +978 -0
  41. package/preflight.mjs +408 -0
  42. package/prepublish-check.mjs +90 -0
  43. package/presence.mjs +328 -0
  44. package/primary-agent.mjs +282 -0
  45. package/publish.mjs +151 -0
  46. package/repo-root.mjs +29 -0
  47. package/restart-controller.mjs +100 -0
  48. package/review-agent.mjs +557 -0
  49. package/rotate-agent-logs.sh +133 -0
  50. package/sdk-conflict-resolver.mjs +973 -0
  51. package/session-tracker.mjs +880 -0
  52. package/setup.mjs +3937 -0
  53. package/shared-knowledge.mjs +410 -0
  54. package/shared-state-manager.mjs +841 -0
  55. package/shared-workspace-cli.mjs +199 -0
  56. package/shared-workspace-registry.mjs +537 -0
  57. package/shared-workspaces.json +18 -0
  58. package/startup-service.mjs +1070 -0
  59. package/sync-engine.mjs +1063 -0
  60. package/task-archiver.mjs +801 -0
  61. package/task-assessment.mjs +550 -0
  62. package/task-claims.mjs +924 -0
  63. package/task-complexity.mjs +581 -0
  64. package/task-executor.mjs +5111 -0
  65. package/task-store.mjs +753 -0
  66. package/telegram-bot.mjs +9281 -0
  67. package/telegram-sentinel.mjs +2010 -0
  68. package/ui/app.js +867 -0
  69. package/ui/app.legacy.js +1464 -0
  70. package/ui/app.monolith.js +2488 -0
  71. package/ui/components/charts.js +226 -0
  72. package/ui/components/chat-view.js +567 -0
  73. package/ui/components/command-palette.js +587 -0
  74. package/ui/components/diff-viewer.js +190 -0
  75. package/ui/components/forms.js +327 -0
  76. package/ui/components/kanban-board.js +451 -0
  77. package/ui/components/session-list.js +305 -0
  78. package/ui/components/shared.js +473 -0
  79. package/ui/index.html +70 -0
  80. package/ui/modules/api.js +297 -0
  81. package/ui/modules/icons.js +461 -0
  82. package/ui/modules/router.js +81 -0
  83. package/ui/modules/settings-schema.js +261 -0
  84. package/ui/modules/state.js +679 -0
  85. package/ui/modules/telegram.js +331 -0
  86. package/ui/modules/utils.js +270 -0
  87. package/ui/styles/animations.css +140 -0
  88. package/ui/styles/base.css +98 -0
  89. package/ui/styles/components.css +1915 -0
  90. package/ui/styles/kanban.css +286 -0
  91. package/ui/styles/layout.css +809 -0
  92. package/ui/styles/sessions.css +827 -0
  93. package/ui/styles/variables.css +188 -0
  94. package/ui/styles.css +141 -0
  95. package/ui/styles.monolith.css +1046 -0
  96. package/ui/tabs/agents.js +1417 -0
  97. package/ui/tabs/chat.js +74 -0
  98. package/ui/tabs/control.js +887 -0
  99. package/ui/tabs/dashboard.js +515 -0
  100. package/ui/tabs/infra.js +537 -0
  101. package/ui/tabs/logs.js +783 -0
  102. package/ui/tabs/settings.js +1487 -0
  103. package/ui/tabs/tasks.js +1385 -0
  104. package/ui-server.mjs +4073 -0
  105. package/update-check.mjs +465 -0
  106. package/utils.mjs +172 -0
  107. package/ve-kanban.mjs +654 -0
  108. package/ve-kanban.ps1 +1365 -0
  109. package/ve-kanban.sh +18 -0
  110. package/ve-orchestrator.mjs +340 -0
  111. package/ve-orchestrator.ps1 +6546 -0
  112. package/ve-orchestrator.sh +18 -0
  113. package/vibe-kanban-wrapper.mjs +41 -0
  114. package/vk-error-resolver.mjs +470 -0
  115. package/vk-log-stream.mjs +914 -0
  116. package/whatsapp-channel.mjs +520 -0
  117. package/workspace-monitor.mjs +581 -0
  118. package/workspace-reaper.mjs +405 -0
  119. package/workspace-registry.mjs +238 -0
  120. package/worktree-manager.mjs +1266 -0
@@ -0,0 +1,190 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ * Component: Diff Viewer — VS Code-style diff display
3
+ * ────────────────────────────────────────────────────────────── */
4
+ import { h } from "preact";
5
+ import { useState, useEffect, useCallback } from "preact/hooks";
6
+ import htm from "htm";
7
+ import { apiFetch } from "../modules/api.js";
8
+
9
+ const html = htm.bind(h);
10
+
11
+ /* ─── File type icons ─── */
12
+ const EXT_ICONS = {
13
+ js: "📜", mjs: "📜", cjs: "📜",
14
+ ts: "🔷", tsx: "🔷",
15
+ json: "📋", yaml: "📋", yml: "📋", toml: "📋",
16
+ css: "🎨", scss: "🎨", less: "🎨",
17
+ html: "🌐", htm: "🌐",
18
+ md: "📝", txt: "📄",
19
+ py: "🐍", rb: "💎", go: "🔵", rs: "🦀",
20
+ sh: "🐚", bash: "🐚", ps1: "🐚",
21
+ sql: "🗃️", graphql: "🗃️",
22
+ };
23
+
24
+ function fileIcon(filename) {
25
+ const ext = (filename || "").split(".").pop().toLowerCase();
26
+ return EXT_ICONS[ext] || "📄";
27
+ }
28
+
29
+ /* ─── Parse unified diff into lines ─── */
30
+ function parseDiffLines(rawDiff) {
31
+ if (!rawDiff) return [];
32
+ return rawDiff.split("\n").map((line, i) => {
33
+ let type = "context";
34
+ if (line.startsWith("+") && !line.startsWith("+++")) type = "addition";
35
+ else if (line.startsWith("-") && !line.startsWith("---")) type = "deletion";
36
+ else if (line.startsWith("@@")) type = "hunk-header";
37
+ return { text: line, type, index: i };
38
+ });
39
+ }
40
+
41
+ /* ─── DiffFile component ─── */
42
+ function DiffFile({ file }) {
43
+ const [expanded, setExpanded] = useState(false);
44
+ const lines = parseDiffLines(file.patch || file.diff);
45
+ const additions = file.additions ?? lines.filter((l) => l.type === "addition").length;
46
+ const deletions = file.deletions ?? lines.filter((l) => l.type === "deletion").length;
47
+
48
+ const statusClass = file.status === "added"
49
+ ? "added"
50
+ : file.status === "removed" || file.status === "deleted"
51
+ ? "deleted"
52
+ : "modified";
53
+
54
+ return html`
55
+ <div class="diff-file-item">
56
+ <div
57
+ class="diff-file-header ${statusClass}"
58
+ onClick=${() => setExpanded(!expanded)}
59
+ >
60
+ <span class="diff-file-icon">${fileIcon(file.filename)}</span>
61
+ <span class="diff-file-name">${file.filename}</span>
62
+ <span class="diff-file-stats">
63
+ ${additions > 0 && html`<span class="diff-stat-add">+${additions}</span>`}
64
+ ${deletions > 0 && html`<span class="diff-stat-del">-${deletions}</span>`}
65
+ </span>
66
+ <span class="diff-file-toggle">${expanded ? "▾" : "▸"}</span>
67
+ </div>
68
+ ${expanded && lines.length > 0 && html`
69
+ <div class="diff-hunk">
70
+ ${lines.map(
71
+ (line) => html`
72
+ <div key=${line.index} class="diff-line ${line.type}">
73
+ <span class="diff-line-text">${line.text}</span>
74
+ </div>
75
+ `,
76
+ )}
77
+ </div>
78
+ `}
79
+ ${expanded && lines.length === 0 && html`
80
+ <div class="diff-hunk">
81
+ <div class="diff-line context">
82
+ <span class="diff-line-text">(no diff available)</span>
83
+ </div>
84
+ </div>
85
+ `}
86
+ </div>
87
+ `;
88
+ }
89
+
90
+ /* ─── DiffViewer component ─── */
91
+ export function DiffViewer({ sessionId }) {
92
+ const [diffData, setDiffData] = useState(null);
93
+ const [loading, setLoading] = useState(false);
94
+ const [error, setError] = useState(null);
95
+
96
+ useEffect(() => {
97
+ if (!sessionId) return;
98
+ let active = true;
99
+ setLoading(true);
100
+ setError(null);
101
+
102
+ apiFetch(`/api/sessions/${sessionId}/diff`, { _silent: true })
103
+ .then((res) => {
104
+ if (!active) return;
105
+ setDiffData(res?.diff || null);
106
+ })
107
+ .catch(() => {
108
+ if (active) setError("unavailable");
109
+ })
110
+ .finally(() => {
111
+ if (active) setLoading(false);
112
+ });
113
+
114
+ return () => { active = false; };
115
+ }, [sessionId]);
116
+
117
+ const handleRetry = useCallback(() => {
118
+ setError(null);
119
+ setLoading(true);
120
+ apiFetch(`/api/sessions/${sessionId}/diff`, { _silent: true })
121
+ .then((res) => setDiffData(res?.diff || null))
122
+ .catch(() => setError("unavailable"))
123
+ .finally(() => setLoading(false));
124
+ }, [sessionId]);
125
+
126
+ if (!sessionId) {
127
+ return html`
128
+ <div class="diff-viewer diff-empty">
129
+ <div class="session-empty-icon">📝</div>
130
+ <div class="session-empty-text">Select a session to view diffs</div>
131
+ </div>
132
+ `;
133
+ }
134
+
135
+ if (loading) {
136
+ return html`
137
+ <div class="diff-viewer">
138
+ <div class="diff-loading">Loading diff…</div>
139
+ </div>
140
+ `;
141
+ }
142
+
143
+ if (error) {
144
+ return html`
145
+ <div class="diff-viewer">
146
+ <div class="session-empty">
147
+ <div class="session-empty-icon">📝</div>
148
+ <div class="session-empty-text">Diff not available</div>
149
+ <button class="btn btn-primary btn-sm" onClick=${handleRetry}>
150
+ Retry
151
+ </button>
152
+ </div>
153
+ </div>
154
+ `;
155
+ }
156
+
157
+ const files = diffData?.files || [];
158
+ const totalAdditions = files.reduce(
159
+ (n, f) => n + (f.additions ?? 0),
160
+ 0,
161
+ );
162
+ const totalDeletions = files.reduce(
163
+ (n, f) => n + (f.deletions ?? 0),
164
+ 0,
165
+ );
166
+
167
+ return html`
168
+ <div class="diff-viewer">
169
+ ${files.length > 0 && html`
170
+ <div class="diff-summary">
171
+ <span>${files.length} file${files.length !== 1 ? "s" : ""} changed</span>
172
+ ${totalAdditions > 0 && html`<span class="diff-stat-add">+${totalAdditions}</span>`}
173
+ ${totalDeletions > 0 && html`<span class="diff-stat-del">-${totalDeletions}</span>`}
174
+ </div>
175
+ `}
176
+ <div class="diff-file-list">
177
+ ${files.length > 0
178
+ ? files.map(
179
+ (f) => html`<${DiffFile} key=${f.filename} file=${f} />`,
180
+ )
181
+ : html`
182
+ <div class="session-empty">
183
+ <div class="session-empty-icon">✨</div>
184
+ <div class="session-empty-text">No changes yet</div>
185
+ </div>
186
+ `}
187
+ </div>
188
+ </div>
189
+ `;
190
+ }
@@ -0,0 +1,327 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ * VirtEngine Control Center – Form / Control Components
3
+ * SegmentedControl, Collapsible, PullToRefresh, SearchInput,
4
+ * Toggle, Stepper, SliderControl
5
+ * ────────────────────────────────────────────────────────────── */
6
+
7
+ import { h } from "preact";
8
+ import {
9
+ useState,
10
+ useRef,
11
+ useCallback,
12
+ } from "preact/hooks";
13
+ import htm from "htm";
14
+
15
+ const html = htm.bind(h);
16
+
17
+ import { ICONS } from "../modules/icons.js";
18
+ import { haptic } from "../modules/telegram.js";
19
+
20
+ /* ═══════════════════════════════════════════════
21
+ * SegmentedControl
22
+ * ═══════════════════════════════════════════════ */
23
+
24
+ /**
25
+ * Pill-shaped segmented control.
26
+ * @param {{options: Array<{value: string, label: string}>, value: string, onChange: (v: string) => void}} props
27
+ */
28
+ export function SegmentedControl({ options = [], value, onChange, disabled = false }) {
29
+ return html`
30
+ <div class="segmented-control ${disabled ? "disabled" : ""}">
31
+ ${options.map(
32
+ (opt) => html`
33
+ <button
34
+ key=${opt.value}
35
+ class="segmented-btn ${value === opt.value ? "active" : ""}"
36
+ disabled=${disabled}
37
+ onClick=${() => {
38
+ if (disabled) return;
39
+ haptic("light");
40
+ onChange(opt.value);
41
+ }}
42
+ >
43
+ ${opt.label}
44
+ </button>
45
+ `,
46
+ )}
47
+ </div>
48
+ `;
49
+ }
50
+
51
+ /* ═══════════════════════════════════════════════
52
+ * Collapsible
53
+ * ═══════════════════════════════════════════════ */
54
+
55
+ /**
56
+ * Expandable section with chevron rotation animation.
57
+ * @param {{title: string, defaultOpen?: boolean, children?: any}} props
58
+ */
59
+ export function Collapsible({ title, defaultOpen = true, children }) {
60
+ const [open, setOpen] = useState(defaultOpen);
61
+
62
+ return html`
63
+ <div class="collapsible">
64
+ <button
65
+ class="collapsible-header ${open ? "open" : ""}"
66
+ onClick=${() => {
67
+ haptic("light");
68
+ setOpen(!open);
69
+ }}
70
+ >
71
+ <span class="collapsible-title">${title}</span>
72
+ <span class="collapsible-chevron ${open ? "open" : ""}">
73
+ ${ICONS.chevronDown}
74
+ </span>
75
+ </button>
76
+ <div class="collapsible-body ${open ? "open" : ""}">${children}</div>
77
+ </div>
78
+ `;
79
+ }
80
+
81
+ /* ═══════════════════════════════════════════════
82
+ * PullToRefresh
83
+ * ═══════════════════════════════════════════════ */
84
+
85
+ /**
86
+ * Wraps content with pull-to-refresh gesture detection.
87
+ * Shows a spinner while refreshing.
88
+ * @param {{onRefresh: () => Promise<void>, children?: any}} props
89
+ */
90
+ export function PullToRefresh({ onRefresh, children }) {
91
+ const [refreshing, setRefreshing] = useState(false);
92
+ const [pullDistance, setPullDistance] = useState(0);
93
+ const containerRef = useRef(null);
94
+ const startYRef = useRef(0);
95
+ const pullingRef = useRef(false);
96
+
97
+ const THRESHOLD = 64;
98
+
99
+ const handleTouchStart = useCallback((e) => {
100
+ if (containerRef.current && containerRef.current.scrollTop <= 0) {
101
+ startYRef.current = e.touches[0].clientY;
102
+ pullingRef.current = true;
103
+ }
104
+ }, []);
105
+
106
+ const handleTouchMove = useCallback((e) => {
107
+ if (!pullingRef.current) return;
108
+ const diff = e.touches[0].clientY - startYRef.current;
109
+ if (diff > 0) {
110
+ // Apply diminishing returns to pull distance
111
+ setPullDistance(Math.min(diff * 0.4, THRESHOLD * 1.5));
112
+ }
113
+ }, []);
114
+
115
+ const handleTouchEnd = useCallback(async () => {
116
+ if (!pullingRef.current) return;
117
+ pullingRef.current = false;
118
+
119
+ if (pullDistance >= THRESHOLD) {
120
+ setRefreshing(true);
121
+ haptic("medium");
122
+ try {
123
+ await onRefresh();
124
+ } finally {
125
+ setRefreshing(false);
126
+ }
127
+ }
128
+ setPullDistance(0);
129
+ }, [onRefresh, pullDistance]);
130
+
131
+ return html`
132
+ <div
133
+ ref=${containerRef}
134
+ class="pull-to-refresh-container"
135
+ onTouchStart=${handleTouchStart}
136
+ onTouchMove=${handleTouchMove}
137
+ onTouchEnd=${handleTouchEnd}
138
+ >
139
+ ${(refreshing || pullDistance > 0) &&
140
+ html`
141
+ <div
142
+ class="ptr-indicator"
143
+ style="height: ${refreshing ? THRESHOLD : pullDistance}px;
144
+ display:flex;align-items:center;justify-content:center;
145
+ transition: ${pullingRef.current ? "none" : "height 0.2s ease"}"
146
+ >
147
+ <div
148
+ class="ptr-spinner-icon ${refreshing ? "spinning" : ""}"
149
+ style="transform: rotate(${pullDistance * 4}deg);
150
+ opacity: ${Math.min(1, pullDistance / THRESHOLD)}"
151
+ >
152
+ ${ICONS.refresh}
153
+ </div>
154
+ </div>
155
+ `}
156
+ ${children}
157
+ </div>
158
+ `;
159
+ }
160
+
161
+ /* ═══════════════════════════════════════════════
162
+ * SearchInput
163
+ * ═══════════════════════════════════════════════ */
164
+
165
+ /**
166
+ * Search input with magnifying glass icon and clear button.
167
+ * @param {{value: string, onInput: (e: Event) => void, placeholder?: string, onClear?: () => void}} props
168
+ */
169
+ export function SearchInput({
170
+ value = "",
171
+ onInput,
172
+ placeholder = "Search…",
173
+ onClear,
174
+ disabled = false,
175
+ inputRef,
176
+ }) {
177
+ return html`
178
+ <div class="search-input-wrap ${disabled ? "disabled" : ""}">
179
+ <span class="search-input-icon">${ICONS.search}</span>
180
+ <input
181
+ ref=${inputRef}
182
+ class="input search-input"
183
+ type="text"
184
+ placeholder=${placeholder}
185
+ value=${value}
186
+ onInput=${onInput}
187
+ disabled=${disabled}
188
+ />
189
+ ${value && !disabled
190
+ ? html`
191
+ <button
192
+ class="search-input-clear"
193
+ onClick=${() => {
194
+ if (onClear) onClear();
195
+ }}
196
+ >
197
+ ${ICONS.close}
198
+ </button>
199
+ `
200
+ : null}
201
+ </div>
202
+ `;
203
+ }
204
+
205
+ /* ═══════════════════════════════════════════════
206
+ * Toggle
207
+ * ═══════════════════════════════════════════════ */
208
+
209
+ /**
210
+ * iOS-style toggle switch.
211
+ * @param {{checked: boolean, onChange: (checked: boolean) => void, label?: string}} props
212
+ */
213
+ export function Toggle({ checked = false, onChange, label, disabled = false }) {
214
+ const handleClick = () => {
215
+ if (disabled) return;
216
+ haptic("light");
217
+ onChange(!checked);
218
+ };
219
+
220
+ return html`
221
+ <div class="toggle-wrap ${disabled ? "disabled" : ""}" onClick=${handleClick}>
222
+ ${label ? html`<span class="toggle-label">${label}</span>` : null}
223
+ <div class="toggle ${checked ? "toggle-on" : ""} ${disabled ? "disabled" : ""}">
224
+ <div class="toggle-thumb"></div>
225
+ </div>
226
+ </div>
227
+ `;
228
+ }
229
+
230
+ /* ═══════════════════════════════════════════════
231
+ * Stepper
232
+ * ═══════════════════════════════════════════════ */
233
+
234
+ /**
235
+ * Numeric stepper with − and + buttons.
236
+ * @param {{value: number, min?: number, max?: number, step?: number, onChange: (v: number) => void, label?: string}} props
237
+ */
238
+ export function Stepper({
239
+ value = 0,
240
+ min = 0,
241
+ max = 100,
242
+ step = 1,
243
+ onChange,
244
+ label,
245
+ disabled = false,
246
+ }) {
247
+ const decrement = () => {
248
+ if (disabled) return;
249
+ const next = Math.max(min, value - step);
250
+ if (next !== value) {
251
+ haptic("light");
252
+ onChange(next);
253
+ }
254
+ };
255
+ const increment = () => {
256
+ if (disabled) return;
257
+ const next = Math.min(max, value + step);
258
+ if (next !== value) {
259
+ haptic("light");
260
+ onChange(next);
261
+ }
262
+ };
263
+
264
+ return html`
265
+ <div class="stepper-wrap ${disabled ? "disabled" : ""}">
266
+ ${label ? html`<span class="stepper-label">${label}</span>` : null}
267
+ <div class="stepper ${disabled ? "disabled" : ""}">
268
+ <button
269
+ class="stepper-btn"
270
+ onClick=${decrement}
271
+ disabled=${disabled || value <= min}
272
+ >
273
+
274
+ </button>
275
+ <span class="stepper-value">${value}</span>
276
+ <button
277
+ class="stepper-btn"
278
+ onClick=${increment}
279
+ disabled=${disabled || value >= max}
280
+ >
281
+ +
282
+ </button>
283
+ </div>
284
+ </div>
285
+ `;
286
+ }
287
+
288
+ /* ═══════════════════════════════════════════════
289
+ * SliderControl
290
+ * ═══════════════════════════════════════════════ */
291
+
292
+ /**
293
+ * Range slider with value display pill.
294
+ * @param {{value: number, min?: number, max?: number, step?: number, onChange: (v: number) => void, label?: string, suffix?: string}} props
295
+ */
296
+ export function SliderControl({
297
+ value = 0,
298
+ min = 0,
299
+ max = 100,
300
+ step = 1,
301
+ onChange,
302
+ label,
303
+ suffix = "",
304
+ }) {
305
+ return html`
306
+ <div class="slider-control">
307
+ ${label
308
+ ? html`<div class="slider-control-header">
309
+ <span class="slider-control-label">${label}</span>
310
+ <span class="pill">${value}${suffix}</span>
311
+ </div>`
312
+ : null}
313
+ <div class="slider-control-row">
314
+ <input
315
+ type="range"
316
+ class="slider-input"
317
+ min=${min}
318
+ max=${max}
319
+ step=${step}
320
+ value=${value}
321
+ onInput=${(e) => onChange(Number(e.target.value))}
322
+ />
323
+ ${!label ? html`<span class="pill">${value}${suffix}</span>` : null}
324
+ </div>
325
+ </div>
326
+ `;
327
+ }