aidevops 3.28.0 → 3.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/VERSION +1 -1
- package/aidevops.sh +1 -1
- package/package.json +1 -1
- package/packages/gui-api/src/app.ts +133 -1
- package/packages/gui-api/src/status-adapter-utils.ts +35 -0
- package/packages/gui-api/src/status-adapter.ts +12 -39
- package/packages/gui-api/src/status-managed-apps.ts +228 -0
- package/packages/gui-api/src/status-vault.ts +1 -1
- package/packages/gui-desktop/scripts/install-macos-app.sh +76 -37
- package/packages/gui-shared/src/contracts.ts +63 -2
- package/packages/gui-shared/src/fixtures.ts +1 -0
- package/packages/gui-web/src/App.tsx +131 -2
- package/packages/gui-web/src/AppNavigation.tsx +13 -9
- package/packages/gui-web/src/AppWorkspace.tsx +20 -9
- package/packages/gui-web/src/CommandPalette.tsx +9 -2
- package/packages/gui-web/src/InventorySurfaces.tsx +107 -7
- package/packages/gui-web/src/app-model.ts +2 -11
- package/packages/gui-web/src/status-client.ts +1 -0
- package/packages/gui-web/src/styles.css +443 -5
- package/setup.sh +1 -1
|
@@ -5,7 +5,7 @@ import type { GuiFileRootId, GuiStatusData } from "../../gui-shared/src";
|
|
|
5
5
|
import { SurfaceGlyph } from "./AppNavigation";
|
|
6
6
|
import type { ConversationMode, ShellMode, SurfaceId, SurfaceNavItem } from "./app-model";
|
|
7
7
|
import { inventorySurfaceConfigs, text } from "./app-model";
|
|
8
|
-
import { CommandPalette, commandPaletteShortcutQuery } from "./CommandPalette";
|
|
8
|
+
import { CommandPalette, type CommandPaletteSelection, commandPaletteShortcutQuery } from "./CommandPalette";
|
|
9
9
|
import { FileExplorerSurface } from "./FileExplorerSurface";
|
|
10
10
|
import { AppsSurface, EditableInventorySurface, InstallationSurface } from "./InventorySurfaces";
|
|
11
11
|
import { AiProvidersSurface, LocalReposSurface, LockedVaultGate, OverviewSurface, PlannedSurface, ProjectsSurface, SecuritySurface, VaultSurface } from "./StatusSurfaces";
|
|
@@ -16,7 +16,7 @@ const communityLinks = {
|
|
|
16
16
|
x: "https://x.com/marcuswquinn",
|
|
17
17
|
} as const;
|
|
18
18
|
|
|
19
|
-
export function Workspace({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, conversationMode, fileRoot, goBack, goForward, selectedLocalRepoIndex, setActiveSurface, setConversationMode, setShellMode, shellMode, status }: {
|
|
19
|
+
export function Workspace({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, conversationMode, fileRoot, goBack, goForward, selectedLocalRepoIndex, selectedSessionId, setActiveSurface, setConversationMode, setSelectedLocalRepoIndex, setSelectedSessionId, setShellMode, shellMode, status }: {
|
|
20
20
|
activeItem: SurfaceNavItem;
|
|
21
21
|
activeSectionLabel: string;
|
|
22
22
|
activeSurface: SurfaceId;
|
|
@@ -27,25 +27,28 @@ export function Workspace({ activeItem, activeSectionLabel, activeSurface, canGo
|
|
|
27
27
|
goBack: () => void;
|
|
28
28
|
goForward: () => void;
|
|
29
29
|
selectedLocalRepoIndex: number;
|
|
30
|
+
selectedSessionId: string | undefined;
|
|
30
31
|
setActiveSurface: (surface: SurfaceId) => void;
|
|
31
32
|
setConversationMode: (mode: ConversationMode) => void;
|
|
33
|
+
setSelectedLocalRepoIndex: (index: number) => void;
|
|
34
|
+
setSelectedSessionId: (id: string | undefined) => void;
|
|
32
35
|
setShellMode: (mode: ShellMode) => void;
|
|
33
36
|
shellMode: ShellMode;
|
|
34
37
|
status: GuiStatusData;
|
|
35
38
|
}) {
|
|
36
39
|
return (
|
|
37
40
|
<section className="app-inset" aria-label={text.workspaceLabel}>
|
|
38
|
-
<WorkspaceHeader activeItem={activeItem} activeSectionLabel={activeSectionLabel} activeSurface={activeSurface} canGoBack={canGoBack} canGoForward={canGoForward} goBack={goBack} goForward={goForward} setActiveSurface={setActiveSurface} setConversationMode={setConversationMode} setShellMode={setShellMode} status={status} />
|
|
41
|
+
<WorkspaceHeader activeItem={activeItem} activeSectionLabel={activeSectionLabel} activeSurface={activeSurface} canGoBack={canGoBack} canGoForward={canGoForward} goBack={goBack} goForward={goForward} setActiveSurface={setActiveSurface} setConversationMode={setConversationMode} setSelectedLocalRepoIndex={setSelectedLocalRepoIndex} setSelectedSessionId={setSelectedSessionId} setShellMode={setShellMode} status={status} />
|
|
39
42
|
<div className="workspace-scroll">
|
|
40
43
|
{shellMode === "sessions"
|
|
41
|
-
? <ConversationWorkspace conversationMode={conversationMode} selectedLocalRepoIndex={selectedLocalRepoIndex} status={status} />
|
|
44
|
+
? <ConversationWorkspace conversationMode={conversationMode} selectedLocalRepoIndex={selectedLocalRepoIndex} selectedSessionId={selectedSessionId} status={status} />
|
|
42
45
|
: <SurfaceContent activeItem={activeItem} activeSurface={activeSurface} fileRoot={fileRoot} status={status} />}
|
|
43
46
|
</div>
|
|
44
47
|
</section>
|
|
45
48
|
);
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, goBack, goForward, setActiveSurface, setConversationMode, setShellMode, status }: {
|
|
51
|
+
function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, goBack, goForward, setActiveSurface, setConversationMode, setSelectedLocalRepoIndex, setSelectedSessionId, setShellMode, status }: {
|
|
49
52
|
activeItem: SurfaceNavItem;
|
|
50
53
|
activeSectionLabel: string;
|
|
51
54
|
activeSurface: SurfaceId;
|
|
@@ -55,6 +58,8 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
|
|
|
55
58
|
goForward: () => void;
|
|
56
59
|
setActiveSurface: (surface: SurfaceId) => void;
|
|
57
60
|
setConversationMode: (mode: ConversationMode) => void;
|
|
61
|
+
setSelectedLocalRepoIndex: (index: number) => void;
|
|
62
|
+
setSelectedSessionId: (id: string | undefined) => void;
|
|
58
63
|
setShellMode: (mode: ShellMode) => void;
|
|
59
64
|
status: GuiStatusData;
|
|
60
65
|
}): ReactElement {
|
|
@@ -87,8 +92,13 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
|
|
|
87
92
|
setProfileOpen(false);
|
|
88
93
|
};
|
|
89
94
|
|
|
90
|
-
const openItem = ({ conversationMode, shellMode, surface }:
|
|
95
|
+
const openItem = ({ conversationMode, repoPathRef, sessionId, shellMode, surface }: CommandPaletteSelection) => {
|
|
96
|
+
const repoIndex = status.local_repos.repos.findIndex((repo) => repo.path_ref === repoPathRef);
|
|
91
97
|
setShellMode(shellMode ?? "devices");
|
|
98
|
+
if (repoIndex >= 0) {
|
|
99
|
+
setSelectedLocalRepoIndex(repoIndex);
|
|
100
|
+
}
|
|
101
|
+
setSelectedSessionId(sessionId);
|
|
92
102
|
if (conversationMode) {
|
|
93
103
|
setConversationMode(conversationMode);
|
|
94
104
|
}
|
|
@@ -176,15 +186,16 @@ function NotificationsMenu({ openSurface }: { openSurface: (surface: SurfaceId)
|
|
|
176
186
|
);
|
|
177
187
|
}
|
|
178
188
|
|
|
179
|
-
function ConversationWorkspace({ conversationMode, selectedLocalRepoIndex, status }: {
|
|
189
|
+
function ConversationWorkspace({ conversationMode, selectedLocalRepoIndex, selectedSessionId, status }: {
|
|
180
190
|
conversationMode: ConversationMode;
|
|
181
191
|
selectedLocalRepoIndex: number;
|
|
192
|
+
selectedSessionId: string | undefined;
|
|
182
193
|
status: GuiStatusData;
|
|
183
194
|
}) {
|
|
184
195
|
const selectedRepo = status.local_repos.repos[selectedLocalRepoIndex] ?? status.local_repos.repos[0];
|
|
185
196
|
const selectedSession = selectedRepo === undefined
|
|
186
197
|
? undefined
|
|
187
|
-
: status.opencode_sessions.sessions.find((session) => session.repo_path_ref === selectedRepo.path_ref);
|
|
198
|
+
: status.opencode_sessions.sessions.find((session) => session.id_ref === selectedSessionId && session.repo_path_ref === selectedRepo.path_ref) ?? status.opencode_sessions.sessions.find((session) => session.repo_path_ref === selectedRepo.path_ref);
|
|
188
199
|
const title = conversationMode === "ai" ? selectedRepo?.name ?? text.opencodeSessions : text.teams;
|
|
189
200
|
|
|
190
201
|
return (
|
|
@@ -295,7 +306,7 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, status }: {
|
|
|
295
306
|
maintenance: <PlannedSurface label={text.maintenance} detail={text.projectWorkIntro} />,
|
|
296
307
|
performance: <PlannedSurface label={text.performance} detail={text.projectWorkIntro} />,
|
|
297
308
|
reports: <PlannedSurface label={text.reports} detail={text.projectWorkIntro} />,
|
|
298
|
-
apps: <AppsSurface />,
|
|
309
|
+
apps: <AppsSurface status={status} />,
|
|
299
310
|
installation: <InstallationSurface />,
|
|
300
311
|
projects: <ProjectsSurface status={status} />,
|
|
301
312
|
security: <SecuritySurface status={status} />,
|
|
@@ -15,7 +15,9 @@ interface CommandPaletteItem {
|
|
|
15
15
|
iconGlyph?: string;
|
|
16
16
|
id: string;
|
|
17
17
|
label: string;
|
|
18
|
+
repoPathRef?: string;
|
|
18
19
|
searchText: string;
|
|
20
|
+
sessionId?: string;
|
|
19
21
|
shellMode?: ShellMode;
|
|
20
22
|
surface: SurfaceId;
|
|
21
23
|
tag: string;
|
|
@@ -61,6 +63,8 @@ const plannedChannelItems: CommandPaletteItem[] = [
|
|
|
61
63
|
|
|
62
64
|
export interface CommandPaletteSelection {
|
|
63
65
|
conversationMode?: ConversationMode;
|
|
66
|
+
repoPathRef?: string;
|
|
67
|
+
sessionId?: string;
|
|
64
68
|
shellMode?: ShellMode;
|
|
65
69
|
surface: SurfaceId;
|
|
66
70
|
}
|
|
@@ -80,7 +84,7 @@ export function CommandPalette({ activeSurface, close, initialQuery, openItem, s
|
|
|
80
84
|
if (item.action === "copy-current-link") {
|
|
81
85
|
void copyCurrentSurfaceLink(item.surface);
|
|
82
86
|
}
|
|
83
|
-
openItem({ conversationMode: item.conversationMode, shellMode: item.shellMode, surface: item.surface });
|
|
87
|
+
openItem({ conversationMode: item.conversationMode, repoPathRef: item.repoPathRef, sessionId: item.sessionId, shellMode: item.shellMode, surface: item.surface });
|
|
84
88
|
}
|
|
85
89
|
}, [openItem, recentIds]);
|
|
86
90
|
|
|
@@ -165,13 +169,14 @@ export function commandPaletteMatches(items: CommandPaletteItem[], query: string
|
|
|
165
169
|
const normalizedQuery = query.trim().toLowerCase();
|
|
166
170
|
const prefix = normalizedQuery.charAt(0);
|
|
167
171
|
const scopedItems = itemsForCommandPrefix(items, prefix);
|
|
172
|
+
const filterQuery = commandPaletteShortcutEntries.some(([shortcut]) => shortcut === prefix) ? normalizedQuery.slice(1).trim() : normalizedQuery;
|
|
168
173
|
|
|
169
174
|
if (normalizedQuery.length === 0 || commandPaletteShortcutEntries.some(([shortcut]) => shortcut === normalizedQuery)) {
|
|
170
175
|
return orderCommandItemsByRecency(scopedItems, recentIds).slice(0, 8);
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
return scopedItems
|
|
174
|
-
.filter((item) => item.searchText.toLowerCase().includes(
|
|
179
|
+
.filter((item) => item.searchText.toLowerCase().includes(filterQuery))
|
|
175
180
|
.slice(0, 8);
|
|
176
181
|
}
|
|
177
182
|
|
|
@@ -197,7 +202,9 @@ function commandPaletteItems(status: GuiStatusData, activeSurface: SurfaceId): C
|
|
|
197
202
|
iconGlyph: "_",
|
|
198
203
|
id: `session-${session.id_ref}`,
|
|
199
204
|
label: `_${session.title}`,
|
|
205
|
+
repoPathRef: session.repo_path_ref,
|
|
200
206
|
searchText: `_${session.title} ${session.title} ${session.repo_path_ref} ${session.agent} ${session.model}`,
|
|
207
|
+
sessionId: session.id_ref,
|
|
201
208
|
surface: "git",
|
|
202
209
|
shellMode: "sessions",
|
|
203
210
|
conversationMode: "ai",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { GuiAppActionId, GuiAppActionJobSummary, GuiManagedAppSummary, GuiResponseEnvelope, GuiStatusData } from "@aidevops/gui-shared";
|
|
2
|
+
import { type Dispatch, type ReactElement, type SetStateAction, useEffect, useState } from "react";
|
|
3
|
+
import { FiDownload, FiExternalLink, FiRefreshCw, FiRepeat, FiTrash2 } from "react-icons/fi";
|
|
2
4
|
import type { InventoryColumn } from "./app-model";
|
|
3
|
-
import {
|
|
5
|
+
import { installationRows, text } from "./app-model";
|
|
4
6
|
|
|
5
7
|
interface DraftInventoryRow {
|
|
6
8
|
id: string;
|
|
@@ -9,7 +11,23 @@ interface DraftInventoryRow {
|
|
|
9
11
|
|
|
10
12
|
let draftRowCounter = 0;
|
|
11
13
|
|
|
12
|
-
export function AppsSurface(): ReactElement {
|
|
14
|
+
export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement {
|
|
15
|
+
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
|
|
16
|
+
const [jobs, setJobs] = useState<Record<string, GuiAppActionJobSummary>>({});
|
|
17
|
+
const selectedJob = selectedJobId === null ? null : jobs[selectedJobId] ?? null;
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (selectedJob === null || selectedJob.status !== "running") {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const timer = window.setInterval(() => {
|
|
25
|
+
void refreshJob(selectedJob.id, setJobs);
|
|
26
|
+
}, 1_500);
|
|
27
|
+
|
|
28
|
+
return () => window.clearInterval(timer);
|
|
29
|
+
}, [selectedJob]);
|
|
30
|
+
|
|
13
31
|
return (
|
|
14
32
|
<section className="panel" aria-label={text.apps}>
|
|
15
33
|
<div className="section-heading">
|
|
@@ -17,16 +35,98 @@ export function AppsSurface(): ReactElement {
|
|
|
17
35
|
<h2>{text.apps}</h2>
|
|
18
36
|
<p>{text.appsIntro}</p>
|
|
19
37
|
</div>
|
|
20
|
-
<div className="
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<div className="data-row" key={row.name}><span>{row.name}</span><span>{row.latest}</span><span>{row.channel}</span><span>{row.website}</span></div>
|
|
38
|
+
<div className="managed-app-list">
|
|
39
|
+
{status.managed_apps.map((app) => (
|
|
40
|
+
<ManagedAppRow app={app} key={app.id} onJob={(job) => { setJobs((current) => ({ ...current, [job.id]: job })); setSelectedJobId(job.id); }} />
|
|
24
41
|
))}
|
|
25
42
|
</div>
|
|
43
|
+
{selectedJob ? <AppActionTerminal job={selectedJob} /> : <p className="empty-state compact-notice">Install/update actions run as allowlisted background jobs and stream here. 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>}
|
|
44
|
+
</section>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ManagedAppRow({ app, onJob }: { app: GuiManagedAppSummary; onJob: (job: GuiAppActionJobSummary) => void }): ReactElement {
|
|
49
|
+
return (
|
|
50
|
+
<article className="managed-app-row">
|
|
51
|
+
<div className="managed-app-main">
|
|
52
|
+
<div>
|
|
53
|
+
<p className="eyebrow">{app.category}</p>
|
|
54
|
+
<h3>{app.name}</h3>
|
|
55
|
+
<p>{app.description}</p>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="managed-app-links">
|
|
58
|
+
<OriginLink href={app.origin_website_url} label={text.website} />
|
|
59
|
+
<OriginLink href={app.origin_repo_url} label="Repo" />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="managed-app-details">
|
|
63
|
+
<ToggleSwitch checked={app.aidevops_install} label="setup installs" />
|
|
64
|
+
<ToggleSwitch checked={app.aidevops_update} label="update maintains" />
|
|
65
|
+
<AppMeta label="Installed" value={app.installed_version} />
|
|
66
|
+
<AppMeta label="Latest" value={app.latest_version} />
|
|
67
|
+
<AppMeta label={text.path} value={app.install_path_ref} />
|
|
68
|
+
<AppMeta label="Status" value={app.status} />
|
|
69
|
+
</div>
|
|
70
|
+
<div className="managed-app-actions">
|
|
71
|
+
{app.actions.map((action) => <AppActionButton action={action.id} app={app} disabled={!action.enabled} key={action.id} onJob={onJob} title={action.command_preview} />)}
|
|
72
|
+
</div>
|
|
73
|
+
</article>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function OriginLink({ href, label }: { href: string; label: string }): ReactElement {
|
|
78
|
+
if (href.length === 0) {
|
|
79
|
+
return <span className="origin-missing">{label}: source pending</span>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return <a href={href} rel="noreferrer" target="_blank">{label} <FiExternalLink aria-hidden="true" /></a>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function ToggleSwitch({ checked, label }: { checked: boolean; label: string }): ReactElement {
|
|
86
|
+
return <span className="managed-toggle"><span aria-hidden="true" className={checked ? "switch-track checked" : "switch-track"}><span /></span>{label}</span>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function AppMeta({ label, value }: { label: string; value: string }): ReactElement {
|
|
90
|
+
return <span className="app-meta"><small>{label}</small><strong>{value}</strong></span>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function AppActionButton({ action, app, disabled, onJob, title }: { action: GuiAppActionId; app: GuiManagedAppSummary; disabled: boolean; onJob: (job: GuiAppActionJobSummary) => void; title: string }): ReactElement {
|
|
94
|
+
const icon = action === "install" ? <FiDownload /> : action === "update" ? <FiRefreshCw /> : action === "reinstall" ? <FiRepeat /> : <FiTrash2 />;
|
|
95
|
+
|
|
96
|
+
async function runAction(): Promise<void> {
|
|
97
|
+
if (disabled) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if ((action === "remove" || action === "reinstall") && !window.confirm(`${action} ${app.name}?`)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const response = await fetch(`/api/apps/${encodeURIComponent(app.id)}/actions/${action}`, { method: "POST" });
|
|
105
|
+
const envelope = await response.json() as GuiResponseEnvelope<GuiAppActionJobSummary>;
|
|
106
|
+
onJob(envelope.data);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return <button aria-label={`${action} ${app.name}`} className="app-action-button" disabled={disabled} onClick={() => void runAction()} title={title} type="button">{icon}</button>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function AppActionTerminal({ job }: { job: GuiAppActionJobSummary }): ReactElement {
|
|
113
|
+
return (
|
|
114
|
+
<section className="app-terminal" aria-label="App action terminal output">
|
|
115
|
+
<header><strong>{job.app_id} {job.action}</strong><span>{job.status}{job.exit_code === null ? "" : ` (${job.exit_code})`}</span></header>
|
|
116
|
+
<pre>{job.output.join("\n")}</pre>
|
|
26
117
|
</section>
|
|
27
118
|
);
|
|
28
119
|
}
|
|
29
120
|
|
|
121
|
+
async function refreshJob(jobId: string, setJobs: Dispatch<SetStateAction<Record<string, GuiAppActionJobSummary>>>): Promise<void> {
|
|
122
|
+
const response = await fetch(`/api/apps/jobs/${encodeURIComponent(jobId)}`);
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const envelope = await response.json() as GuiResponseEnvelope<GuiAppActionJobSummary>;
|
|
127
|
+
setJobs((current) => ({ ...current, [envelope.data.id]: envelope.data }));
|
|
128
|
+
}
|
|
129
|
+
|
|
30
130
|
export function InstallationSurface(): ReactElement {
|
|
31
131
|
return (
|
|
32
132
|
<section className="panel" aria-label={text.installation}>
|
|
@@ -141,7 +141,7 @@ export const text = {
|
|
|
141
141
|
aidevops: "aidevops",
|
|
142
142
|
appShell: "App shell",
|
|
143
143
|
apps: "Apps",
|
|
144
|
-
appsIntro: "App and CLI inventory for tools installed or updated by aidevops
|
|
144
|
+
appsIntro: "App and CLI inventory for tools installed or updated by aidevops, with origin links, install/update policy, versions, paths, and allowlisted background actions.",
|
|
145
145
|
appStatusPending: "adapter pending",
|
|
146
146
|
brands: "Brands",
|
|
147
147
|
brandsIntro: "Draft brand inventory for names and website references.",
|
|
@@ -420,15 +420,6 @@ export const fileRootBySurface: Partial<Record<SurfaceId, GuiFileRootId>> = {
|
|
|
420
420
|
git: "git",
|
|
421
421
|
};
|
|
422
422
|
|
|
423
|
-
export const appRows = [
|
|
424
|
-
{ name: "aidevops", latest: text.appStatusPending, channel: "setup/update", website: "https://aidevops.sh" },
|
|
425
|
-
{ name: "OpenCode", latest: text.appStatusPending, channel: "runtime", website: text.appStatusPending },
|
|
426
|
-
{ name: "Bun", latest: text.appStatusPending, channel: "toolchain", website: text.appStatusPending },
|
|
427
|
-
{ name: "GitHub CLI", latest: text.appStatusPending, channel: "git", website: text.appStatusPending },
|
|
428
|
-
{ name: "ShellCheck", latest: text.appStatusPending, channel: "quality", website: text.appStatusPending },
|
|
429
|
-
{ name: "Biome", latest: text.appStatusPending, channel: "quality", website: text.appStatusPending },
|
|
430
|
-
];
|
|
431
|
-
|
|
432
423
|
export const installationRows = [
|
|
433
424
|
{ name: "OpenCode runtime", install: true, update: true, scope: "core" },
|
|
434
425
|
{ name: "GUI desktop launcher", install: true, update: true, scope: "local" },
|
|
@@ -582,7 +573,7 @@ export function surfaceRecordCounts(status: GuiStatusData): SurfaceRecordCounts
|
|
|
582
573
|
return {
|
|
583
574
|
agents: status.capabilities.length,
|
|
584
575
|
aiProviders: oauthAccounts,
|
|
585
|
-
apps:
|
|
576
|
+
apps: status.managed_apps.length || status.ai_apps.length,
|
|
586
577
|
config: status.settings.key_count || status.settings.keys.length,
|
|
587
578
|
git: status.local_repos.total || status.local_repos.repos.length,
|
|
588
579
|
hosts: populatedInventoryRowCount("hosts"),
|
|
@@ -86,6 +86,7 @@ function normalizeStatusEnvelope(envelope: GuiResponseEnvelope<Partial<GuiStatus
|
|
|
86
86
|
oauth_pool: { ...statusFixture.oauth_pool, ...data.oauth_pool, providers: data.oauth_pool?.providers ?? statusFixture.oauth_pool.providers },
|
|
87
87
|
setup_targets: data.setup_targets ?? statusFixture.setup_targets,
|
|
88
88
|
ai_apps: data.ai_apps ?? statusFixture.ai_apps,
|
|
89
|
+
managed_apps: data.managed_apps ?? statusFixture.managed_apps,
|
|
89
90
|
vault: {
|
|
90
91
|
...statusFixture.vault,
|
|
91
92
|
...data.vault,
|