aidevops 3.29.18 → 3.29.20
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/status-adapter-utils.ts +48 -4
- package/packages/gui-api/src/status-managed-apps.ts +16 -4
- package/packages/gui-desktop/scripts/install-macos-app.sh +15 -1
- package/packages/gui-web/src/App.tsx +5 -8
- package/packages/gui-web/src/AppNavigation.tsx +20 -8
- package/packages/gui-web/src/InventorySurfaces.tsx +65 -8
- package/packages/gui-web/src/app-model.ts +6 -6
- package/packages/gui-web/src/dashboard.ts +1 -1
- package/packages/gui-web/src/navigation-history.ts +49 -0
- package/packages/gui-web/src/styles.css +54 -15
- 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.20
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { accessSync, constants, existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
5
|
export function readJsonObject(pathName: string): { health: "present" | "missing" | "invalid"; value: Record<string, unknown> } {
|
|
@@ -112,12 +112,34 @@ export function firstExistingPathRef(pathRefs: string[]): string | null {
|
|
|
112
112
|
return pathRefs.find((pathRef) => existsSync(expandHome(pathRef))) ?? null;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
export function firstExecutablePathRef(pathRefs: string[]): string | null {
|
|
116
|
+
return pathRefs.find((pathRef) => isExecutable(expandHome(pathRef))) ?? null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function isExecutable(pathName: string): boolean {
|
|
120
|
+
try {
|
|
121
|
+
if (!statSync(pathName).isFile()) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
accessSync(pathName, constants.X_OK);
|
|
125
|
+
return true;
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
115
131
|
export function resolveBinary(binary: string): string | null {
|
|
132
|
+
const candidatePath = binaryPathCandidates(binary).find((pathName) => isExecutable(pathName));
|
|
133
|
+
if (candidatePath !== undefined) {
|
|
134
|
+
return candidatePath;
|
|
135
|
+
}
|
|
136
|
+
|
|
116
137
|
try {
|
|
117
138
|
const output = execFileSync("which", [binary], {
|
|
139
|
+
env: { ...process.env, PATH: expandedPath() },
|
|
118
140
|
encoding: "utf8",
|
|
119
141
|
stdio: ["ignore", "pipe", "ignore"],
|
|
120
|
-
timeout:
|
|
142
|
+
timeout: 350,
|
|
121
143
|
}).trim();
|
|
122
144
|
return output.split("\n").find((line) => line.length > 0) ?? null;
|
|
123
145
|
} catch {
|
|
@@ -125,6 +147,27 @@ export function resolveBinary(binary: string): string | null {
|
|
|
125
147
|
}
|
|
126
148
|
}
|
|
127
149
|
|
|
150
|
+
function binaryPathCandidates(binary: string): string[] {
|
|
151
|
+
return expandedPath().split(":").filter(Boolean).map((dir) => join(dir, binary));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function expandedPath(): string {
|
|
155
|
+
const home = process.env.HOME ?? "";
|
|
156
|
+
const extraPaths = [
|
|
157
|
+
"/opt/homebrew/bin",
|
|
158
|
+
"/usr/local/bin",
|
|
159
|
+
"/usr/bin",
|
|
160
|
+
"/bin",
|
|
161
|
+
"/usr/sbin",
|
|
162
|
+
"/sbin",
|
|
163
|
+
join(home, ".bun/bin"),
|
|
164
|
+
join(home, ".local/bin"),
|
|
165
|
+
join(home, ".cursor/bin"),
|
|
166
|
+
join(home, ".qlty/bin"),
|
|
167
|
+
];
|
|
168
|
+
return [...new Set([...(process.env.PATH ?? "").split(":"), ...extraPaths].filter(Boolean))].join(":");
|
|
169
|
+
}
|
|
170
|
+
|
|
128
171
|
export function readBinaryVersion(binaryPath: string, args: string[]): string {
|
|
129
172
|
if (args.length === 0) {
|
|
130
173
|
return "unknown";
|
|
@@ -132,9 +175,10 @@ export function readBinaryVersion(binaryPath: string, args: string[]): string {
|
|
|
132
175
|
|
|
133
176
|
try {
|
|
134
177
|
const output = execFileSync(binaryPath, args, {
|
|
178
|
+
env: { ...process.env, PATH: expandedPath() },
|
|
135
179
|
encoding: "utf8",
|
|
136
|
-
stdio: ["ignore", "pipe", "
|
|
137
|
-
timeout:
|
|
180
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
181
|
+
timeout: 650,
|
|
138
182
|
}).trim();
|
|
139
183
|
return output.split("\n").find((line) => line.length > 0) ?? "unknown";
|
|
140
184
|
} catch {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import type { GuiAppActionId, GuiManagedAppSummary } from "../../gui-shared/src";
|
|
3
|
-
import { collapseHome, expandHome, firstExistingPathRef, readBinaryVersion, resolveBinary } from "./status-adapter-utils";
|
|
3
|
+
import { collapseHome, expandHome, firstExecutablePathRef, firstExistingPathRef, readBinaryVersion, resolveBinary } from "./status-adapter-utils";
|
|
4
4
|
|
|
5
5
|
type ManagedAppActionCommands = Partial<Record<GuiAppActionId, string>>;
|
|
6
6
|
|
|
@@ -180,7 +180,7 @@ function setupScopeActions(scope: string): ManagedAppActionCommands {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
function managedAppSummary(definition: ManagedAppDefinition, latestVersion: string, installedVersion: string): GuiManagedAppSummary {
|
|
183
|
-
const binaryPath = definition.binary === null ? null : resolveBinary(definition.binary);
|
|
183
|
+
const binaryPath = definition.binary === null ? null : executableInstallPath(definition.install_path_refs) ?? resolveBinary(definition.binary);
|
|
184
184
|
const installPathRef = firstExistingPathRef(definition.install_path_refs) ?? definition.install_path_refs[0] ?? "not found";
|
|
185
185
|
const installedVersionText = readManagedAppVersion(definition, binaryPath, installedVersion);
|
|
186
186
|
const latestVersionText = definition.latest_version_source === "aidevops"
|
|
@@ -231,7 +231,13 @@ function readManagedAppVersion(definition: ManagedAppDefinition, binaryPath: str
|
|
|
231
231
|
return readAppBundleVersion(existingPathRef) ?? "installed";
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
const versionText = readBinaryVersion(binaryPath, definition.version_args);
|
|
235
|
+
return versionText === "unknown" ? "installed" : versionText;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function executableInstallPath(pathRefs: string[]): string | null {
|
|
239
|
+
const executablePathRef = firstExecutablePathRef(pathRefs);
|
|
240
|
+
return executablePathRef === null ? null : expandHome(executablePathRef);
|
|
235
241
|
}
|
|
236
242
|
|
|
237
243
|
function readAppBundleVersion(pathRef: string): string | null {
|
|
@@ -239,5 +245,11 @@ function readAppBundleVersion(pathRef: string): string | null {
|
|
|
239
245
|
return null;
|
|
240
246
|
}
|
|
241
247
|
|
|
242
|
-
|
|
248
|
+
const version = readBinaryVersion("/usr/libexec/PlistBuddy", ["-c", "Print :CFBundleShortVersionString", `${expandHome(pathRef)}/Contents/Info.plist`]);
|
|
249
|
+
if (version !== "unknown") {
|
|
250
|
+
return version;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const bundleVersion = readBinaryVersion("/usr/libexec/PlistBuddy", ["-c", "Print :CFBundleVersion", `${expandHome(pathRef)}/Contents/Info.plist`]);
|
|
254
|
+
return bundleVersion === "unknown" ? null : bundleVersion;
|
|
243
255
|
}
|
|
@@ -338,7 +338,7 @@ final class DraggableTitlebarView: NSView {
|
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
-
final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKScriptMessageHandler, NSMenuItemValidation, NSWindowDelegate {
|
|
341
|
+
final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler, NSMenuItemValidation, NSWindowDelegate {
|
|
342
342
|
private var window: NSWindow!
|
|
343
343
|
private var aboutWindow: NSWindow?
|
|
344
344
|
private var titlebarToggleButton: NSButton?
|
|
@@ -416,6 +416,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
|
|
|
416
416
|
decisionHandler(.allow)
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
+
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
|
420
|
+
guard let url = navigationAction.request.url else {
|
|
421
|
+
return nil
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if url.scheme == "http" || url.scheme == "https" {
|
|
425
|
+
NSWorkspace.shared.open(url)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return nil
|
|
429
|
+
}
|
|
430
|
+
|
|
419
431
|
private func isLocalAppURL(_ url: URL) -> Bool {
|
|
420
432
|
let host = url.host ?? ""
|
|
421
433
|
return (host == "127.0.0.1" || host == "localhost") && url.port == Int(webPort)
|
|
@@ -677,11 +689,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
|
|
|
677
689
|
private func configureWindow() {
|
|
678
690
|
let configuration = WKWebViewConfiguration()
|
|
679
691
|
configuration.defaultWebpagePreferences.allowsContentJavaScript = true
|
|
692
|
+
configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
|
680
693
|
let userContentController = WKUserContentController()
|
|
681
694
|
userContentController.add(self, name: "accentHue")
|
|
682
695
|
configuration.userContentController = userContentController
|
|
683
696
|
webView = WKWebView(frame: .zero, configuration: configuration)
|
|
684
697
|
webView.navigationDelegate = self
|
|
698
|
+
webView.uiDelegate = self
|
|
685
699
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
|
686
700
|
|
|
687
701
|
let contentContainer = NSView()
|
|
@@ -4,13 +4,9 @@ import { MachineRail, Sidebar } from "./AppNavigation";
|
|
|
4
4
|
import { Workspace } from "./AppWorkspace";
|
|
5
5
|
import type { ConversationMode, FontPreference, FontSizePreference, ShellMode, SurfaceId, ThemePreference } from "./app-model";
|
|
6
6
|
import { DEFAULT_ACCENT_HUE, DEFAULT_FONT, DEFAULT_FONT_SIZE, fileRootBySurface, findSurface, findSurfaceSectionLabel, fontFamilyForPreference, fontSizeForPreference, getSystemTheme, isFontPreference, isFontSizePreference } from "./app-model";
|
|
7
|
+
import { type NavigationHistory, nextHistoryIndex, useNavigationHistoryKeyboard } from "./navigation-history";
|
|
7
8
|
import { fetchStatus, mockedStatus } from "./status-client";
|
|
8
9
|
|
|
9
|
-
interface NavigationHistory {
|
|
10
|
-
entries: SurfaceId[];
|
|
11
|
-
index: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
10
|
interface WebKitBridgeWindow extends Window {
|
|
15
11
|
webkit?: {
|
|
16
12
|
messageHandlers?: {
|
|
@@ -37,7 +33,7 @@ export const appearanceStorageKeys = {
|
|
|
37
33
|
} as const;
|
|
38
34
|
|
|
39
35
|
const defaultSidebarWidth = 302;
|
|
40
|
-
const minSidebarWidth =
|
|
36
|
+
const minSidebarWidth = 300;
|
|
41
37
|
const maxSidebarWidth = 520;
|
|
42
38
|
export const loadingSkeletonPanelLabels = ["machine rail", "sidebar", "workspace", "status bar"] as const;
|
|
43
39
|
export const loadingBrandGlyph = ">_";
|
|
@@ -93,6 +89,7 @@ export function App(): ReactElement {
|
|
|
93
89
|
const canGoBack = navigation.index > 0;
|
|
94
90
|
const canGoForward = navigation.index < navigation.entries.length - 1;
|
|
95
91
|
const fileRoot = fileRootBySurface[activeSurface];
|
|
92
|
+
useNavigationHistoryKeyboard(setNavigation);
|
|
96
93
|
|
|
97
94
|
const setActiveSurface = (surface: SurfaceId) => {
|
|
98
95
|
setNavigation((current) => {
|
|
@@ -108,11 +105,11 @@ export function App(): ReactElement {
|
|
|
108
105
|
};
|
|
109
106
|
|
|
110
107
|
const goBack = () => {
|
|
111
|
-
setNavigation((current) => ({ ...current, index:
|
|
108
|
+
setNavigation((current) => ({ ...current, index: nextHistoryIndex(current, -1) }));
|
|
112
109
|
};
|
|
113
110
|
|
|
114
111
|
const goForward = () => {
|
|
115
|
-
setNavigation((current) => ({ ...current, index:
|
|
112
|
+
setNavigation((current) => ({ ...current, index: nextHistoryIndex(current, 1) }));
|
|
116
113
|
};
|
|
117
114
|
|
|
118
115
|
useEffect(() => {
|
|
@@ -145,7 +145,7 @@ export function Sidebar({ activeSurface, accentHue, conversationMode, fontPrefer
|
|
|
145
145
|
|
|
146
146
|
return (
|
|
147
147
|
<aside className="app-sidebar" aria-label={text.navigationLabel}>
|
|
148
|
-
<SidebarHeader setShellMode={setShellMode} shellMode={shellMode} />
|
|
148
|
+
<SidebarHeader setActiveSurface={setActiveSurface} setShellMode={setShellMode} shellMode={shellMode} />
|
|
149
149
|
<nav className="sidebar-content">
|
|
150
150
|
{shellMode === "devices" ? <>
|
|
151
151
|
<SidebarModeTabs mode={sidebarMode} setMode={(mode) => setSidebarMode(mode as SidebarMode)} />
|
|
@@ -357,17 +357,18 @@ function SidebarItem({ activeSurface, item, recordCount, setActiveSurface, showC
|
|
|
357
357
|
);
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
function SidebarHeader({ setShellMode, shellMode }: {
|
|
360
|
+
function SidebarHeader({ setActiveSurface, setShellMode, shellMode }: {
|
|
361
|
+
setActiveSurface: (surface: SurfaceId) => void;
|
|
361
362
|
setShellMode: (mode: ShellMode) => void;
|
|
362
363
|
shellMode: ShellMode;
|
|
363
364
|
}) {
|
|
364
365
|
return (
|
|
365
366
|
<header className="sidebar-header">
|
|
366
367
|
<div className="sidebar-titlebar">
|
|
367
|
-
<
|
|
368
|
+
<button className="brand-lockup" onClick={() => setActiveSurface(dashboardNavItem.id)} title="Return to dashboard" type="button">
|
|
368
369
|
<span className="terminal-mark" aria-hidden="true">›_</span>
|
|
369
370
|
<strong>{text.aidevops}</strong>
|
|
370
|
-
</
|
|
371
|
+
</button>
|
|
371
372
|
<IconSwitch
|
|
372
373
|
ariaLabel="Navigation scope"
|
|
373
374
|
options={[
|
|
@@ -501,14 +502,25 @@ function SidebarFooter({ accentHue, fontPreference, fontSizePreference, setAccen
|
|
|
501
502
|
id="font-size-slider"
|
|
502
503
|
max={fontSizeOptions.length - 1}
|
|
503
504
|
min="0"
|
|
504
|
-
onChange={(event) => setFontSizePreference(fontSizeOptions[Number.parseInt(event.currentTarget.value, 10)]?.value ?? "
|
|
505
|
+
onChange={(event) => setFontSizePreference(fontSizeOptions[Number.parseInt(event.currentTarget.value, 10)]?.value ?? "xs")}
|
|
505
506
|
step="1"
|
|
506
507
|
type="range"
|
|
507
508
|
value={fontSizeIndex}
|
|
508
509
|
/>
|
|
509
|
-
<
|
|
510
|
-
|
|
511
|
-
|
|
510
|
+
<fieldset className="range-labels">
|
|
511
|
+
<legend className="sr-only">Font size shortcuts</legend>
|
|
512
|
+
{fontSizeOptions.map((option) => (
|
|
513
|
+
<button
|
|
514
|
+
aria-pressed={option.value === fontSizePreference}
|
|
515
|
+
className={option.value === fontSizePreference ? "active" : ""}
|
|
516
|
+
key={option.value}
|
|
517
|
+
onClick={() => setFontSizePreference(option.value)}
|
|
518
|
+
type="button"
|
|
519
|
+
>
|
|
520
|
+
{option.label}
|
|
521
|
+
</button>
|
|
522
|
+
))}
|
|
523
|
+
</fieldset>
|
|
512
524
|
</div>
|
|
513
525
|
<div className="font-control">
|
|
514
526
|
<span id="appearance-font-selector-label">{text.font}</span>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GuiAppActionId, GuiAppActionJobSummary, GuiManagedAppSummary, GuiResponseEnvelope, GuiStatusData } from "@aidevops/gui-shared";
|
|
2
|
-
import { type Dispatch, type MouseEvent as ReactMouseEvent, type ReactElement, type ReactNode, type SetStateAction, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { type CSSProperties, type Dispatch, type FocusEvent as ReactFocusEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactElement, type ReactNode, type SetStateAction, useEffect, useRef, useState } from "react";
|
|
3
3
|
import type { IconType } from "react-icons";
|
|
4
4
|
import { FaApple, FaLinux, FaWindows } from "react-icons/fa";
|
|
5
5
|
import { FiChevronDown, FiCode, FiDownload, FiExternalLink, FiGlobe, FiMonitor, FiRefreshCw, FiRepeat, FiTerminal, FiTrash2, FiX } from "react-icons/fi";
|
|
@@ -24,18 +24,20 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
|
|
|
24
24
|
const [expandedAppId, setExpandedAppId] = useState<string | null>(() => sortedManagedApps(status.managed_apps).find((app) => managedCategoryForApp(app) === "core")?.id ?? null);
|
|
25
25
|
const [jobs, setJobs] = useState<Record<string, GuiAppActionJobSummary>>({});
|
|
26
26
|
const [dismissedJobIds, setDismissedJobIds] = useState<Set<string>>(() => new Set());
|
|
27
|
+
const [tooltip, setTooltip] = useState<AppTooltipState | null>(null);
|
|
27
28
|
const [policyJobs, setPolicyJobs] = useState<Record<string, GuiAppActionJobSummary[]>>({});
|
|
28
29
|
const [policyToggles, setPolicyToggles] = useState<ManagedPolicyToggleState>(() => readManagedPolicyToggles());
|
|
29
30
|
const selectedJob = selectedJobId === null ? null : jobs[selectedJobId] ?? null;
|
|
30
31
|
const managedApps = sortedManagedApps(status.managed_apps).map((app) => applyManagedPolicyToggles(app, policyToggles[app.id]));
|
|
31
32
|
const visibleManagedApps = managedApps.filter((app) => managedCategoryForApp(app) === managedCategory);
|
|
33
|
+
const firstVisibleManagedAppId = visibleManagedApps[0]?.id ?? null;
|
|
32
34
|
const visibleRecommendedApps = recommendedApps.filter((app) => recommendedAppMatchesFilters(app, recommendedPlatform, recommendedOs));
|
|
33
35
|
const selectRecommendedPlatform = (value: RecommendedPlatformFilterId) => setRecommendedPlatform((current) => nextRecommendedFilterValue(current, value, "all"));
|
|
34
36
|
const selectRecommendedOs = (value: RecommendedOsId) => setRecommendedOs((current) => nextRecommendedFilterValue(current, value, "all"));
|
|
35
37
|
|
|
36
38
|
useEffect(() => {
|
|
37
|
-
setExpandedAppId(
|
|
38
|
-
}, [
|
|
39
|
+
setExpandedAppId(firstVisibleManagedAppId);
|
|
40
|
+
}, [firstVisibleManagedAppId]);
|
|
39
41
|
|
|
40
42
|
const runningJobIdsKey = Object.values(jobs).filter((job) => job.status === "running").map((job) => job.id).sort().join("|");
|
|
41
43
|
|
|
@@ -57,7 +59,7 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
|
|
|
57
59
|
}, [runningJobIdsKey]);
|
|
58
60
|
|
|
59
61
|
return (
|
|
60
|
-
<section className="apps-surface" aria-label={text.apps}>
|
|
62
|
+
<section className="apps-surface" aria-label={text.apps} onBlur={() => setTooltip(null)} onFocus={showFocusedAppTooltip(setTooltip)} onPointerLeave={() => setTooltip(null)} onPointerMove={showPointedAppTooltip(setTooltip)}>
|
|
61
63
|
<div className="section-heading">
|
|
62
64
|
<p className="eyebrow">{text.infrastructure}</p>
|
|
63
65
|
<h2>{text.apps}</h2>
|
|
@@ -103,6 +105,7 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
|
|
|
103
105
|
</div>
|
|
104
106
|
<p className="empty-state compact-notice">Install/update actions run as allowlisted background jobs and stream inside each app panel. xterm.js with a node-pty bridge is the right next step for full TUI apps such as OpenCode; this view starts with non-interactive command logs.</p>
|
|
105
107
|
</> : <RecommendedAppsSurface apps={visibleRecommendedApps} os={recommendedOs} platform={recommendedPlatform} setOs={selectRecommendedOs} setPlatform={selectRecommendedPlatform} />}
|
|
108
|
+
{tooltip === null ? null : <AppTooltip tooltip={tooltip} />}
|
|
106
109
|
</section>
|
|
107
110
|
);
|
|
108
111
|
}
|
|
@@ -151,7 +154,8 @@ function ManagedAppPanel({ app, dismissedJobIds, expanded, job, onDismissJob, on
|
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
function SummaryChip({ label, value }: { label: string; value: string }): ReactElement {
|
|
154
|
-
|
|
157
|
+
const tooltip = `${label}: ${value}`;
|
|
158
|
+
return <span className="managed-summary-chip" data-tooltip={tooltip} title={tooltip}><small>{label}</small><span>{value}</span></span>;
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
function OriginLink({ href, label }: { href: string; label: string }): ReactElement {
|
|
@@ -159,7 +163,7 @@ function OriginLink({ href, label }: { href: string; label: string }): ReactElem
|
|
|
159
163
|
return <span className="origin-missing">{label}: source pending</span>;
|
|
160
164
|
}
|
|
161
165
|
|
|
162
|
-
return <a href={href} onClick={(event) => openExternalLink(event, href)} rel="noreferrer" target="_blank" title={href}>{label} <FiExternalLink aria-hidden="true" /></a>;
|
|
166
|
+
return <a data-tooltip={href} href={href} onClick={(event) => openExternalLink(event, href)} rel="noreferrer" target="_blank" title={href}>{label} <FiExternalLink aria-hidden="true" /></a>;
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
function ToggleSwitch({ checked, disabled = false, label, onChange }: { checked: boolean; disabled?: boolean; label: string; onChange: (checked: boolean) => void }): ReactElement {
|
|
@@ -218,7 +222,7 @@ function AppActionTerminal({ job, onDismiss }: { job: GuiAppActionJobSummary; on
|
|
|
218
222
|
if (outputRef.current !== null) {
|
|
219
223
|
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
|
220
224
|
}
|
|
221
|
-
}, [job.output]);
|
|
225
|
+
}, [job.output.length]);
|
|
222
226
|
|
|
223
227
|
return (
|
|
224
228
|
<section className="app-terminal" id={`app-job-${job.id}`} aria-label="App action terminal output">
|
|
@@ -594,17 +598,70 @@ function writeManagedPolicyToggles(policy: ManagedPolicyToggleState): void {
|
|
|
594
598
|
window.localStorage.setItem("aidevops-gui-managed-app-policy", JSON.stringify(policy));
|
|
595
599
|
}
|
|
596
600
|
|
|
601
|
+
interface AppTooltipState {
|
|
602
|
+
text: string;
|
|
603
|
+
x: number;
|
|
604
|
+
y: number;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function showPointedAppTooltip(setTooltip: Dispatch<SetStateAction<AppTooltipState | null>>) {
|
|
608
|
+
return (event: ReactPointerEvent<HTMLElement>) => {
|
|
609
|
+
const target = tooltipTarget(event.target);
|
|
610
|
+
setTooltip(target === null ? null : tooltipForElement(target));
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function showFocusedAppTooltip(setTooltip: Dispatch<SetStateAction<AppTooltipState | null>>) {
|
|
615
|
+
return (event: ReactFocusEvent<HTMLElement>) => {
|
|
616
|
+
const target = tooltipTarget(event.target);
|
|
617
|
+
if (target !== null) {
|
|
618
|
+
setTooltip(tooltipForElement(target));
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function tooltipTarget(target: EventTarget): HTMLElement | null {
|
|
624
|
+
return target instanceof HTMLElement ? target.closest<HTMLElement>("[data-tooltip]") : null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function tooltipForElement(element: HTMLElement): AppTooltipState | null {
|
|
628
|
+
const textValue = element.dataset.tooltip;
|
|
629
|
+
if (textValue === undefined || textValue.length === 0) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const rect = element.getBoundingClientRect();
|
|
634
|
+
return { text: textValue, x: rect.left + (rect.width / 2), y: rect.top };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function AppTooltip({ tooltip }: { tooltip: AppTooltipState }): ReactElement {
|
|
638
|
+
const style = {
|
|
639
|
+
"--tooltip-x": `${tooltip.x}px`,
|
|
640
|
+
"--tooltip-y": `${tooltip.y}px`,
|
|
641
|
+
} as CSSProperties;
|
|
642
|
+
|
|
643
|
+
return <div className="app-global-tooltip" role="tooltip" style={style}>{tooltip.text}</div>;
|
|
644
|
+
}
|
|
645
|
+
|
|
597
646
|
function openExternalLink(event: ReactMouseEvent<HTMLAnchorElement>, href: string): void {
|
|
598
647
|
event.stopPropagation();
|
|
599
648
|
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
600
649
|
return;
|
|
601
650
|
}
|
|
602
651
|
|
|
652
|
+
event.preventDefault();
|
|
653
|
+
if (document.documentElement.dataset.desktopShell === "macos") {
|
|
654
|
+
window.location.assign(href);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
603
658
|
const opened = window.open(href, "_blank", "noopener,noreferrer");
|
|
604
659
|
if (opened !== null) {
|
|
605
|
-
event.preventDefault();
|
|
606
660
|
opened.opener = null;
|
|
661
|
+
return;
|
|
607
662
|
}
|
|
663
|
+
|
|
664
|
+
window.location.assign(href);
|
|
608
665
|
}
|
|
609
666
|
|
|
610
667
|
export function InstallationSurface(): ReactElement {
|
|
@@ -303,15 +303,15 @@ export const text = {
|
|
|
303
303
|
} as const;
|
|
304
304
|
|
|
305
305
|
export const DEFAULT_ACCENT_HUE = 123;
|
|
306
|
-
export const DEFAULT_FONT: FontPreference = "
|
|
307
|
-
export const DEFAULT_FONT_SIZE: FontSizePreference = "
|
|
306
|
+
export const DEFAULT_FONT: FontPreference = "Inter";
|
|
307
|
+
export const DEFAULT_FONT_SIZE: FontSizePreference = "xs";
|
|
308
308
|
|
|
309
309
|
export const fontOptions: FontOption[] = [
|
|
310
310
|
{ value: "IBM Plex Mono", label: "IBM Plex Mono", fontFamily: '"IBM Plex Mono", Menlo, Monaco, Consolas, monospace' },
|
|
311
311
|
{ value: "IBM Plex Sans", label: "IBM Plex Sans", fontFamily: '"IBM Plex Sans", Inter, system-ui, sans-serif' },
|
|
312
312
|
{ value: "IBM Plex Serif", label: "IBM Plex Serif", fontFamily: '"IBM Plex Serif", Georgia, serif' },
|
|
313
|
-
{ value: "Inter", label: "Inter", fontFamily: 'Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' },
|
|
314
|
-
{ value: "Menlo", label: "Menlo
|
|
313
|
+
{ value: "Inter", label: "Inter (default)", fontFamily: 'Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' },
|
|
314
|
+
{ value: "Menlo", label: "Menlo", fontFamily: 'Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' },
|
|
315
315
|
{ value: "Playpen Sans", label: "Playpen Sans", fontFamily: '"Playpen Sans", "Comic Sans MS", Inter, system-ui, sans-serif' },
|
|
316
316
|
{ value: "Poppins", label: "Poppins", fontFamily: 'Poppins, Inter, system-ui, sans-serif' },
|
|
317
317
|
{ value: "Source Sans", label: "Source Sans", fontFamily: '"Source Sans 3", "Source Sans Pro", "Source Sans", Inter, system-ui, sans-serif' },
|
|
@@ -543,11 +543,11 @@ export function getSystemTheme(): "light" | "dark" {
|
|
|
543
543
|
}
|
|
544
544
|
|
|
545
545
|
export function fontFamilyForPreference(font: FontPreference): string {
|
|
546
|
-
return fontOptions.find((option) => option.value === font)?.fontFamily ?? fontOptions.find((option) => option.value === DEFAULT_FONT)?.fontFamily ?? "
|
|
546
|
+
return fontOptions.find((option) => option.value === font)?.fontFamily ?? fontOptions.find((option) => option.value === DEFAULT_FONT)?.fontFamily ?? "Inter, system-ui, sans-serif";
|
|
547
547
|
}
|
|
548
548
|
|
|
549
549
|
export function fontSizeForPreference(size: FontSizePreference): string {
|
|
550
|
-
return fontSizeOptions.find((option) => option.value === size)?.cssSize ?? fontSizeOptions.find((option) => option.value === DEFAULT_FONT_SIZE)?.cssSize ?? "
|
|
550
|
+
return fontSizeOptions.find((option) => option.value === size)?.cssSize ?? fontSizeOptions.find((option) => option.value === DEFAULT_FONT_SIZE)?.cssSize ?? "14px";
|
|
551
551
|
}
|
|
552
552
|
|
|
553
553
|
export function isFontPreference(value: string | null): value is FontPreference {
|
|
@@ -55,7 +55,7 @@ export function renderDashboardHtml(status: GuiResponseEnvelope<GuiStatusData>):
|
|
|
55
55
|
`<h1>aidevops app interface</h1>`,
|
|
56
56
|
`<p>Made for creators.</p>`,
|
|
57
57
|
`<p>AI-assisted development workflows, code quality, and deployment automation.</p>`,
|
|
58
|
-
`<p>Theme follows system preferences with light and dark overrides. Sidebar modes: DevOps and Comms. Appearance controls can be hidden or shown, and include editable Hue, icon Reset, Show borders toggle, Show counts toggle, Font size choices xs, s, m, lg, xl, and Font options: IBM Plex Mono, IBM Plex Sans, IBM Plex Serif, Inter
|
|
58
|
+
`<p>Theme follows system preferences with light and dark overrides. Sidebar modes: DevOps and Comms. Appearance controls can be hidden or shown, and include editable Hue, icon Reset, Show borders toggle, Show counts toggle, Font size choices xs, s, m, lg, xl, and Font options: IBM Plex Mono, IBM Plex Sans, IBM Plex Serif, Inter (default), Menlo, Playpen Sans, Poppins, Source Sans, Source Serif, Tilt Neon, Ubuntu Mono. The workspace header includes a command palette, reserved signposts/help slot immediately left of notifications, notifications menu, AI Assistant panel, and profile menu with Settings, Theme, Language, Community, Admin, and Logout entries. Notifications reuse the OpenCode toast status cache and GUI readiness data. A desktop status bar shows local readiness, repo totals, secret reference count, provider accounts, and update state.</p>`,
|
|
59
59
|
`<p>Secret references stay read-only and redacted.</p>`,
|
|
60
60
|
`<p>Vault surfaces show padlock indicators. Encrypted by aidevops Vault; contents visible only when unlocked through app or authorised vault commands.</p>`,
|
|
61
61
|
`<p>${escapeHtml(status.data.update.message)}</p>`,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Dispatch, type SetStateAction, useEffect } from "react";
|
|
2
|
+
import type { SurfaceId } from "./app-model";
|
|
3
|
+
|
|
4
|
+
export interface NavigationHistory {
|
|
5
|
+
entries: SurfaceId[];
|
|
6
|
+
index: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function nextHistoryIndex(current: NavigationHistory, direction: -1 | 1): number {
|
|
10
|
+
return Math.min(Math.max(0, current.entries.length - 1), Math.max(0, current.index + direction));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useNavigationHistoryKeyboard(setNavigation: Dispatch<SetStateAction<NavigationHistory>>): void {
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const navigateHistoryWithKeyboard = (event: globalThis.KeyboardEvent) => {
|
|
16
|
+
if (shouldIgnoreHistoryShortcut(event)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const direction = historyShortcutDirection(event.key);
|
|
21
|
+
if (direction === null) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
setNavigation((current) => ({ ...current, index: nextHistoryIndex(current, direction) }));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
window.addEventListener("keydown", navigateHistoryWithKeyboard);
|
|
30
|
+
|
|
31
|
+
return () => window.removeEventListener("keydown", navigateHistoryWithKeyboard);
|
|
32
|
+
}, [setNavigation]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function shouldIgnoreHistoryShortcut(event: globalThis.KeyboardEvent): boolean {
|
|
36
|
+
return !(event.metaKey || event.ctrlKey) || isEditableKeyboardTarget(event.target) || isEditableKeyboardTarget(document.activeElement);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function historyShortcutDirection(key: string): -1 | 1 | null {
|
|
40
|
+
return key === "[" ? -1 : key === "]" ? 1 : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isEditableKeyboardTarget(target: EventTarget | null): boolean {
|
|
44
|
+
if (typeof HTMLElement === "undefined" || !(target instanceof HTMLElement)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return target.isContentEditable || target.closest("input, textarea, select, [contenteditable], [role='textbox']") !== null;
|
|
49
|
+
}
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
--border-hover: color-mix(in srgb, rgb(0 0 0 / 24%) 64%, rgb(255 255 255 / 48%));
|
|
11
11
|
--code-bg: hsl(var(--accent-hue) 55% 91%);
|
|
12
12
|
--desktop-titlebar-height: 0px;
|
|
13
|
-
--font-family-app:
|
|
14
|
-
--font-size-app:
|
|
13
|
+
--font-family-app: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
14
|
+
--font-size-app: 14px;
|
|
15
15
|
--glass-panel-shadow: inset 0 1px 0 rgb(255 255 255 / 58%), inset 0 -1px 0 rgb(0 0 0 / 3%), var(--shadow-soft);
|
|
16
16
|
--notification-error-bg: rgb(207 34 46 / 10%);
|
|
17
17
|
--notification-error-fg: #cf222e;
|
|
@@ -119,7 +119,7 @@ code {
|
|
|
119
119
|
.app-shell {
|
|
120
120
|
display: grid;
|
|
121
121
|
column-gap: 8px;
|
|
122
|
-
grid-template-columns: 72px var(--sidebar-width, 302px)
|
|
122
|
+
grid-template-columns: 72px var(--sidebar-width, 302px) 6px minmax(0, 1fr);
|
|
123
123
|
grid-template-rows: minmax(0, 1fr) auto;
|
|
124
124
|
height: 100vh;
|
|
125
125
|
padding: calc(var(--shell-gutter) + var(--desktop-titlebar-height)) var(--shell-gutter) var(--shell-gutter);
|
|
@@ -127,7 +127,7 @@ code {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
.app-shell.machine-rail-collapsed {
|
|
130
|
-
grid-template-columns: var(--sidebar-width, 302px)
|
|
130
|
+
grid-template-columns: var(--sidebar-width, 302px) 6px minmax(0, 1fr);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
.desktop-titlebar-tagline {
|
|
@@ -296,10 +296,19 @@ code {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
.brand-lockup {
|
|
299
|
+
background: transparent;
|
|
300
|
+
border: 0;
|
|
301
|
+
color: var(--text-primary);
|
|
299
302
|
gap: 12px;
|
|
300
303
|
padding: 2px 4px;
|
|
301
304
|
}
|
|
302
305
|
|
|
306
|
+
.brand-lockup:hover,
|
|
307
|
+
.brand-lockup:focus-visible {
|
|
308
|
+
color: var(--accent);
|
|
309
|
+
outline: none;
|
|
310
|
+
}
|
|
311
|
+
|
|
303
312
|
.sidebar-titlebar {
|
|
304
313
|
gap: 10px;
|
|
305
314
|
justify-content: space-between;
|
|
@@ -819,28 +828,32 @@ code {
|
|
|
819
828
|
}
|
|
820
829
|
|
|
821
830
|
.range-labels {
|
|
831
|
+
border: 0;
|
|
822
832
|
color: var(--text-muted);
|
|
823
833
|
display: grid;
|
|
824
834
|
font-size: 0.7rem;
|
|
825
835
|
font-weight: 900;
|
|
826
836
|
grid-template-columns: repeat(5, 1fr);
|
|
837
|
+
margin: 0;
|
|
838
|
+
padding: 0;
|
|
827
839
|
text-transform: uppercase;
|
|
828
840
|
}
|
|
829
841
|
|
|
830
|
-
.range-labels
|
|
842
|
+
.range-labels button {
|
|
843
|
+
background: transparent;
|
|
844
|
+
border: 0;
|
|
845
|
+
border-radius: 8px;
|
|
846
|
+
color: inherit;
|
|
847
|
+
font: inherit;
|
|
848
|
+
padding: 3px 0;
|
|
831
849
|
text-align: center;
|
|
832
850
|
}
|
|
833
851
|
|
|
834
|
-
.range-labels
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
.range-labels span:last-child {
|
|
839
|
-
text-align: right;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
.range-labels .active {
|
|
852
|
+
.range-labels button:hover,
|
|
853
|
+
.range-labels button:focus-visible,
|
|
854
|
+
.range-labels button.active {
|
|
843
855
|
color: var(--accent);
|
|
856
|
+
outline: none;
|
|
844
857
|
}
|
|
845
858
|
|
|
846
859
|
.font-control select {
|
|
@@ -2206,7 +2219,7 @@ code {
|
|
|
2206
2219
|
align-items: center;
|
|
2207
2220
|
color: var(--text-secondary);
|
|
2208
2221
|
display: inline-flex;
|
|
2209
|
-
font-size:
|
|
2222
|
+
font-size: 1em;
|
|
2210
2223
|
font-weight: 800;
|
|
2211
2224
|
gap: 4px;
|
|
2212
2225
|
max-width: 100%;
|
|
@@ -2545,6 +2558,32 @@ button.managed-toggle:focus-visible {
|
|
|
2545
2558
|
transform: translate(-50%, 0);
|
|
2546
2559
|
}
|
|
2547
2560
|
|
|
2561
|
+
.apps-surface [data-tooltip]::after {
|
|
2562
|
+
content: none;
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
.app-global-tooltip {
|
|
2566
|
+
background: color-mix(in srgb, var(--surface-elevated) 94%, black);
|
|
2567
|
+
border: 1px solid color-mix(in srgb, var(--accent) 36%, var(--border));
|
|
2568
|
+
border-radius: 10px;
|
|
2569
|
+
box-shadow: 0 18px 46px rgb(0 0 0 / 38%);
|
|
2570
|
+
color: var(--text-primary);
|
|
2571
|
+
font-size: 0.72rem;
|
|
2572
|
+
font-weight: 700;
|
|
2573
|
+
left: clamp(12px, var(--tooltip-x), calc(100vw - 12px));
|
|
2574
|
+
line-height: 1.35;
|
|
2575
|
+
max-width: min(440px, 72vw);
|
|
2576
|
+
overflow-wrap: anywhere;
|
|
2577
|
+
padding: 8px 10px;
|
|
2578
|
+
pointer-events: none;
|
|
2579
|
+
position: fixed;
|
|
2580
|
+
text-align: left;
|
|
2581
|
+
top: var(--tooltip-y);
|
|
2582
|
+
transform: translate(-50%, calc(-100% - 10px));
|
|
2583
|
+
width: max-content;
|
|
2584
|
+
z-index: 10000;
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2548
2587
|
.data-row,
|
|
2549
2588
|
.editable-row {
|
|
2550
2589
|
display: grid;
|
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.20
|
|
16
16
|
#
|
|
17
17
|
# Quick Install:
|
|
18
18
|
# npm install -g aidevops && aidevops update (recommended)
|