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 CHANGED
@@ -58,7 +58,7 @@ The result: an AI operations platform that manages projects across every busines
58
58
  [![Copyright](https://img.shields.io/badge/Copyright-Marcus%20Quinn%202025--2026-blue.svg)](https://github.com/marcusquinn)
59
59
 
60
60
  <!-- Release & Version Info -->
61
- [![Version](https://img.shields.io/badge/Version-3.29.0-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
61
+ [![Version](https://img.shields.io/badge/Version-3.29.2-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
62
62
  [![npm version](https://img.shields.io/npm/v/aidevops)](https://www.npmjs.com/package/aidevops)
63
63
  [![Homebrew](https://img.shields.io/badge/homebrew-marcusquinn%2Ftap-orange)](https://github.com/marcusquinn/homebrew-tap)
64
64
  [![GitHub repository](https://img.shields.io/badge/github-repository-181717.svg?logo=github)](https://github.com/marcusquinn/aidevops)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.29.0
1
+ 3.29.2
package/aidevops.sh CHANGED
@@ -5,7 +5,7 @@
5
5
  # AI DevOps Framework CLI
6
6
  # Usage: aidevops <command> [options]
7
7
  #
8
- # Version: 3.29.0
8
+ # Version: 3.29.2
9
9
 
10
10
  set -euo pipefail
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.29.0",
3
+ "version": "3.29.2",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -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: 500,
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: 1_000,
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 ! url_ready "\${health_url}"; then
227
- return 0
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}" "http://127.0.0.1:\${API_PORT}/api/status"
301
- replace_stale_gui_on_port "\${WEB_PORT}" "http://127.0.0.1:\${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 "http://127.0.0.1:\${API_PORT}/api/status" || ! url_ready "http://127.0.0.1:\${WEB_PORT}/"; then
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 "http://127.0.0.1:\${API_PORT}/api/status"; then
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 "http://127.0.0.1:\${WEB_PORT}/"; then
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 "http://127.0.0.1:\${API_PORT}/api/status" 20 1; then
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 "http://127.0.0.1:\${WEB_PORT}/" 30 1; then
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
- loadAppURL()
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>&gt;</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
- body { align-items: center; background: #0a0a0a; color: #f6f7f2; display: flex; font: 15px -apple-system, BlinkMacSystemFont, sans-serif; height: 100vh; justify-content: center; margin: 0; }
912
- main { background: #11130f; border: 1px solid #2c3324; border-radius: 18px; max-width: 560px; padding: 28px; }
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><main><h1>\(escapedTitle)</h1><p>\(escapedDetail)</p></main></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">&gt;</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.0
15
+ # Version: 3.29.2
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)