aidevops 3.29.21 → 3.29.23
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 +9 -5
- package/packages/gui-desktop/scripts/install-macos-app.sh +31 -8
- package/packages/gui-shared/src/notifications.ts +3 -2
- package/packages/gui-web/src/App.tsx +37 -8
- package/packages/gui-web/src/AppActionTerminal.tsx +81 -0
- package/packages/gui-web/src/AppNavigation.tsx +11 -189
- package/packages/gui-web/src/AppWorkspace.tsx +21 -29
- package/packages/gui-web/src/AppearanceControls.tsx +244 -0
- package/packages/gui-web/src/InventoryEditorSurfaces.tsx +99 -0
- package/packages/gui-web/src/InventorySurfaces.tsx +39 -500
- package/packages/gui-web/src/ManagedAppPanel.tsx +142 -0
- package/packages/gui-web/src/RecommendedAppsSurface.tsx +199 -0
- package/packages/gui-web/src/app-model.ts +18 -0
- package/packages/gui-web/src/dashboard.ts +1 -1
- package/packages/gui-web/src/external-links.ts +60 -0
- package/packages/gui-web/src/styles.css +135 -3
- package/packages/gui-web/src/workspace-header-state.ts +107 -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.23
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -152,7 +152,6 @@ function binaryPathCandidates(binary: string): string[] {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
function expandedPath(): string {
|
|
155
|
-
const home = process.env.HOME ?? "";
|
|
156
155
|
const extraPaths = [
|
|
157
156
|
"/opt/homebrew/bin",
|
|
158
157
|
"/usr/local/bin",
|
|
@@ -160,11 +159,16 @@ function expandedPath(): string {
|
|
|
160
159
|
"/bin",
|
|
161
160
|
"/usr/sbin",
|
|
162
161
|
"/sbin",
|
|
163
|
-
join(home, ".bun/bin"),
|
|
164
|
-
join(home, ".local/bin"),
|
|
165
|
-
join(home, ".cursor/bin"),
|
|
166
|
-
join(home, ".qlty/bin"),
|
|
167
162
|
];
|
|
163
|
+
const home = process.env.HOME;
|
|
164
|
+
if (home) {
|
|
165
|
+
extraPaths.push(
|
|
166
|
+
join(home, ".bun/bin"),
|
|
167
|
+
join(home, ".local/bin"),
|
|
168
|
+
join(home, ".cursor/bin"),
|
|
169
|
+
join(home, ".qlty/bin"),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
168
172
|
return [...new Set([...(process.env.PATH ?? "").split(":"), ...extraPaths].filter(Boolean))].join(":");
|
|
169
173
|
}
|
|
170
174
|
|
|
@@ -382,20 +382,35 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
|
|
|
382
382
|
}
|
|
383
383
|
|
|
384
384
|
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
385
|
-
|
|
385
|
+
switch message.name {
|
|
386
|
+
case "accentHue":
|
|
387
|
+
updateAccentHueFromMessage(message.body)
|
|
388
|
+
case "externalLink":
|
|
389
|
+
openExternalLinkFromMessage(message.body)
|
|
390
|
+
default:
|
|
386
391
|
return
|
|
387
392
|
}
|
|
393
|
+
}
|
|
388
394
|
|
|
389
|
-
|
|
395
|
+
private func updateAccentHueFromMessage(_ body: Any) {
|
|
396
|
+
if let number = body as? NSNumber {
|
|
390
397
|
updateTitlebarAccent(hue: CGFloat(number.doubleValue))
|
|
391
398
|
return
|
|
392
399
|
}
|
|
393
400
|
|
|
394
|
-
if let value =
|
|
401
|
+
if let value = body as? String, let hue = Double(value) {
|
|
395
402
|
updateTitlebarAccent(hue: CGFloat(hue))
|
|
396
403
|
}
|
|
397
404
|
}
|
|
398
405
|
|
|
406
|
+
private func openExternalLinkFromMessage(_ body: Any) {
|
|
407
|
+
guard let value = body as? String, let url = URL(string: value) else {
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
_ = openExternalURL(url)
|
|
412
|
+
}
|
|
413
|
+
|
|
399
414
|
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
|
400
415
|
guard let url = navigationAction.request.url else {
|
|
401
416
|
decisionHandler(.allow)
|
|
@@ -407,8 +422,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
|
|
|
407
422
|
return
|
|
408
423
|
}
|
|
409
424
|
|
|
410
|
-
if url
|
|
411
|
-
NSWorkspace.shared.open(url)
|
|
425
|
+
if openExternalURL(url) {
|
|
412
426
|
decisionHandler(.cancel)
|
|
413
427
|
return
|
|
414
428
|
}
|
|
@@ -421,13 +435,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
|
|
|
421
435
|
return nil
|
|
422
436
|
}
|
|
423
437
|
|
|
424
|
-
|
|
425
|
-
NSWorkspace.shared.open(url)
|
|
426
|
-
}
|
|
438
|
+
_ = openExternalURL(url)
|
|
427
439
|
|
|
428
440
|
return nil
|
|
429
441
|
}
|
|
430
442
|
|
|
443
|
+
private func openExternalURL(_ url: URL) -> Bool {
|
|
444
|
+
let scheme = url.scheme?.lowercased()
|
|
445
|
+
guard scheme == "http" || scheme == "https" else {
|
|
446
|
+
return false
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
NSWorkspace.shared.open(url)
|
|
450
|
+
return true
|
|
451
|
+
}
|
|
452
|
+
|
|
431
453
|
private func isLocalAppURL(_ url: URL) -> Bool {
|
|
432
454
|
let host = url.host ?? ""
|
|
433
455
|
return (host == "127.0.0.1" || host == "localhost") && url.port == Int(webPort)
|
|
@@ -692,6 +714,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, WKNavigationDelegate,
|
|
|
692
714
|
configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
|
693
715
|
let userContentController = WKUserContentController()
|
|
694
716
|
userContentController.add(self, name: "accentHue")
|
|
717
|
+
userContentController.add(self, name: "externalLink")
|
|
695
718
|
configuration.userContentController = userContentController
|
|
696
719
|
webView = WKWebView(frame: .zero, configuration: configuration)
|
|
697
720
|
webView.navigationDelegate = self
|
|
@@ -97,6 +97,7 @@ function buildGuiStateNotifications(input: BuildNotificationInput): GuiNotificat
|
|
|
97
97
|
const appNeedsUpdate = (input.aiApps ?? []).filter((app) => app.needs_update);
|
|
98
98
|
const authErrors = (input.oauthPool?.providers ?? []).filter((provider) => provider.auth_errors > 0);
|
|
99
99
|
const rateLimited = (input.oauthPool?.providers ?? []).filter((provider) => provider.rate_limited > 0);
|
|
100
|
+
const oauthPoolSourceRef = input.oauthPool?.path_ref ?? "oauth_pool";
|
|
100
101
|
const notifications: GuiNotificationSummary[] = [];
|
|
101
102
|
|
|
102
103
|
if (input.restartRequired) {
|
|
@@ -135,7 +136,7 @@ function buildGuiStateNotifications(input: BuildNotificationInput): GuiNotificat
|
|
|
135
136
|
severity: "error",
|
|
136
137
|
category: "runtime",
|
|
137
138
|
source: "gui-status",
|
|
138
|
-
source_ref:
|
|
139
|
+
source_ref: oauthPoolSourceRef,
|
|
139
140
|
status: "active",
|
|
140
141
|
actions: [surfaceAction("open-ai-providers", "Open AI providers", "aiProviders"), commandAction("check-oauth-pool", "Check provider pool", `aidevops oauth-pool check ${provider.provider}`)],
|
|
141
142
|
}));
|
|
@@ -149,7 +150,7 @@ function buildGuiStateNotifications(input: BuildNotificationInput): GuiNotificat
|
|
|
149
150
|
severity: "warning",
|
|
150
151
|
category: "runtime",
|
|
151
152
|
source: "gui-status",
|
|
152
|
-
source_ref:
|
|
153
|
+
source_ref: oauthPoolSourceRef,
|
|
153
154
|
status: "active",
|
|
154
155
|
actions: [surfaceAction("open-ai-providers", "Open AI providers", "aiProviders")],
|
|
155
156
|
}));
|
|
@@ -2,8 +2,8 @@ import type { GuiResponseEnvelope, GuiStatusData } from "@aidevops/gui-shared";
|
|
|
2
2
|
import { type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent, type ReactElement, useEffect, useState } from "react";
|
|
3
3
|
import { MachineRail, Sidebar } from "./AppNavigation";
|
|
4
4
|
import { Workspace } from "./AppWorkspace";
|
|
5
|
-
import type { ConversationMode, FontPreference, FontSizePreference, ShellMode, SurfaceId, ThemePreference } from "./app-model";
|
|
6
|
-
import { DEFAULT_ACCENT_HUE, DEFAULT_FONT, DEFAULT_FONT_SIZE, fileRootBySurface, findSurface, findSurfaceSectionLabel, fontFamilyForPreference, fontSizeForPreference, getSystemTheme, isFontPreference, isFontSizePreference } from "./app-model";
|
|
5
|
+
import type { ContrastPreference, ConversationMode, FontPreference, FontSizePreference, ShellMode, SurfaceId, ThemePreference } from "./app-model";
|
|
6
|
+
import { DEFAULT_ACCENT_HUE, DEFAULT_CONTRAST, DEFAULT_FONT, DEFAULT_FONT_SIZE, fileRootBySurface, findSurface, findSurfaceSectionLabel, fontFamilyForPreference, fontSizeForPreference, getSystemTheme, isContrastPreference, isFontPreference, isFontSizePreference } from "./app-model";
|
|
7
7
|
import { type NavigationHistory, nextHistoryIndex, useNavigationHistoryKeyboard } from "./navigation-history";
|
|
8
8
|
import { fetchStatus, mockedStatus } from "./status-client";
|
|
9
9
|
|
|
@@ -23,6 +23,7 @@ type AppearanceStorageKey = (typeof appearanceStorageKeys)[keyof typeof appearan
|
|
|
23
23
|
|
|
24
24
|
export const appearanceStorageKeys = {
|
|
25
25
|
accentHue: "aidevops-gui-accent-hue",
|
|
26
|
+
contrast: "aidevops-gui-contrast",
|
|
26
27
|
font: "aidevops-gui-font",
|
|
27
28
|
fontSize: "aidevops-gui-font-size",
|
|
28
29
|
machineRail: "aidevops-gui-show-machine-rail",
|
|
@@ -37,9 +38,27 @@ const minSidebarWidth = 300;
|
|
|
37
38
|
const maxSidebarWidth = 520;
|
|
38
39
|
export const loadingSkeletonPanelLabels = ["machine rail", "sidebar", "workspace", "status bar"] as const;
|
|
39
40
|
export const loadingBrandGlyph = ">_";
|
|
41
|
+
const loadingMachineOrbKeys = ["machine-orb-a", "machine-orb-b", "machine-orb-c", "machine-orb-d", "machine-orb-e", "machine-orb-f"] as const;
|
|
42
|
+
const loadingListRows = [
|
|
43
|
+
{ key: "loading-list-a", variant: "short" },
|
|
44
|
+
{ key: "loading-list-b", variant: "line" },
|
|
45
|
+
{ key: "loading-list-c", variant: "line" },
|
|
46
|
+
{ key: "loading-list-d", variant: "short" },
|
|
47
|
+
{ key: "loading-list-e", variant: "line" },
|
|
48
|
+
{ key: "loading-list-f", variant: "line" },
|
|
49
|
+
{ key: "loading-list-g", variant: "short" },
|
|
50
|
+
{ key: "loading-list-h", variant: "line" },
|
|
51
|
+
{ key: "loading-list-i", variant: "line" },
|
|
52
|
+
{ key: "loading-list-j", variant: "short" },
|
|
53
|
+
{ key: "loading-list-k", variant: "line" },
|
|
54
|
+
{ key: "loading-list-l", variant: "line" },
|
|
55
|
+
] as const;
|
|
56
|
+
const loadingCardKeys = ["loading-card-a", "loading-card-b", "loading-card-c", "loading-card-d", "loading-card-e", "loading-card-f"] as const;
|
|
57
|
+
const loadingStatusRows = ["dot", "line-a", "line-b", "line-c", "line-d", "line-e", "line-f", "line-g"] as const;
|
|
40
58
|
|
|
41
59
|
export interface StoredAppearancePreferences {
|
|
42
60
|
accentHue: number;
|
|
61
|
+
contrastPreference: ContrastPreference;
|
|
43
62
|
fontPreference: FontPreference;
|
|
44
63
|
fontSizePreference: FontSizePreference;
|
|
45
64
|
machineRailVisible: boolean;
|
|
@@ -51,11 +70,13 @@ export interface StoredAppearancePreferences {
|
|
|
51
70
|
export function readStoredAppearancePreferences(storage: ReadableAppearanceStorage | undefined = browserLocalStorage(), isDesktopShell = isMacosDesktopBrowser()): StoredAppearancePreferences {
|
|
52
71
|
const savedTheme = storage?.getItem(appearanceStorageKeys.theme);
|
|
53
72
|
const savedAccentHue = Number.parseInt(storage?.getItem(appearanceStorageKeys.accentHue) ?? "", 10);
|
|
73
|
+
const savedContrast = storage?.getItem(appearanceStorageKeys.contrast) ?? null;
|
|
54
74
|
const savedFont = storage?.getItem(appearanceStorageKeys.font) ?? null;
|
|
55
75
|
const savedFontSize = storage?.getItem(appearanceStorageKeys.fontSize) ?? null;
|
|
56
76
|
|
|
57
77
|
return {
|
|
58
78
|
accentHue: Number.isFinite(savedAccentHue) && savedAccentHue >= 0 && savedAccentHue <= 359 ? savedAccentHue : DEFAULT_ACCENT_HUE,
|
|
79
|
+
contrastPreference: isContrastPreference(savedContrast) ? savedContrast : DEFAULT_CONTRAST,
|
|
59
80
|
fontPreference: isFontPreference(savedFont) ? savedFont : DEFAULT_FONT,
|
|
60
81
|
fontSizePreference: isFontSizePreference(savedFontSize) ? savedFontSize : DEFAULT_FONT_SIZE,
|
|
61
82
|
machineRailVisible: isDesktopShell ? readStoredBoolean(storage, appearanceStorageKeys.machineRail, true) : true,
|
|
@@ -72,6 +93,7 @@ export function App(): ReactElement {
|
|
|
72
93
|
const [navigation, setNavigation] = useState<NavigationHistory>({ entries: ["overview"], index: 0 });
|
|
73
94
|
const [themePreference, setThemePreference] = useState<ThemePreference>(storedAppearancePreferences.themePreference);
|
|
74
95
|
const [accentHue, setAccentHue] = useState(storedAppearancePreferences.accentHue);
|
|
96
|
+
const [contrastPreference, setContrastPreference] = useState<ContrastPreference>(storedAppearancePreferences.contrastPreference);
|
|
75
97
|
const [fontPreference, setFontPreference] = useState<FontPreference>(storedAppearancePreferences.fontPreference);
|
|
76
98
|
const [fontSizePreference, setFontSizePreference] = useState<FontSizePreference>(storedAppearancePreferences.fontSizePreference);
|
|
77
99
|
const [machineRailVisible, setMachineRailVisible] = useState(storedAppearancePreferences.machineRailVisible);
|
|
@@ -146,6 +168,11 @@ export function App(): ReactElement {
|
|
|
146
168
|
sendNativeAccentHue(accentHue);
|
|
147
169
|
}, [accentHue]);
|
|
148
170
|
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
document.documentElement.dataset.contrast = contrastPreference;
|
|
173
|
+
persistAppearancePreference(appearanceStorageKeys.contrast, contrastPreference);
|
|
174
|
+
}, [contrastPreference]);
|
|
175
|
+
|
|
149
176
|
useEffect(() => {
|
|
150
177
|
document.documentElement.style.setProperty("--font-family-app", fontFamilyForPreference(fontPreference));
|
|
151
178
|
persistAppearancePreference(appearanceStorageKeys.font, fontPreference);
|
|
@@ -191,12 +218,14 @@ export function App(): ReactElement {
|
|
|
191
218
|
<Sidebar
|
|
192
219
|
activeSurface={activeSurface}
|
|
193
220
|
accentHue={accentHue}
|
|
221
|
+
contrastPreference={contrastPreference}
|
|
194
222
|
conversationMode={conversationMode}
|
|
195
223
|
fontSizePreference={fontSizePreference}
|
|
196
224
|
fontPreference={fontPreference}
|
|
197
225
|
selectedLocalRepoIndex={selectedLocalRepoIndex}
|
|
198
226
|
selectedSessionId={selectedSessionId}
|
|
199
227
|
setAccentHue={setAccentHue}
|
|
228
|
+
setContrastPreference={setContrastPreference}
|
|
200
229
|
setActiveSurface={setActiveSurface}
|
|
201
230
|
setConversationMode={setConversationMode}
|
|
202
231
|
setFontSizePreference={setFontSizePreference}
|
|
@@ -246,7 +275,7 @@ export function AppLoadingSkeleton({ machineRailVisible }: { machineRailVisible:
|
|
|
246
275
|
<LoadingBrandOverlay />
|
|
247
276
|
{machineRailVisible ? (
|
|
248
277
|
<section className="machine-rail loading-panel" aria-label={loadingSkeletonPanelLabels[0]}>
|
|
249
|
-
{
|
|
278
|
+
{loadingMachineOrbKeys.map((key) => <span className="loading-orb" key={key} />)}
|
|
250
279
|
</section>
|
|
251
280
|
) : null}
|
|
252
281
|
<section className="app-sidebar loading-panel" aria-label={loadingSkeletonPanelLabels[1]}>
|
|
@@ -256,7 +285,7 @@ export function AppLoadingSkeleton({ machineRailVisible }: { machineRailVisible:
|
|
|
256
285
|
</div>
|
|
257
286
|
<span className="loading-pill" />
|
|
258
287
|
<div className="loading-list">
|
|
259
|
-
{
|
|
288
|
+
{loadingListRows.map((row) => <span className={row.variant === "short" ? "loading-line loading-line-short" : "loading-line"} key={row.key} />)}
|
|
260
289
|
</div>
|
|
261
290
|
<div className="loading-sidebar-footer">
|
|
262
291
|
<span className="loading-pill" />
|
|
@@ -275,13 +304,13 @@ export function AppLoadingSkeleton({ machineRailVisible }: { machineRailVisible:
|
|
|
275
304
|
<div className="loading-workspace-body">
|
|
276
305
|
<span className="loading-line loading-line-hero" />
|
|
277
306
|
<div className="loading-card-grid">
|
|
278
|
-
{
|
|
307
|
+
{loadingCardKeys.map((key) => <span className="loading-card" key={key} />)}
|
|
279
308
|
</div>
|
|
280
309
|
<span className="loading-panel-block" />
|
|
281
310
|
</div>
|
|
282
311
|
</section>
|
|
283
|
-
<div className="desktop-status-bar loading-status-bar" aria-label={loadingSkeletonPanelLabels[3]}>
|
|
284
|
-
{
|
|
312
|
+
<div className="desktop-status-bar loading-status-bar" aria-label={loadingSkeletonPanelLabels[3]} role="status">
|
|
313
|
+
{loadingStatusRows.map((row) => <span className={row === "dot" ? "loading-dot" : "loading-line"} key={row} />)}
|
|
285
314
|
</div>
|
|
286
315
|
</main>
|
|
287
316
|
);
|
|
@@ -289,7 +318,7 @@ export function AppLoadingSkeleton({ machineRailVisible }: { machineRailVisible:
|
|
|
289
318
|
|
|
290
319
|
function LoadingBrandOverlay(): ReactElement {
|
|
291
320
|
return (
|
|
292
|
-
<div className="loading-brand-overlay" aria-label="Starting aidevops">
|
|
321
|
+
<div className="loading-brand-overlay" aria-label="Starting aidevops" role="status">
|
|
293
322
|
<span className="loading-brand-mark" aria-hidden="true">
|
|
294
323
|
<span className="loading-brand-word">aidevops</span>
|
|
295
324
|
<span className="loading-brand-chevron">></span>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { GuiAppActionJobSummary } from "@aidevops/gui-shared";
|
|
2
|
+
import { type ReactElement, type ReactNode, useEffect, useRef } from "react";
|
|
3
|
+
import { FiX } from "react-icons/fi";
|
|
4
|
+
|
|
5
|
+
const ANSI_PATTERN = /\x1b\[([0-9;]*)m/g;
|
|
6
|
+
|
|
7
|
+
export function AppActionTerminal({ job, onDismiss }: { job: GuiAppActionJobSummary; onDismiss: (jobId: string) => void }): ReactElement {
|
|
8
|
+
const outputRef = useRef<HTMLPreElement | null>(null);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (outputRef.current !== null) {
|
|
12
|
+
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
|
13
|
+
}
|
|
14
|
+
}, [job.output.length]);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<section className="app-terminal" id={`app-job-${job.id}`} aria-label="App action terminal output">
|
|
18
|
+
<header><strong>{job.app_id} {job.action}</strong><span>{terminalStatusLabel(job)}</span><button aria-label={`Dismiss ${job.app_id} ${job.action} terminal output`} className="terminal-close-button" onClick={() => onDismiss(job.id)} title="Dismiss terminal output" type="button"><FiX aria-hidden="true" /></button></header>
|
|
19
|
+
<pre ref={outputRef}>{renderTerminalOutput(job.output)}</pre>
|
|
20
|
+
</section>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function terminalStatusLabel(job: GuiAppActionJobSummary): string {
|
|
25
|
+
if (job.status === "running") {
|
|
26
|
+
return "running";
|
|
27
|
+
}
|
|
28
|
+
if (job.exit_code === null) {
|
|
29
|
+
return job.status;
|
|
30
|
+
}
|
|
31
|
+
return `${job.status} · exit ${job.exit_code}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderTerminalOutput(lines: string[]): ReactNode[] {
|
|
35
|
+
return lines.flatMap((line, index) => [
|
|
36
|
+
...renderAnsiLine(line, `line-${index}`),
|
|
37
|
+
index === lines.length - 1 ? null : "\n",
|
|
38
|
+
]).filter((node): node is ReactNode => node !== null);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderAnsiLine(line: string, keyPrefix: string): ReactNode[] {
|
|
42
|
+
const nodes: ReactNode[] = [];
|
|
43
|
+
ANSI_PATTERN.lastIndex = 0;
|
|
44
|
+
let activeClass = "";
|
|
45
|
+
let lastIndex = 0;
|
|
46
|
+
let match = ANSI_PATTERN.exec(line);
|
|
47
|
+
|
|
48
|
+
while (match !== null) {
|
|
49
|
+
if (match.index > lastIndex) {
|
|
50
|
+
nodes.push(terminalSpan(line.slice(lastIndex, match.index), activeClass, `${keyPrefix}-${nodes.length}`));
|
|
51
|
+
}
|
|
52
|
+
activeClass = ansiClassForCodes(match[1]);
|
|
53
|
+
lastIndex = ANSI_PATTERN.lastIndex;
|
|
54
|
+
match = ANSI_PATTERN.exec(line);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (lastIndex < line.length) {
|
|
58
|
+
nodes.push(terminalSpan(line.slice(lastIndex), activeClass, `${keyPrefix}-${nodes.length}`));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (nodes.length === 0) {
|
|
62
|
+
nodes.push(terminalSpan(line, activeClass, `${keyPrefix}-0`));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return nodes;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function terminalSpan(textValue: string, className: string, key: string): ReactNode {
|
|
69
|
+
const promptClass = textValue.startsWith("$ ") ? "ansi-prompt" : "";
|
|
70
|
+
const combinedClass = [className, promptClass].filter(Boolean).join(" ");
|
|
71
|
+
return combinedClass.length > 0 ? <span className={combinedClass} key={key}>{textValue}</span> : textValue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ansiClassForCodes(rawCodes: string): string {
|
|
75
|
+
const codes = rawCodes.length === 0 ? [0] : rawCodes.split(";").map((code) => Number.parseInt(code, 10));
|
|
76
|
+
const colorCode = [...codes].reverse().find((code) => (code >= 30 && code <= 37) || (code >= 90 && code <= 97));
|
|
77
|
+
if (colorCode === undefined) {
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
return `ansi-fg-${colorCode}`;
|
|
81
|
+
}
|
|
@@ -7,10 +7,8 @@ import {
|
|
|
7
7
|
FiBox,
|
|
8
8
|
FiBriefcase,
|
|
9
9
|
FiCalendar,
|
|
10
|
-
FiChevronDown,
|
|
11
10
|
FiChevronLeft,
|
|
12
11
|
FiChevronRight,
|
|
13
|
-
FiChevronUp,
|
|
14
12
|
FiClock,
|
|
15
13
|
FiDownloadCloud,
|
|
16
14
|
FiFileText,
|
|
@@ -29,17 +27,19 @@ import {
|
|
|
29
27
|
FiMessageSquare,
|
|
30
28
|
FiMonitor,
|
|
31
29
|
FiPackage,
|
|
32
|
-
FiRotateCcw,
|
|
33
30
|
FiServer,
|
|
34
31
|
FiSettings,
|
|
35
32
|
FiShield,
|
|
36
33
|
FiTerminal,
|
|
37
34
|
FiUsers,
|
|
38
35
|
} from "react-icons/fi";
|
|
39
|
-
import type { ConversationMode, FontPreference, FontSizePreference, ShellMode, SidebarMode, SurfaceIconName, SurfaceId, SurfaceNavGroup, SurfaceNavItem, ThemePreference } from "./app-model";
|
|
40
|
-
import {
|
|
36
|
+
import type { ContrastPreference, ConversationMode, FontPreference, FontSizePreference, ShellMode, SidebarMode, SurfaceIconName, SurfaceId, SurfaceNavGroup, SurfaceNavItem, ThemePreference } from "./app-model";
|
|
37
|
+
import { dashboardNavItem, navGroups, sidebarModeForSurface, surfaceRecordCounts, text } from "./app-model";
|
|
38
|
+
import { SidebarFooter } from "./AppearanceControls";
|
|
41
39
|
import { VaultPadlock, vaultCollectionForSurface } from "./VaultBadges";
|
|
42
40
|
|
|
41
|
+
export { hueFromInputValue } from "./AppearanceControls";
|
|
42
|
+
|
|
43
43
|
const surfaceIcons: Record<SurfaceIconName, IconType> = {
|
|
44
44
|
apps: FiBox,
|
|
45
45
|
bookmark: FiBookmark,
|
|
@@ -71,22 +71,6 @@ const surfaceIcons: Record<SurfaceIconName, IconType> = {
|
|
|
71
71
|
users: FiUsers,
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
-
export function hueFromInputValue(value: string): number | null {
|
|
75
|
-
const trimmedValue = value.trim();
|
|
76
|
-
|
|
77
|
-
if (trimmedValue.length === 0) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const hue = Number(trimmedValue);
|
|
82
|
-
|
|
83
|
-
if (!Number.isInteger(hue)) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return Math.min(359, Math.max(0, hue));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
74
|
export function SurfaceGlyph({ icon }: { icon: SurfaceIconName }) {
|
|
91
75
|
const Icon = surfaceIcons[icon];
|
|
92
76
|
|
|
@@ -110,9 +94,10 @@ export function MachineRail({ machine }: { machine?: GuiMachineSummary }) {
|
|
|
110
94
|
);
|
|
111
95
|
}
|
|
112
96
|
|
|
113
|
-
export function Sidebar({ activeSurface, accentHue, conversationMode, fontPreference, fontSizePreference, selectedLocalRepoIndex, selectedSessionId, setAccentHue, setActiveSurface, setConversationMode, setFontPreference, setFontSizePreference, setSelectedLocalRepoIndex, setSelectedSessionId, setShellMode, setShowBorders, setShowNavCounts, setThemePreference, shellMode, showBorders, showNavCounts, status, themePreference }: {
|
|
97
|
+
export function Sidebar({ activeSurface, accentHue, contrastPreference, conversationMode, fontPreference, fontSizePreference, selectedLocalRepoIndex, selectedSessionId, setAccentHue, setActiveSurface, setContrastPreference, setConversationMode, setFontPreference, setFontSizePreference, setSelectedLocalRepoIndex, setSelectedSessionId, setShellMode, setShowBorders, setShowNavCounts, setThemePreference, shellMode, showBorders, showNavCounts, status, themePreference }: {
|
|
114
98
|
activeSurface: SurfaceId;
|
|
115
99
|
accentHue: number;
|
|
100
|
+
contrastPreference: ContrastPreference;
|
|
116
101
|
conversationMode: ConversationMode;
|
|
117
102
|
fontPreference: FontPreference;
|
|
118
103
|
fontSizePreference: FontSizePreference;
|
|
@@ -120,6 +105,7 @@ export function Sidebar({ activeSurface, accentHue, conversationMode, fontPrefer
|
|
|
120
105
|
selectedSessionId: string | undefined;
|
|
121
106
|
setAccentHue: (hue: number) => void;
|
|
122
107
|
setActiveSurface: (surface: SurfaceId) => void;
|
|
108
|
+
setContrastPreference: (contrast: ContrastPreference) => void;
|
|
123
109
|
setConversationMode: (mode: ConversationMode) => void;
|
|
124
110
|
setFontPreference: (font: FontPreference) => void;
|
|
125
111
|
setFontSizePreference: (size: FontSizePreference) => void;
|
|
@@ -165,9 +151,11 @@ export function Sidebar({ activeSurface, accentHue, conversationMode, fontPrefer
|
|
|
165
151
|
</nav>
|
|
166
152
|
<SidebarFooter
|
|
167
153
|
accentHue={accentHue}
|
|
154
|
+
contrastPreference={contrastPreference}
|
|
168
155
|
fontPreference={fontPreference}
|
|
169
156
|
fontSizePreference={fontSizePreference}
|
|
170
157
|
setAccentHue={setAccentHue}
|
|
158
|
+
setContrastPreference={setContrastPreference}
|
|
171
159
|
setFontPreference={setFontPreference}
|
|
172
160
|
setFontSizePreference={setFontSizePreference}
|
|
173
161
|
setShowBorders={setShowBorders}
|
|
@@ -365,7 +353,7 @@ function SidebarHeader({ setActiveSurface, setShellMode, shellMode }: {
|
|
|
365
353
|
return (
|
|
366
354
|
<header className="sidebar-header">
|
|
367
355
|
<div className="sidebar-titlebar">
|
|
368
|
-
<button className="brand-lockup" onClick={() => setActiveSurface(dashboardNavItem.id)} title="Return to dashboard" type="button">
|
|
356
|
+
<button className="brand-lockup" onClick={() => { setShellMode("devices"); setActiveSurface(dashboardNavItem.id); }} title="Return to dashboard" type="button">
|
|
369
357
|
<span className="terminal-mark" aria-hidden="true">›_</span>
|
|
370
358
|
<strong>{text.aidevops}</strong>
|
|
371
359
|
</button>
|
|
@@ -381,169 +369,3 @@ function SidebarHeader({ setActiveSurface, setShellMode, shellMode }: {
|
|
|
381
369
|
</header>
|
|
382
370
|
);
|
|
383
371
|
}
|
|
384
|
-
|
|
385
|
-
function SidebarFooter({ accentHue, fontPreference, fontSizePreference, setAccentHue, setFontPreference, setFontSizePreference, setShowBorders, setShowNavCounts, setThemePreference, showBorders, showNavCounts, themePreference }: {
|
|
386
|
-
accentHue: number;
|
|
387
|
-
fontPreference: FontPreference;
|
|
388
|
-
fontSizePreference: FontSizePreference;
|
|
389
|
-
setAccentHue: (hue: number) => void;
|
|
390
|
-
setFontPreference: (font: FontPreference) => void;
|
|
391
|
-
setFontSizePreference: (size: FontSizePreference) => void;
|
|
392
|
-
setShowBorders: (show: boolean) => void;
|
|
393
|
-
setShowNavCounts: (show: boolean) => void;
|
|
394
|
-
setThemePreference: (theme: ThemePreference) => void;
|
|
395
|
-
showBorders: boolean;
|
|
396
|
-
showNavCounts: boolean;
|
|
397
|
-
themePreference: ThemePreference;
|
|
398
|
-
}) {
|
|
399
|
-
const [appearanceOpen, setAppearanceOpen] = useState(true);
|
|
400
|
-
const AppearanceChevron = appearanceOpen ? FiChevronDown : FiChevronUp;
|
|
401
|
-
const selectedFontFamily = fontFamilyForPreference(fontPreference);
|
|
402
|
-
const fontIndex = Math.max(0, fontOptions.findIndex((option) => option.value === fontPreference));
|
|
403
|
-
const fontSizeIndex = Math.max(0, fontSizeOptions.findIndex((option) => option.value === fontSizePreference));
|
|
404
|
-
const canSelectPreviousFont = fontIndex > 0;
|
|
405
|
-
const canSelectNextFont = fontIndex < fontOptions.length - 1;
|
|
406
|
-
const [hueInput, setHueInput] = useState(() => String(accentHue));
|
|
407
|
-
const setFontByIndex = (index: number) => {
|
|
408
|
-
const nextFont = fontOptions[index]?.value;
|
|
409
|
-
|
|
410
|
-
if (nextFont === undefined) {
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
setFontPreference(nextFont);
|
|
415
|
-
};
|
|
416
|
-
const updateAccentHue = (value: number) => {
|
|
417
|
-
if (!Number.isFinite(value)) {
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
setAccentHue(Math.min(359, Math.max(0, value)));
|
|
421
|
-
};
|
|
422
|
-
const updateHueInput = (value: string) => {
|
|
423
|
-
setHueInput(value);
|
|
424
|
-
|
|
425
|
-
const nextHue = hueFromInputValue(value);
|
|
426
|
-
|
|
427
|
-
if (nextHue === null) {
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
updateAccentHue(nextHue);
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
useEffect(() => {
|
|
435
|
-
setHueInput(String(accentHue));
|
|
436
|
-
}, [accentHue]);
|
|
437
|
-
|
|
438
|
-
return (
|
|
439
|
-
<footer className="sidebar-footer">
|
|
440
|
-
<section className={appearanceOpen ? "appearance-panel open" : "appearance-panel collapsed"}>
|
|
441
|
-
<button
|
|
442
|
-
aria-expanded={appearanceOpen}
|
|
443
|
-
className="appearance-panel-tab"
|
|
444
|
-
onClick={() => setAppearanceOpen((current) => !current)}
|
|
445
|
-
type="button"
|
|
446
|
-
>
|
|
447
|
-
{text.appearance}
|
|
448
|
-
<AppearanceChevron aria-hidden="true" className="appearance-chevron" />
|
|
449
|
-
</button>
|
|
450
|
-
{appearanceOpen ? <div className="appearance-panel-body">
|
|
451
|
-
<fieldset className="theme-control compact" aria-label={text.theme}>
|
|
452
|
-
{(["system", "light", "dark"] as const).map((theme) => (
|
|
453
|
-
<button
|
|
454
|
-
aria-pressed={themePreference === theme}
|
|
455
|
-
className={themePreference === theme ? "theme-option active" : "theme-option"}
|
|
456
|
-
key={theme}
|
|
457
|
-
onClick={() => setThemePreference(theme)}
|
|
458
|
-
type="button"
|
|
459
|
-
>
|
|
460
|
-
{theme}
|
|
461
|
-
</button>
|
|
462
|
-
))}
|
|
463
|
-
</fieldset>
|
|
464
|
-
<div className="theme-hue-control">
|
|
465
|
-
<div className="theme-control-heading">
|
|
466
|
-
<div className="hue-label-row">
|
|
467
|
-
<label htmlFor="theme-hue-slider">{text.hue}</label>
|
|
468
|
-
<input
|
|
469
|
-
aria-label="Hue value"
|
|
470
|
-
className="hue-number-input"
|
|
471
|
-
max="359"
|
|
472
|
-
min="0"
|
|
473
|
-
onChange={(event) => updateHueInput(event.currentTarget.value)}
|
|
474
|
-
type="number"
|
|
475
|
-
value={hueInput}
|
|
476
|
-
/>
|
|
477
|
-
</div>
|
|
478
|
-
<button aria-label="Reset hue to default" className="icon-reset-button" onClick={() => setAccentHue(DEFAULT_ACCENT_HUE)} title={text.reset} type="button"><FiRotateCcw aria-hidden="true" /></button>
|
|
479
|
-
</div>
|
|
480
|
-
<input
|
|
481
|
-
id="theme-hue-slider"
|
|
482
|
-
max="359"
|
|
483
|
-
min="0"
|
|
484
|
-
onChange={(event) => updateAccentHue(Number.parseInt(event.currentTarget.value, 10))}
|
|
485
|
-
type="range"
|
|
486
|
-
value={accentHue}
|
|
487
|
-
/>
|
|
488
|
-
</div>
|
|
489
|
-
<label className="switch-control appearance-switch">
|
|
490
|
-
<strong>{text.showBorders}</strong>
|
|
491
|
-
<input checked={showBorders} onChange={(event) => setShowBorders(event.currentTarget.checked)} type="checkbox" />
|
|
492
|
-
<span aria-hidden="true" />
|
|
493
|
-
</label>
|
|
494
|
-
<label className="switch-control appearance-switch">
|
|
495
|
-
<strong>{text.showCounts}</strong>
|
|
496
|
-
<input checked={showNavCounts} onChange={(event) => setShowNavCounts(event.currentTarget.checked)} type="checkbox" />
|
|
497
|
-
<span aria-hidden="true" />
|
|
498
|
-
</label>
|
|
499
|
-
<div className="font-size-control">
|
|
500
|
-
<label htmlFor="font-size-slider">{text.fontSize}</label>
|
|
501
|
-
<input
|
|
502
|
-
id="font-size-slider"
|
|
503
|
-
max={fontSizeOptions.length - 1}
|
|
504
|
-
min="0"
|
|
505
|
-
onChange={(event) => setFontSizePreference(fontSizeOptions[Number.parseInt(event.currentTarget.value, 10)]?.value ?? "xs")}
|
|
506
|
-
step="1"
|
|
507
|
-
type="range"
|
|
508
|
-
value={fontSizeIndex}
|
|
509
|
-
/>
|
|
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>
|
|
524
|
-
</div>
|
|
525
|
-
<div className="font-control">
|
|
526
|
-
<span id="appearance-font-selector-label">{text.font}</span>
|
|
527
|
-
<div className="selector-with-stepper font-selector-row">
|
|
528
|
-
<button aria-label="Previous font" className="selector-step-button" disabled={!canSelectPreviousFont} onClick={() => setFontByIndex(fontIndex - 1)} type="button"><FiChevronLeft aria-hidden="true" /></button>
|
|
529
|
-
<select
|
|
530
|
-
aria-labelledby="appearance-font-selector-label"
|
|
531
|
-
onChange={(event) => setFontPreference(event.currentTarget.value as FontPreference)}
|
|
532
|
-
style={{ fontFamily: selectedFontFamily }}
|
|
533
|
-
value={fontPreference}
|
|
534
|
-
>
|
|
535
|
-
{fontOptions.map((option) => (
|
|
536
|
-
<option key={option.value} style={{ fontFamily: option.fontFamily }} value={option.value}>
|
|
537
|
-
{option.label}
|
|
538
|
-
</option>
|
|
539
|
-
))}
|
|
540
|
-
</select>
|
|
541
|
-
<button aria-label="Next font" className="selector-step-button" disabled={!canSelectNextFont} onClick={() => setFontByIndex(fontIndex + 1)} type="button"><FiChevronRight aria-hidden="true" /></button>
|
|
542
|
-
</div>
|
|
543
|
-
</div>
|
|
544
|
-
</div>
|
|
545
|
-
: null}
|
|
546
|
-
</section>
|
|
547
|
-
</footer>
|
|
548
|
-
);
|
|
549
|
-
}
|