aidevops 3.29.0 → 3.29.2
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/README.md +1 -1
- package/VERSION +1 -1
- package/aidevops.sh +1 -1
- package/package.json +1 -1
- package/packages/gui-api/src/app.ts +8 -0
- package/packages/gui-api/src/status-adapter-utils.ts +1 -1
- package/packages/gui-api/src/status-vault.ts +1 -1
- package/packages/gui-desktop/scripts/install-macos-app.sh +76 -37
- package/packages/gui-shared/src/contracts.ts +110 -0
- package/packages/gui-web/src/App.tsx +70 -1
- package/packages/gui-web/src/styles.css +207 -0
- package/setup.sh +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ The result: an AI operations platform that manages projects across every busines
|
|
|
58
58
|
[](https://github.com/marcusquinn)
|
|
59
59
|
|
|
60
60
|
<!-- Release & Version Info -->
|
|
61
|
-
[](https://github.com/marcusquinn/aidevops/releases)
|
|
62
62
|
[](https://www.npmjs.com/package/aidevops)
|
|
63
63
|
[](https://github.com/marcusquinn/homebrew-tap)
|
|
64
64
|
[](https://github.com/marcusquinn/aidevops)
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.29.
|
|
1
|
+
3.29.2
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -35,6 +35,14 @@ const appActionJobs = new Map<string, GuiAppActionJobSummary>();
|
|
|
35
35
|
export function createGuiApiApp() {
|
|
36
36
|
const app = new Hono();
|
|
37
37
|
|
|
38
|
+
app.get("/api/health", (context) => {
|
|
39
|
+
return context.json(createEnvelope({
|
|
40
|
+
operation_id: "capabilities.read",
|
|
41
|
+
source: { surface: "health", authority: "in-process readiness probe", path_refs: [] },
|
|
42
|
+
data: { status: "ok", service: "aidevops-gui-api" },
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
|
|
38
46
|
app.get(STATUS_ROUTE_MANIFEST.route, (context) => {
|
|
39
47
|
return context.json(readStatus());
|
|
40
48
|
});
|
|
@@ -134,7 +134,7 @@ export function readBinaryVersion(binaryPath: string, args: string[]): string {
|
|
|
134
134
|
const output = execFileSync(binaryPath, args, {
|
|
135
135
|
encoding: "utf8",
|
|
136
136
|
stdio: ["ignore", "pipe", "ignore"],
|
|
137
|
-
timeout:
|
|
137
|
+
timeout: 150,
|
|
138
138
|
}).trim();
|
|
139
139
|
return output.split("\n").find((line) => line.length > 0) ?? "unknown";
|
|
140
140
|
} catch {
|
|
@@ -63,7 +63,7 @@ function readVaultCommand<T extends string>(
|
|
|
63
63
|
const output = execFileSync("sh", [helperPath, ...args], {
|
|
64
64
|
encoding: "utf8",
|
|
65
65
|
stdio: ["ignore", "pipe", "ignore"],
|
|
66
|
-
timeout:
|
|
66
|
+
timeout: 300,
|
|
67
67
|
}).trim();
|
|
68
68
|
const firstLine = output.split(/\r?\n/).find((line) => line.trim().length > 0)?.trim() ?? "";
|
|
69
69
|
return isAllowed(firstLine) ? firstLine : null;
|
|
@@ -135,6 +135,8 @@ set -euo pipefail
|
|
|
135
135
|
REPO_ROOT="${root}"
|
|
136
136
|
API_PORT="\${AIDEVOPS_GUI_API_PORT:-8787}"
|
|
137
137
|
WEB_PORT="\${AIDEVOPS_GUI_WEB_PORT:-5173}"
|
|
138
|
+
API_HEALTH_URL="http://127.0.0.1:\${API_PORT}/api/health"
|
|
139
|
+
WEB_HEALTH_URL="http://127.0.0.1:\${WEB_PORT}/"
|
|
138
140
|
LOG_DIR="\${HOME}/Library/Logs/aidevops-gui"
|
|
139
141
|
LAUNCHER_LOG="\${LOG_DIR}/launcher.log"
|
|
140
142
|
BUN_BIN=""
|
|
@@ -223,20 +225,18 @@ replace_stale_gui_on_port() {
|
|
|
223
225
|
local pid=""
|
|
224
226
|
local cwd=""
|
|
225
227
|
|
|
226
|
-
if
|
|
227
|
-
|
|
228
|
+
if url_ready "\${health_url}"; then
|
|
229
|
+
while IFS= read -r pid; do
|
|
230
|
+
if [[ -z "\${pid}" ]]; then
|
|
231
|
+
continue
|
|
232
|
+
fi
|
|
233
|
+
cwd="\$(pid_cwd "\${pid}" || true)"
|
|
234
|
+
if [[ "\${cwd}" == "\${REPO_ROOT}" ]]; then
|
|
235
|
+
return 0
|
|
236
|
+
fi
|
|
237
|
+
done < <(port_pids "\${port}")
|
|
228
238
|
fi
|
|
229
239
|
|
|
230
|
-
while IFS= read -r pid; do
|
|
231
|
-
if [[ -z "\${pid}" ]]; then
|
|
232
|
-
continue
|
|
233
|
-
fi
|
|
234
|
-
cwd="\$(pid_cwd "\${pid}" || true)"
|
|
235
|
-
if [[ "\${cwd}" == "\${REPO_ROOT}" ]]; then
|
|
236
|
-
return 0
|
|
237
|
-
fi
|
|
238
|
-
done < <(port_pids "\${port}")
|
|
239
|
-
|
|
240
240
|
while IFS= read -r pid; do
|
|
241
241
|
if [[ -z "\${pid}" ]]; then
|
|
242
242
|
continue
|
|
@@ -274,16 +274,6 @@ stop_gui_services() {
|
|
|
274
274
|
return 0
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
api_contract_current() {
|
|
278
|
-
local payload=""
|
|
279
|
-
|
|
280
|
-
payload="\$(curl --silent --fail --max-time 2 "http://127.0.0.1:\${API_PORT}/api/status" 2>/dev/null || true)"
|
|
281
|
-
if [[ "\${payload}" == *'"local_repos"'* && "\${payload}" == *'"oauth_pool"'* ]]; then
|
|
282
|
-
return 0
|
|
283
|
-
fi
|
|
284
|
-
return 1
|
|
285
|
-
}
|
|
286
|
-
|
|
287
277
|
if [[ "\${MODE}" == "stop" ]]; then
|
|
288
278
|
stop_gui_services
|
|
289
279
|
exit 0
|
|
@@ -297,36 +287,33 @@ if [[ -f "\${HOME}/.aidevops/agents/VERSION" && -f "\${REPO_ROOT}/VERSION" ]]; t
|
|
|
297
287
|
fi
|
|
298
288
|
fi
|
|
299
289
|
|
|
300
|
-
replace_stale_gui_on_port "\${API_PORT}" "
|
|
301
|
-
replace_stale_gui_on_port "\${WEB_PORT}" "
|
|
302
|
-
if url_ready "http://127.0.0.1:\${API_PORT}/api/status" && ! api_contract_current; then
|
|
303
|
-
kill_gui_on_port_for_repo "\${API_PORT}"
|
|
304
|
-
kill_gui_on_port_for_repo "\${WEB_PORT}"
|
|
305
|
-
fi
|
|
290
|
+
replace_stale_gui_on_port "\${API_PORT}" "\${API_HEALTH_URL}"
|
|
291
|
+
replace_stale_gui_on_port "\${WEB_PORT}" "\${WEB_HEALTH_URL}"
|
|
306
292
|
sleep 1
|
|
307
293
|
|
|
308
|
-
if ! url_ready "
|
|
294
|
+
if ! url_ready "\${API_HEALTH_URL}" || ! url_ready "\${WEB_HEALTH_URL}"; then
|
|
309
295
|
require_bun
|
|
310
296
|
fi
|
|
311
297
|
|
|
312
|
-
if ! url_ready "
|
|
298
|
+
if ! url_ready "\${API_HEALTH_URL}"; then
|
|
313
299
|
nohup env AIDEVOPS_GUI_API_PORT="\${API_PORT}" "\${BUN_BIN}" run packages/gui-api/src/server.ts >"\${LOG_DIR}/api.log" 2>&1 &
|
|
314
300
|
fi
|
|
315
|
-
if ! url_ready "
|
|
301
|
+
if ! url_ready "\${WEB_HEALTH_URL}"; then
|
|
316
302
|
nohup "\${BUN_BIN}" ./node_modules/vite/bin/vite.js --config packages/gui-web/vite.config.ts --host 127.0.0.1 --port "\${WEB_PORT}" >"\${LOG_DIR}/web.log" 2>&1 &
|
|
317
303
|
fi
|
|
318
304
|
|
|
319
|
-
if ! wait_for_url "
|
|
305
|
+
if ! wait_for_url "\${API_HEALTH_URL}" 20 0.25; then
|
|
320
306
|
printf 'aidevops GUI API did not become ready. Check %s/api.log.\n' "\${LOG_DIR}" >"\${LAUNCHER_LOG}"
|
|
321
307
|
notify "aidevops GUI API did not become ready. Check \${LOG_DIR}/api.log."
|
|
322
308
|
exit 1
|
|
323
309
|
fi
|
|
324
|
-
if ! wait_for_url "
|
|
310
|
+
if ! wait_for_url "\${WEB_HEALTH_URL}" 30 0.25; then
|
|
325
311
|
printf 'aidevops GUI web did not become ready. Check %s/web.log.\n' "\${LOG_DIR}" >"\${LAUNCHER_LOG}"
|
|
326
312
|
notify "aidevops GUI web did not become ready. Check \${LOG_DIR}/web.log."
|
|
327
313
|
exit 1
|
|
328
314
|
fi
|
|
329
315
|
|
|
316
|
+
printf 'aidevops GUI services ready: API %s, web %s\n' "\${API_HEALTH_URL}" "\${WEB_HEALTH_URL}" >"\${LAUNCHER_LOG}"
|
|
330
317
|
exit 0
|
|
331
318
|
LAUNCHER
|
|
332
319
|
chmod 755 "$helper_path"
|
|
@@ -362,6 +349,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
|
|
|
362
349
|
private let mainWindowFrameAutosaveName = "aidevops-main-window"
|
|
363
350
|
private let titlebarHeight: CGFloat = 24
|
|
364
351
|
private let serviceQueue = DispatchQueue(label: "sh.aidevops.gui.services")
|
|
352
|
+
private var hasLoadedDashboard = false
|
|
365
353
|
private var servicesStopped = false
|
|
366
354
|
|
|
367
355
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
@@ -604,7 +592,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
|
|
|
604
592
|
@objc private func openDashboard(_ sender: Any?) {
|
|
605
593
|
window.makeKeyAndOrderFront(nil)
|
|
606
594
|
NSApp.activate(ignoringOtherApps: true)
|
|
607
|
-
|
|
595
|
+
if !hasLoadedDashboard {
|
|
596
|
+
loadAppURL()
|
|
597
|
+
}
|
|
608
598
|
}
|
|
609
599
|
|
|
610
600
|
@objc private func hideWindow(_ sender: Any?) {
|
|
@@ -895,12 +885,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
|
|
|
895
885
|
loadStatusHTML(title: "Invalid aidevops URL", detail: "The configured web port is not valid: \(webPort)")
|
|
896
886
|
return
|
|
897
887
|
}
|
|
888
|
+
hasLoadedDashboard = true
|
|
898
889
|
webView.load(URLRequest(url: url))
|
|
899
890
|
}
|
|
900
891
|
|
|
901
892
|
private func loadStatusHTML(title: String, detail: String) {
|
|
902
893
|
let escapedTitle = escapeHTML(title)
|
|
903
894
|
let escapedDetail = escapeHTML(detail).replacingOccurrences(of: "\n", with: "<br>")
|
|
895
|
+
let contentHTML: String
|
|
896
|
+
if title == "Starting aidevops" {
|
|
897
|
+
contentHTML = """
|
|
898
|
+
<main class="shell" aria-busy="true" aria-label="Loading aidevops interface">
|
|
899
|
+
<section class="brand" aria-label="Starting aidevops"><strong><span>aidevops</span><b>></b><em>_</em></strong><small>Preparing local GUI</small></section>
|
|
900
|
+
<aside class="rail panel"><i></i><i></i><i></i><i></i><i></i><i></i></aside>
|
|
901
|
+
<aside class="sidebar panel"><header><b></b><span></span></header><nav><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></nav><footer><i></i><i></i></footer></aside>
|
|
902
|
+
<section class="workspace panel"><header><b></b><span></span><i></i><i></i></header><article><h1>\(escapedTitle)</h1><p>\(escapedDetail)</p><div class="cards"><i></i><i></i><i></i><i></i><i></i><i></i></div><div class="block"></div></article></section>
|
|
903
|
+
<footer class="status"><i></i><span></span><span></span><span></span><span></span><span></span></footer>
|
|
904
|
+
</main>
|
|
905
|
+
"""
|
|
906
|
+
} else {
|
|
907
|
+
contentHTML = "<main class=\"message\"><h1>\(escapedTitle)</h1><p>\(escapedDetail)</p></main>"
|
|
908
|
+
}
|
|
904
909
|
let html = """
|
|
905
910
|
<!doctype html>
|
|
906
911
|
<html lang="en">
|
|
@@ -908,14 +913,48 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
|
|
|
908
913
|
<meta charset="utf-8">
|
|
909
914
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
910
915
|
<style>
|
|
911
|
-
|
|
912
|
-
|
|
916
|
+
:root { --accent: #b2e969; --border: #2c3324; --panel: #11130f; --muted: #b9c3b0; }
|
|
917
|
+
* { box-sizing: border-box; }
|
|
918
|
+
body { background: radial-gradient(ellipse at top left, rgb(178 233 105 / 10%), transparent 34%), #0a0a0a; color: #f6f7f2; font: 15px -apple-system, BlinkMacSystemFont, sans-serif; height: 100vh; margin: 0; }
|
|
919
|
+
.message { background: var(--panel); border: 1px solid var(--border); border-radius: 18px; left: 50%; max-width: 560px; padding: 28px; position: fixed; top: 50%; transform: translate(-50%, -50%); }
|
|
913
920
|
h1 { font-size: 22px; margin: 0 0 10px; }
|
|
914
921
|
p { color: #b9c3b0; line-height: 1.5; margin: 0; }
|
|
915
922
|
code { color: #b2e969; }
|
|
923
|
+
.shell { display: grid; gap: 8px; grid-template-columns: 72px 302px minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) 30px; height: 100vh; padding: 30px 8px 8px; }
|
|
924
|
+
.brand { align-items: center; backdrop-filter: blur(18px); background: rgb(17 19 15 / 78%); border: 1px solid rgb(178 233 105 / 34%); border-radius: 24px; box-shadow: 0 24px 90px rgb(178 233 105 / 14%), 0 20px 80px rgb(0 0 0 / 35%); display: grid; gap: 10px; justify-items: center; left: 50%; padding: 26px 30px; position: fixed; top: 50%; transform: translate(-50%, -50%); width: min(360px, calc(100vw - 48px)); z-index: 20; }
|
|
925
|
+
.brand strong { align-items: baseline; display: inline-flex; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: clamp(34px, 7vw, 64px); font-weight: 900; letter-spacing: -0.08em; line-height: .9; }
|
|
926
|
+
.brand span { color: #f6f7f2; font-size: .48em; letter-spacing: -0.04em; margin-right: .18em; }
|
|
927
|
+
.brand b, .brand em { color: var(--accent); font-style: normal; }
|
|
928
|
+
.brand em { animation: cursor 0.95s steps(1, end) infinite; }
|
|
929
|
+
.brand small { color: var(--muted); font-size: 11px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; }
|
|
930
|
+
.panel, .status { background: rgb(17 19 15 / 92%); border: 1px solid var(--border); border-radius: 18px; box-shadow: inset 0 1px 0 rgb(255 255 255 / 8%); overflow: hidden; }
|
|
931
|
+
.rail { align-content: start; display: grid; gap: 10px; justify-items: center; padding: 12px 8px; }
|
|
932
|
+
.rail i, .sidebar b, .workspace b { border-radius: 12px; height: 44px; width: 44px; }
|
|
933
|
+
.sidebar { display: grid; grid-template-rows: auto 1fr auto; }
|
|
934
|
+
.sidebar header, .workspace header { align-items: center; display: flex; gap: 12px; padding: 16px; }
|
|
935
|
+
.sidebar header span { flex: 1; height: 18px; }
|
|
936
|
+
.sidebar nav, .sidebar footer, .workspace article { display: grid; gap: 12px; padding: 16px; }
|
|
937
|
+
.sidebar nav i { height: 18px; }
|
|
938
|
+
.sidebar footer { border-top: 1px solid var(--border); }
|
|
939
|
+
.workspace { display: grid; grid-template-rows: auto 1fr; }
|
|
940
|
+
.workspace header { border-bottom: 1px solid var(--border); }
|
|
941
|
+
.workspace header span { flex: 1; height: 38px; }
|
|
942
|
+
.workspace header i { border-radius: 999px; height: 34px; width: 34px; }
|
|
943
|
+
.workspace article { align-content: start; gap: 18px; }
|
|
944
|
+
.cards { display: grid; gap: 14px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
945
|
+
.cards i { border-radius: 18px; height: 128px; }
|
|
946
|
+
.block { border-radius: 20px; height: 220px; }
|
|
947
|
+
.status { align-items: center; display: flex; gap: 10px; grid-column: 1 / -1; justify-content: center; padding: 0 12px; }
|
|
948
|
+
.status i { border-radius: 999px; height: 8px; width: 8px; }
|
|
949
|
+
.status span { height: 10px; width: 96px; }
|
|
950
|
+
.rail i, .sidebar b, .sidebar span, .sidebar i, .workspace b, .workspace span, .workspace i, .cards i, .block, .status i, .status span { animation: pulse 1.35s ease-in-out infinite; background: linear-gradient(90deg, #1a2116, rgb(178 233 105 / 18%), #1a2116); background-size: 220% 100%; border: 1px solid var(--border); border-radius: 999px; display: block; }
|
|
951
|
+
.block, .cards i { border-radius: 18px; }
|
|
952
|
+
@keyframes pulse { 0% { background-position: 120% 0; opacity: .52; } 50% { opacity: .95; } 100% { background-position: -120% 0; opacity: .52; } }
|
|
953
|
+
@keyframes cursor { 0%, 48% { opacity: 1; } 49%, 100% { opacity: 0; } }
|
|
954
|
+
@media (prefers-reduced-motion: reduce) { .rail i, .sidebar b, .sidebar span, .sidebar i, .workspace b, .workspace span, .workspace i, .cards i, .block, .status i, .status span, .brand em { animation: none; } }
|
|
916
955
|
</style>
|
|
917
956
|
</head>
|
|
918
|
-
<body
|
|
957
|
+
<body>\(contentHTML)</body>
|
|
919
958
|
</html>
|
|
920
959
|
"""
|
|
921
960
|
webView.loadHTMLString(html, baseURL: nil)
|
|
@@ -185,6 +185,100 @@ export interface GuiOpenCodeSessionRegistrySummary {
|
|
|
185
185
|
sessions: GuiOpenCodeSessionSummary[];
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
export type GuiConversationType = "ai_session" | "channel" | "dm" | "group_dm" | "worker_thread";
|
|
189
|
+
|
|
190
|
+
export type GuiConversationParticipantKind = "human" | "ai_assistant" | "worker" | "system_bot";
|
|
191
|
+
|
|
192
|
+
export type GuiConversationMessageSenderKind = GuiConversationParticipantKind | "system";
|
|
193
|
+
|
|
194
|
+
export type GuiConversationMessagePartKind = "text" | "file" | "tool_call" | "source" | "tambo_component" | "approval_prompt" | "event_marker";
|
|
195
|
+
|
|
196
|
+
export interface GuiConversationScope {
|
|
197
|
+
tenant_ref: string;
|
|
198
|
+
workspace_ref: string;
|
|
199
|
+
repo_ref: string | null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface GuiConversation {
|
|
203
|
+
id: string;
|
|
204
|
+
type: GuiConversationType;
|
|
205
|
+
title: string;
|
|
206
|
+
scope: GuiConversationScope;
|
|
207
|
+
source_ref: string;
|
|
208
|
+
status: "active" | "archived" | "read_only";
|
|
209
|
+
created_at: string;
|
|
210
|
+
updated_at: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface GuiConversationParticipant {
|
|
214
|
+
id: string;
|
|
215
|
+
conversation_id: string;
|
|
216
|
+
kind: GuiConversationParticipantKind;
|
|
217
|
+
display_name: string;
|
|
218
|
+
identity_ref: string | null;
|
|
219
|
+
agent_ref: string | null;
|
|
220
|
+
worker_ref: string | null;
|
|
221
|
+
membership_state: "active" | "invited" | "left" | "removed";
|
|
222
|
+
joined_at: string;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface GuiConversationUsageMetadata {
|
|
226
|
+
provider_ref: string | null;
|
|
227
|
+
model_ref: string | null;
|
|
228
|
+
input_tokens: number;
|
|
229
|
+
output_tokens: number;
|
|
230
|
+
total_tokens: number;
|
|
231
|
+
cost_ref: string | null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface GuiConversationMessage {
|
|
235
|
+
id: string;
|
|
236
|
+
conversation_id: string;
|
|
237
|
+
sender_participant_id: string | null;
|
|
238
|
+
sender_kind: GuiConversationMessageSenderKind;
|
|
239
|
+
sequence: number;
|
|
240
|
+
status: "draft" | "sent" | "delivered" | "failed" | "redacted";
|
|
241
|
+
usage: GuiConversationUsageMetadata | null;
|
|
242
|
+
created_at: string;
|
|
243
|
+
edited_at: string | null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface GuiConversationMessagePart {
|
|
247
|
+
id: string;
|
|
248
|
+
message_id: string;
|
|
249
|
+
kind: GuiConversationMessagePartKind;
|
|
250
|
+
ordinal: number;
|
|
251
|
+
text: string | null;
|
|
252
|
+
payload_json: Record<string, unknown> | null;
|
|
253
|
+
file_ref: string | null;
|
|
254
|
+
source_ref: string | null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface GuiConversationReaction {
|
|
258
|
+
id: string;
|
|
259
|
+
message_id: string;
|
|
260
|
+
participant_id: string;
|
|
261
|
+
reaction: string;
|
|
262
|
+
created_at: string;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface GuiConversationReadState {
|
|
266
|
+
conversation_id: string;
|
|
267
|
+
participant_id: string;
|
|
268
|
+
last_read_message_id: string | null;
|
|
269
|
+
last_read_sequence: number;
|
|
270
|
+
updated_at: string;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface GuiConversationThread {
|
|
274
|
+
conversation: GuiConversation;
|
|
275
|
+
participants: GuiConversationParticipant[];
|
|
276
|
+
messages: GuiConversationMessage[];
|
|
277
|
+
parts: GuiConversationMessagePart[];
|
|
278
|
+
reactions: GuiConversationReaction[];
|
|
279
|
+
read_states: GuiConversationReadState[];
|
|
280
|
+
}
|
|
281
|
+
|
|
188
282
|
export type GuiAiProviderId = "anthropic" | "openai" | "cursor" | "google";
|
|
189
283
|
|
|
190
284
|
export interface GuiOAuthPoolAccountSummary {
|
|
@@ -499,3 +593,19 @@ export function assertNoSecretSentinels(value: unknown): void {
|
|
|
499
593
|
export function isReadOnlyManifest(manifest: GuiRouteManifest): boolean {
|
|
500
594
|
return manifest.method === "GET" && manifest.classification === "read";
|
|
501
595
|
}
|
|
596
|
+
|
|
597
|
+
export function sortConversationMessages<TMessage extends Pick<GuiConversationMessage, "sequence" | "created_at">>(messages: TMessage[]): TMessage[] {
|
|
598
|
+
return [...messages].sort((left, right) => left.sequence - right.sequence || left.created_at.localeCompare(right.created_at));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export function sortConversationMessageParts<TPart extends Pick<GuiConversationMessagePart, "ordinal">>(parts: TPart[]): TPart[] {
|
|
602
|
+
return [...parts].sort((left, right) => left.ordinal - right.ordinal);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export function participantCanReadConversation(thread: GuiConversationThread, participantId: string): boolean {
|
|
606
|
+
return thread.participants.some((participant) => participant.id === participantId && participant.membership_state === "active");
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function conversationHasScope(thread: GuiConversationThread, scope: GuiConversationScope): boolean {
|
|
610
|
+
return thread.conversation.scope.tenant_ref === scope.tenant_ref && thread.conversation.scope.workspace_ref === scope.workspace_ref && thread.conversation.scope.repo_ref === scope.repo_ref;
|
|
611
|
+
}
|
|
@@ -39,6 +39,8 @@ export const appearanceStorageKeys = {
|
|
|
39
39
|
const defaultSidebarWidth = 302;
|
|
40
40
|
const minSidebarWidth = 248;
|
|
41
41
|
const maxSidebarWidth = 520;
|
|
42
|
+
export const loadingSkeletonPanelLabels = ["machine rail", "sidebar", "workspace", "status bar"] as const;
|
|
43
|
+
export const loadingBrandGlyph = ">_";
|
|
42
44
|
|
|
43
45
|
export interface StoredAppearancePreferences {
|
|
44
46
|
accentHue: number;
|
|
@@ -70,6 +72,7 @@ export function readStoredAppearancePreferences(storage: ReadableAppearanceStora
|
|
|
70
72
|
export function App(): ReactElement {
|
|
71
73
|
const [storedAppearancePreferences] = useState<StoredAppearancePreferences>(() => readStoredAppearancePreferences());
|
|
72
74
|
const [status, setStatus] = useState<GuiResponseEnvelope<GuiStatusData>>(mockedStatus());
|
|
75
|
+
const [statusLoading, setStatusLoading] = useState(true);
|
|
73
76
|
const [navigation, setNavigation] = useState<NavigationHistory>({ entries: ["overview"], index: 0 });
|
|
74
77
|
const [themePreference, setThemePreference] = useState<ThemePreference>(storedAppearancePreferences.themePreference);
|
|
75
78
|
const [accentHue, setAccentHue] = useState(storedAppearancePreferences.accentHue);
|
|
@@ -159,7 +162,8 @@ export function App(): ReactElement {
|
|
|
159
162
|
useEffect(() => {
|
|
160
163
|
fetchStatus()
|
|
161
164
|
.then(setStatus)
|
|
162
|
-
.catch(() => setStatus(mockedStatus()))
|
|
165
|
+
.catch(() => setStatus(mockedStatus()))
|
|
166
|
+
.finally(() => setStatusLoading(false));
|
|
163
167
|
}, []);
|
|
164
168
|
|
|
165
169
|
useEffect(() => {
|
|
@@ -179,6 +183,10 @@ export function App(): ReactElement {
|
|
|
179
183
|
setSelectedLocalRepoIndex((current) => Math.min(current, Math.max(0, status.data.local_repos.repos.length - 1)));
|
|
180
184
|
}, [status.data.local_repos.repos.length]);
|
|
181
185
|
|
|
186
|
+
if (statusLoading) {
|
|
187
|
+
return <AppLoadingSkeleton machineRailVisible={machineRailVisible} />;
|
|
188
|
+
}
|
|
189
|
+
|
|
182
190
|
return (
|
|
183
191
|
<main className={machineRailVisible ? "app-shell" : "app-shell machine-rail-collapsed"}>
|
|
184
192
|
<div className="desktop-titlebar-tagline" aria-hidden="true">Your data protected. Your systems managed. Your creations published.</div>
|
|
@@ -234,6 +242,67 @@ export function App(): ReactElement {
|
|
|
234
242
|
);
|
|
235
243
|
}
|
|
236
244
|
|
|
245
|
+
export function AppLoadingSkeleton({ machineRailVisible }: { machineRailVisible: boolean }): ReactElement {
|
|
246
|
+
return (
|
|
247
|
+
<main className={machineRailVisible ? "app-shell app-loading-shell" : "app-shell app-loading-shell machine-rail-collapsed"} aria-busy="true" aria-label="Loading aidevops interface">
|
|
248
|
+
<div className="desktop-titlebar-tagline loading-line loading-titlebar" aria-hidden="true" />
|
|
249
|
+
<LoadingBrandOverlay />
|
|
250
|
+
{machineRailVisible ? (
|
|
251
|
+
<section className="machine-rail loading-panel" aria-label={loadingSkeletonPanelLabels[0]}>
|
|
252
|
+
{Array.from({ length: 6 }, (_, index) => <span className="loading-orb" key={index} />)}
|
|
253
|
+
</section>
|
|
254
|
+
) : null}
|
|
255
|
+
<section className="app-sidebar loading-panel" aria-label={loadingSkeletonPanelLabels[1]}>
|
|
256
|
+
<div className="loading-sidebar-header">
|
|
257
|
+
<span className="loading-logo" />
|
|
258
|
+
<span className="loading-line loading-line-wide" />
|
|
259
|
+
</div>
|
|
260
|
+
<span className="loading-pill" />
|
|
261
|
+
<div className="loading-list">
|
|
262
|
+
{Array.from({ length: 12 }, (_, index) => <span className={index % 3 === 0 ? "loading-line loading-line-short" : "loading-line"} key={index} />)}
|
|
263
|
+
</div>
|
|
264
|
+
<div className="loading-sidebar-footer">
|
|
265
|
+
<span className="loading-pill" />
|
|
266
|
+
<span className="loading-line" />
|
|
267
|
+
</div>
|
|
268
|
+
</section>
|
|
269
|
+
<SidebarResizeHandle />
|
|
270
|
+
<section className="app-inset loading-panel" aria-label={loadingSkeletonPanelLabels[2]}>
|
|
271
|
+
<div className="workspace-header loading-workspace-header">
|
|
272
|
+
<span className="loading-icon" />
|
|
273
|
+
<span className="loading-line loading-line-wide" />
|
|
274
|
+
<span className="loading-search" />
|
|
275
|
+
<span className="loading-circle" />
|
|
276
|
+
<span className="loading-circle" />
|
|
277
|
+
</div>
|
|
278
|
+
<div className="loading-workspace-body">
|
|
279
|
+
<span className="loading-line loading-line-hero" />
|
|
280
|
+
<div className="loading-card-grid">
|
|
281
|
+
{Array.from({ length: 6 }, (_, index) => <span className="loading-card" key={index} />)}
|
|
282
|
+
</div>
|
|
283
|
+
<span className="loading-panel-block" />
|
|
284
|
+
</div>
|
|
285
|
+
</section>
|
|
286
|
+
<div className="desktop-status-bar loading-status-bar" aria-label={loadingSkeletonPanelLabels[3]}>
|
|
287
|
+
{Array.from({ length: 8 }, (_, index) => <span className={index === 0 ? "loading-dot" : "loading-line"} key={index} />)}
|
|
288
|
+
</div>
|
|
289
|
+
</main>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function LoadingBrandOverlay(): ReactElement {
|
|
294
|
+
return (
|
|
295
|
+
<div className="loading-brand-overlay" aria-label="Starting aidevops">
|
|
296
|
+
<span className="loading-brand-mark" aria-hidden="true">
|
|
297
|
+
<span className="loading-brand-word">aidevops</span>
|
|
298
|
+
<span className="loading-brand-chevron">></span>
|
|
299
|
+
<span className="loading-brand-cursor">_</span>
|
|
300
|
+
</span>
|
|
301
|
+
<span className="loading-brand-status">Preparing local GUI</span>
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
237
306
|
export function clampSidebarWidth(width: number): number {
|
|
238
307
|
return Math.min(maxSidebarWidth, Math.max(minSidebarWidth, Math.round(width)));
|
|
239
308
|
}
|
|
@@ -2168,6 +2168,213 @@ code {
|
|
|
2168
2168
|
text-transform: uppercase;
|
|
2169
2169
|
}
|
|
2170
2170
|
|
|
2171
|
+
.app-loading-shell .loading-panel {
|
|
2172
|
+
overflow: hidden;
|
|
2173
|
+
position: relative;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
.loading-brand-overlay {
|
|
2177
|
+
align-items: center;
|
|
2178
|
+
backdrop-filter: blur(18px);
|
|
2179
|
+
background: color-mix(in srgb, var(--surface) 78%, transparent);
|
|
2180
|
+
border: 1px solid color-mix(in srgb, var(--accent) 34%, var(--border));
|
|
2181
|
+
border-radius: 24px;
|
|
2182
|
+
box-shadow: 0 24px 90px color-mix(in srgb, var(--accent) 14%, transparent), var(--shadow-soft);
|
|
2183
|
+
display: grid;
|
|
2184
|
+
gap: 10px;
|
|
2185
|
+
justify-items: center;
|
|
2186
|
+
left: 50%;
|
|
2187
|
+
padding: 26px 30px;
|
|
2188
|
+
position: fixed;
|
|
2189
|
+
top: 50%;
|
|
2190
|
+
transform: translate(-50%, -50%);
|
|
2191
|
+
width: min(360px, calc(100vw - 48px));
|
|
2192
|
+
z-index: 20;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
.loading-brand-mark {
|
|
2196
|
+
align-items: baseline;
|
|
2197
|
+
color: var(--text-primary);
|
|
2198
|
+
display: inline-flex;
|
|
2199
|
+
font-family: var(--font-family-mono);
|
|
2200
|
+
font-size: clamp(2.1rem, 7vw, 4rem);
|
|
2201
|
+
font-weight: 900;
|
|
2202
|
+
letter-spacing: -0.08em;
|
|
2203
|
+
line-height: 0.9;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
.loading-brand-word {
|
|
2207
|
+
color: color-mix(in srgb, var(--text-primary) 86%, var(--accent));
|
|
2208
|
+
font-size: 0.48em;
|
|
2209
|
+
letter-spacing: -0.04em;
|
|
2210
|
+
margin-right: 0.18em;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
.loading-brand-chevron { color: var(--accent); }
|
|
2214
|
+
|
|
2215
|
+
.loading-brand-cursor {
|
|
2216
|
+
animation: loading-cursor-blink 0.95s steps(1, end) infinite;
|
|
2217
|
+
color: var(--accent);
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
.loading-brand-status {
|
|
2221
|
+
color: var(--text-secondary);
|
|
2222
|
+
font-size: 0.78rem;
|
|
2223
|
+
font-weight: 800;
|
|
2224
|
+
letter-spacing: 0.1em;
|
|
2225
|
+
text-transform: uppercase;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
.loading-line,
|
|
2229
|
+
.loading-pill,
|
|
2230
|
+
.loading-orb,
|
|
2231
|
+
.loading-logo,
|
|
2232
|
+
.loading-icon,
|
|
2233
|
+
.loading-search,
|
|
2234
|
+
.loading-circle,
|
|
2235
|
+
.loading-card,
|
|
2236
|
+
.loading-panel-block,
|
|
2237
|
+
.loading-dot {
|
|
2238
|
+
animation: loading-pulse 1.35s ease-in-out infinite;
|
|
2239
|
+
background: linear-gradient(90deg, color-mix(in srgb, var(--surface-muted) 76%, transparent), color-mix(in srgb, var(--accent) 16%, var(--surface-elevated)), color-mix(in srgb, var(--surface-muted) 76%, transparent));
|
|
2240
|
+
background-size: 220% 100%;
|
|
2241
|
+
border: 1px solid var(--border);
|
|
2242
|
+
display: block;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
.loading-line {
|
|
2246
|
+
border-radius: 999px;
|
|
2247
|
+
height: 14px;
|
|
2248
|
+
width: 100%;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
.loading-line-wide { width: min(72%, 260px); }
|
|
2252
|
+
.loading-line-short { width: 58%; }
|
|
2253
|
+
.loading-line-hero { height: 34px; width: min(56%, 520px); }
|
|
2254
|
+
.loading-titlebar { height: 10px; margin: 7px auto 0; width: min(42%, 520px); }
|
|
2255
|
+
|
|
2256
|
+
.loading-orb,
|
|
2257
|
+
.loading-logo,
|
|
2258
|
+
.loading-icon,
|
|
2259
|
+
.loading-circle,
|
|
2260
|
+
.loading-dot {
|
|
2261
|
+
border-radius: 999px;
|
|
2262
|
+
flex: 0 0 auto;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
.loading-orb {
|
|
2266
|
+
height: 48px;
|
|
2267
|
+
width: 48px;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
.loading-logo {
|
|
2271
|
+
border-radius: 11px;
|
|
2272
|
+
height: 44px;
|
|
2273
|
+
width: 44px;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
.loading-icon,
|
|
2277
|
+
.loading-circle {
|
|
2278
|
+
height: 34px;
|
|
2279
|
+
width: 34px;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
.loading-dot {
|
|
2283
|
+
height: 8px;
|
|
2284
|
+
width: 8px;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
.loading-pill,
|
|
2288
|
+
.loading-search {
|
|
2289
|
+
border-radius: 999px;
|
|
2290
|
+
height: 40px;
|
|
2291
|
+
width: 100%;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
.loading-sidebar-header,
|
|
2295
|
+
.loading-workspace-header {
|
|
2296
|
+
align-items: center;
|
|
2297
|
+
display: flex;
|
|
2298
|
+
gap: 12px;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
.loading-sidebar-header,
|
|
2302
|
+
.loading-sidebar-footer,
|
|
2303
|
+
.loading-list,
|
|
2304
|
+
.loading-workspace-body {
|
|
2305
|
+
display: grid;
|
|
2306
|
+
gap: 12px;
|
|
2307
|
+
padding: 16px;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
.loading-list {
|
|
2311
|
+
padding-top: 0;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
.loading-sidebar-footer {
|
|
2315
|
+
border-top: 1px solid var(--border);
|
|
2316
|
+
margin-top: auto;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
.loading-workspace-header .loading-search {
|
|
2320
|
+
flex: 1 1 auto;
|
|
2321
|
+
max-width: 560px;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
.loading-workspace-body {
|
|
2325
|
+
align-content: start;
|
|
2326
|
+
gap: 18px;
|
|
2327
|
+
overflow: hidden;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
.loading-card-grid {
|
|
2331
|
+
display: grid;
|
|
2332
|
+
gap: 14px;
|
|
2333
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
.loading-card {
|
|
2337
|
+
border-radius: 18px;
|
|
2338
|
+
height: 128px;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
.loading-panel-block {
|
|
2342
|
+
border-radius: 20px;
|
|
2343
|
+
height: 240px;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
.loading-status-bar .loading-line {
|
|
2347
|
+
height: 10px;
|
|
2348
|
+
width: 96px;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
@keyframes loading-pulse {
|
|
2352
|
+
0% { background-position: 120% 0; opacity: 0.52; }
|
|
2353
|
+
50% { opacity: 0.95; }
|
|
2354
|
+
100% { background-position: -120% 0; opacity: 0.52; }
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
@keyframes loading-cursor-blink {
|
|
2358
|
+
0%, 48% { opacity: 1; }
|
|
2359
|
+
49%, 100% { opacity: 0; }
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2363
|
+
.loading-line,
|
|
2364
|
+
.loading-pill,
|
|
2365
|
+
.loading-orb,
|
|
2366
|
+
.loading-logo,
|
|
2367
|
+
.loading-icon,
|
|
2368
|
+
.loading-search,
|
|
2369
|
+
.loading-circle,
|
|
2370
|
+
.loading-card,
|
|
2371
|
+
.loading-panel-block,
|
|
2372
|
+
.loading-dot,
|
|
2373
|
+
.loading-brand-cursor {
|
|
2374
|
+
animation: none;
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2171
2378
|
.desktop-status-bar {
|
|
2172
2379
|
align-items: center;
|
|
2173
2380
|
background: color-mix(in srgb, var(--surface-elevated) 96%, transparent);
|
package/setup.sh
CHANGED
|
@@ -12,7 +12,7 @@ shopt -s inherit_errexit 2>/dev/null || true
|
|
|
12
12
|
# AI Assistant Server Access Framework Setup Script
|
|
13
13
|
# Helps developers set up the framework for their infrastructure
|
|
14
14
|
#
|
|
15
|
-
# Version: 3.29.
|
|
15
|
+
# Version: 3.29.2
|
|
16
16
|
#
|
|
17
17
|
# Quick Install:
|
|
18
18
|
# npm install -g aidevops && aidevops update (recommended)
|