aidevops 3.27.0 → 3.29.0
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 +125 -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-shared/src/contracts.ts +63 -2
- package/packages/gui-shared/src/fixtures.ts +1 -0
- package/packages/gui-web/src/App.tsx +63 -1
- package/packages/gui-web/src/AppNavigation.tsx +16 -9
- package/packages/gui-web/src/AppWorkspace.tsx +95 -29
- package/packages/gui-web/src/CommandPalette.tsx +115 -22
- package/packages/gui-web/src/InventorySurfaces.tsx +107 -7
- package/packages/gui-web/src/app-model.ts +8 -11
- package/packages/gui-web/src/dashboard.ts +1 -1
- package/packages/gui-web/src/status-client.ts +1 -0
- package/packages/gui-web/src/styles.css +268 -5
- package/setup.sh +1 -1
|
@@ -3,22 +3,28 @@ import { type ReactElement, useCallback, useEffect, useMemo, useRef, useState }
|
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { FiSearch } from "react-icons/fi";
|
|
5
5
|
import { SurfaceGlyph } from "./AppNavigation";
|
|
6
|
-
import type { SurfaceId, SurfaceNavItem } from "./app-model";
|
|
7
|
-
import { orderedNavItems } from "./app-model";
|
|
6
|
+
import type { ConversationMode, ShellMode, SurfaceId, SurfaceIconName, SurfaceNavItem } from "./app-model";
|
|
7
|
+
import { navGroups, orderedNavItems, utilityNavItems } from "./app-model";
|
|
8
8
|
import type { GuiStatusData } from "../../gui-shared/src";
|
|
9
9
|
|
|
10
10
|
interface CommandPaletteItem {
|
|
11
|
+
action?: "copy-current-link";
|
|
11
12
|
description: string;
|
|
12
|
-
|
|
13
|
+
conversationMode?: ConversationMode;
|
|
14
|
+
icon: SurfaceIconName;
|
|
15
|
+
iconGlyph?: string;
|
|
13
16
|
id: string;
|
|
14
17
|
label: string;
|
|
18
|
+
repoPathRef?: string;
|
|
15
19
|
searchText: string;
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
shellMode?: ShellMode;
|
|
16
22
|
surface: SurfaceId;
|
|
17
23
|
tag: string;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
const commandPaletteRecentStorageKey = "aidevops-gui-command-palette-recents";
|
|
21
|
-
const commandPaletteRecentLimit =
|
|
27
|
+
const commandPaletteRecentLimit = 5;
|
|
22
28
|
|
|
23
29
|
const slashCommandItems: CommandPaletteItem[] = [
|
|
24
30
|
{ id: "slash-add-device", label: "/add-device", description: "Start device pairing setup", icon: "device", searchText: "/add-device add device pair machine", surface: "devices", tag: "Command" },
|
|
@@ -28,13 +34,43 @@ const slashCommandItems: CommandPaletteItem[] = [
|
|
|
28
34
|
{ id: "slash-add-ai-provider", label: "/add-ai-provider", description: "Connect an AI provider account", icon: "users", searchText: "/add-ai-provider add ai provider oauth", surface: "aiProviders", tag: "Command" },
|
|
29
35
|
];
|
|
30
36
|
|
|
37
|
+
const addCommandItems: CommandPaletteItem[] = slashCommandItems.map((item) => {
|
|
38
|
+
const label = item.label.replace("/", "+");
|
|
39
|
+
|
|
40
|
+
return { ...item, id: item.id.replace("slash-", "plus-"), label, searchText: `${label} ${item.searchText}`, tag: "Add" };
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const removeCommandItems: CommandPaletteItem[] = [
|
|
44
|
+
{ id: "remove-device", label: "-device", description: "Remove a paired device placeholder", icon: "device", searchText: "-device remove device unpair machine", surface: "devices", tag: "Remove" },
|
|
45
|
+
{ id: "remove-local-repo", label: "-local-repo", description: "Remove a local repository reference placeholder", icon: "folder", searchText: "-local-repo remove local repo git", surface: "git", tag: "Remove" },
|
|
46
|
+
{ id: "remove-secret", label: "-secret", description: "Remove a protected secret reference placeholder", icon: "shield", searchText: "-secret remove secret vault password", surface: "security", tag: "Remove" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const terminalCommandItems: CommandPaletteItem[] = [
|
|
50
|
+
{ id: "terminal-new-command", label: "> new terminal command", description: "Send a command to a new terminal session", icon: "terminal", searchText: "> terminal command new session shell", surface: "localSetup", tag: "Terminal" },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const emojiItems: CommandPaletteItem[] = [
|
|
54
|
+
{ id: "emoji-thumbs-up", label: ":thumbsup:", description: "Copy or insert 👍", icon: "message", iconGlyph: "👍", searchText: ":thumbsup thumbs up approve emoji", surface: "messagingAccounts", tag: "Emoji" },
|
|
55
|
+
{ id: "emoji-check", label: ":white_check_mark:", description: "Copy or insert ✅", icon: "message", iconGlyph: "✅", searchText: ":white_check_mark check done emoji", surface: "messagingAccounts", tag: "Emoji" },
|
|
56
|
+
{ id: "emoji-eyes", label: ":eyes:", description: "Copy or insert 👀", icon: "message", iconGlyph: "👀", searchText: ":eyes review look emoji", surface: "messagingAccounts", tag: "Emoji" },
|
|
57
|
+
];
|
|
58
|
+
|
|
31
59
|
const plannedChannelItems: CommandPaletteItem[] = [
|
|
32
|
-
{ id: "channel-devops", label: "#devops", description: "Chat channel for aidevops operations", icon: "
|
|
33
|
-
{ id: "channel-comms", label: "#comms", description: "Chat channel for communications", icon: "
|
|
60
|
+
{ id: "channel-devops", label: "#devops", description: "Chat channel for aidevops operations", icon: "hash", searchText: "#devops devops channel chat", surface: "messagingAccounts", shellMode: "sessions", conversationMode: "people", tag: "Channel" },
|
|
61
|
+
{ id: "channel-comms", label: "#comms", description: "Chat channel for communications", icon: "hash", searchText: "#comms comms channel chat", surface: "messagingAccounts", shellMode: "sessions", conversationMode: "people", tag: "Channel" },
|
|
34
62
|
];
|
|
35
63
|
|
|
36
|
-
export
|
|
37
|
-
|
|
64
|
+
export interface CommandPaletteSelection {
|
|
65
|
+
conversationMode?: ConversationMode;
|
|
66
|
+
repoPathRef?: string;
|
|
67
|
+
sessionId?: string;
|
|
68
|
+
shellMode?: ShellMode;
|
|
69
|
+
surface: SurfaceId;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function CommandPalette({ activeSurface, close, initialQuery, openItem, status }: { activeSurface: SurfaceId; close: () => void; initialQuery: string; openItem: (selection: CommandPaletteSelection) => void; status: GuiStatusData }): ReactElement {
|
|
73
|
+
const allItems = useMemo(() => commandPaletteItems(status, activeSurface), [activeSurface, status]);
|
|
38
74
|
const [query, setQuery] = useState(initialQuery);
|
|
39
75
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
40
76
|
const [recentIds, setRecentIds] = useState<string[]>(() => readCommandPaletteRecentIds());
|
|
@@ -45,9 +81,12 @@ export function CommandPalette({ close, initialQuery, openSurface, status }: { c
|
|
|
45
81
|
if (item) {
|
|
46
82
|
const nextRecentIds = rememberCommandPaletteItemId(item.id, recentIds);
|
|
47
83
|
setRecentIds(nextRecentIds);
|
|
48
|
-
|
|
84
|
+
if (item.action === "copy-current-link") {
|
|
85
|
+
void copyCurrentSurfaceLink(item.surface);
|
|
86
|
+
}
|
|
87
|
+
openItem({ conversationMode: item.conversationMode, repoPathRef: item.repoPathRef, sessionId: item.sessionId, shellMode: item.shellMode, surface: item.surface });
|
|
49
88
|
}
|
|
50
|
-
}, [
|
|
89
|
+
}, [openItem, recentIds]);
|
|
51
90
|
|
|
52
91
|
useEffect(() => {
|
|
53
92
|
inputRef.current?.focus();
|
|
@@ -83,13 +122,13 @@ export function CommandPalette({ close, initialQuery, openSurface, status }: { c
|
|
|
83
122
|
<section aria-label="Command palette" className="command-palette">
|
|
84
123
|
<label className="command-input-row">
|
|
85
124
|
<FiSearch aria-hidden="true" />
|
|
86
|
-
<input aria-activedescendant={matches[activeIndex]?.id} aria-controls="command-palette-results" aria-expanded="true" aria-autocomplete="list" ref={inputRef} onChange={(event) => { setQuery(event.currentTarget.value); setActiveIndex(0); }} placeholder="Search commands, #
|
|
125
|
+
<input aria-activedescendant={matches[activeIndex]?.id} aria-controls="command-palette-results" aria-expanded="true" aria-autocomplete="list" ref={inputRef} onChange={(event) => { setQuery(event.currentTarget.value); setActiveIndex(0); }} placeholder="Search /commands, #channels, _sessions, &infra, ?help, .agents" role="combobox" value={query} />
|
|
87
126
|
</label>
|
|
88
127
|
<ul id="command-palette-results">
|
|
89
128
|
{matches.map((item, index) => (
|
|
90
129
|
<li key={item.id}>
|
|
91
130
|
<button className={index === activeIndex ? "active" : undefined} id={item.id} onClick={() => selectItem(item)} type="button">
|
|
92
|
-
<span className="surface-icon" aria-hidden="true"
|
|
131
|
+
<span className="surface-icon" aria-hidden="true">{item.iconGlyph ?? <SurfaceGlyph icon={item.icon} />}</span>
|
|
93
132
|
<span><strong>{item.label}</strong><small>{item.tag} · {item.description}</small></span>
|
|
94
133
|
</button>
|
|
95
134
|
</li>
|
|
@@ -106,9 +145,11 @@ export function commandPaletteShortcutQuery(event: Pick<KeyboardEvent, "key" | "
|
|
|
106
145
|
return undefined;
|
|
107
146
|
}
|
|
108
147
|
|
|
109
|
-
return new Map(
|
|
148
|
+
return new Map(commandPaletteShortcutEntries).get(event.key);
|
|
110
149
|
}
|
|
111
150
|
|
|
151
|
+
export const commandPaletteShortcutEntries = ["#", "_", "&", "?", ">", "+", "-", "/", "=", "~", "!", ".", "*", ":", "^"].map((shortcut) => [shortcut, shortcut] as const);
|
|
152
|
+
|
|
112
153
|
export function orderCommandItemsByRecency<T extends { id: string }>(items: T[], recentIds: string[]): T[] {
|
|
113
154
|
const recentItems = recentIds
|
|
114
155
|
.map((id) => items.find((item) => item.id === id))
|
|
@@ -124,21 +165,22 @@ export function rememberCommandPaletteItemId(itemId: string, currentIds: string[
|
|
|
124
165
|
return nextIds;
|
|
125
166
|
}
|
|
126
167
|
|
|
127
|
-
function commandPaletteMatches(items: CommandPaletteItem[], query: string, recentIds: string[]): CommandPaletteItem[] {
|
|
168
|
+
export function commandPaletteMatches(items: CommandPaletteItem[], query: string, recentIds: string[]): CommandPaletteItem[] {
|
|
128
169
|
const normalizedQuery = query.trim().toLowerCase();
|
|
129
170
|
const prefix = normalizedQuery.charAt(0);
|
|
130
171
|
const scopedItems = itemsForCommandPrefix(items, prefix);
|
|
172
|
+
const filterQuery = commandPaletteShortcutEntries.some(([shortcut]) => shortcut === prefix) ? normalizedQuery.slice(1).trim() : normalizedQuery;
|
|
131
173
|
|
|
132
|
-
if (normalizedQuery.length === 0 || [
|
|
174
|
+
if (normalizedQuery.length === 0 || commandPaletteShortcutEntries.some(([shortcut]) => shortcut === normalizedQuery)) {
|
|
133
175
|
return orderCommandItemsByRecency(scopedItems, recentIds).slice(0, 8);
|
|
134
176
|
}
|
|
135
177
|
|
|
136
178
|
return scopedItems
|
|
137
|
-
.filter((item) => item.searchText.toLowerCase().includes(
|
|
179
|
+
.filter((item) => item.searchText.toLowerCase().includes(filterQuery))
|
|
138
180
|
.slice(0, 8);
|
|
139
181
|
}
|
|
140
182
|
|
|
141
|
-
function commandPaletteItems(status: GuiStatusData): CommandPaletteItem[] {
|
|
183
|
+
function commandPaletteItems(status: GuiStatusData, activeSurface: SurfaceId): CommandPaletteItem[] {
|
|
142
184
|
const surfaceItems: CommandPaletteItem[] = orderedNavItems.map((item) => ({
|
|
143
185
|
description: item.description,
|
|
144
186
|
icon: item.icon,
|
|
@@ -146,15 +188,26 @@ function commandPaletteItems(status: GuiStatusData): CommandPaletteItem[] {
|
|
|
146
188
|
label: item.label,
|
|
147
189
|
searchText: `${item.label} ${item.description}`,
|
|
148
190
|
surface: item.id,
|
|
149
|
-
tag:
|
|
191
|
+
tag: surfaceTagForItem(item),
|
|
150
192
|
}));
|
|
193
|
+
const helpItems: CommandPaletteItem[] = [
|
|
194
|
+
{ id: "help-shortcuts", label: "? shortcuts", description: "Open command palette and keyboard shortcut help", icon: "help", searchText: "? shortcuts help command palette keyboard", surface: "help", tag: "Help" },
|
|
195
|
+
];
|
|
196
|
+
const currentLinkItems: CommandPaletteItem[] = [
|
|
197
|
+
{ action: "copy-current-link", id: "copy-current-link", label: `^ copy ${activeSurface} link`, description: "Copy a link that can reopen this page in the web or local app", icon: "link", searchText: `^ link copy current page url ${activeSurface} aidevops://surface/${activeSurface}`, surface: activeSurface, tag: "Link" },
|
|
198
|
+
];
|
|
151
199
|
const sessionItems: CommandPaletteItem[] = status.opencode_sessions.sessions.map((session) => ({
|
|
152
200
|
description: `AI session in ${session.repo_path_ref}`,
|
|
153
201
|
icon: "terminal",
|
|
202
|
+
iconGlyph: "_",
|
|
154
203
|
id: `session-${session.id_ref}`,
|
|
155
|
-
label:
|
|
156
|
-
|
|
204
|
+
label: `_${session.title}`,
|
|
205
|
+
repoPathRef: session.repo_path_ref,
|
|
206
|
+
searchText: `_${session.title} ${session.title} ${session.repo_path_ref} ${session.agent} ${session.model}`,
|
|
207
|
+
sessionId: session.id_ref,
|
|
157
208
|
surface: "git",
|
|
209
|
+
shellMode: "sessions",
|
|
210
|
+
conversationMode: "ai",
|
|
158
211
|
tag: "AI session",
|
|
159
212
|
}));
|
|
160
213
|
const directMessageItems: CommandPaletteItem[] = [{
|
|
@@ -167,20 +220,60 @@ function commandPaletteItems(status: GuiStatusData): CommandPaletteItem[] {
|
|
|
167
220
|
tag: "Direct message",
|
|
168
221
|
}];
|
|
169
222
|
|
|
170
|
-
return [...surfaceItems, ...sessionItems, ...plannedChannelItems, ...directMessageItems, ...slashCommandItems];
|
|
223
|
+
return [...surfaceItems, ...helpItems, ...sessionItems, ...plannedChannelItems, ...directMessageItems, ...slashCommandItems, ...addCommandItems, ...removeCommandItems, ...terminalCommandItems, ...emojiItems, ...currentLinkItems];
|
|
171
224
|
}
|
|
172
225
|
|
|
173
226
|
function itemsForCommandPrefix(items: CommandPaletteItem[], prefix: string): CommandPaletteItem[] {
|
|
174
227
|
const tagsByPrefix = new Map<string, string[]>([
|
|
175
228
|
["/", ["Command"]],
|
|
176
229
|
["@", ["Direct message"]],
|
|
177
|
-
["#", ["
|
|
230
|
+
["#", ["Channel"]],
|
|
231
|
+
["_", ["AI session"]],
|
|
232
|
+
["&", ["Infrastructure"]],
|
|
233
|
+
["?", ["Help"]],
|
|
234
|
+
[">", ["Terminal"]],
|
|
235
|
+
["+", ["Add"]],
|
|
236
|
+
["-", ["Remove"]],
|
|
237
|
+
["=", ["Comms"]],
|
|
238
|
+
["~", ["Settings"]],
|
|
239
|
+
["!", ["Notifications"]],
|
|
240
|
+
[".", ["Agent"]],
|
|
241
|
+
["*", ["Secret"]],
|
|
242
|
+
[":", ["Emoji"]],
|
|
243
|
+
["^", ["Link"]],
|
|
178
244
|
]);
|
|
179
245
|
const allowedTags = tagsByPrefix.get(prefix) ?? ["Surface"];
|
|
180
246
|
|
|
181
247
|
return items.filter((item) => allowedTags.includes(item.tag));
|
|
182
248
|
}
|
|
183
249
|
|
|
250
|
+
function surfaceTagForItem(item: SurfaceNavItem): string {
|
|
251
|
+
const explicitTags: Partial<Record<SurfaceId, string>> = {
|
|
252
|
+
agents: "Agent",
|
|
253
|
+
config: "Settings",
|
|
254
|
+
help: "Help",
|
|
255
|
+
notifications: "Notifications",
|
|
256
|
+
projectConfig: "Settings",
|
|
257
|
+
security: "Secret",
|
|
258
|
+
settings: "Settings",
|
|
259
|
+
vault: "Secret",
|
|
260
|
+
};
|
|
261
|
+
const infrastructureIds = navGroups.find((group) => group.label === "Infrastructure")?.items.map((groupItem) => groupItem.id) ?? [];
|
|
262
|
+
const commsIds = navGroups.filter((group) => group.mode === "comms").flatMap((group) => group.items.map((groupItem) => groupItem.id));
|
|
263
|
+
const utilityFallback = utilityNavItems.some((utilityItem) => utilityItem.id === item.id) ? "Surface" : undefined;
|
|
264
|
+
|
|
265
|
+
return explicitTags[item.id] ?? (infrastructureIds.includes(item.id) ? "Infrastructure" : undefined) ?? (commsIds.includes(item.id) ? "Comms" : undefined) ?? utilityFallback ?? "Surface";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function copyCurrentSurfaceLink(surface: SurfaceId): Promise<void> {
|
|
269
|
+
const browserUrl = typeof window === "undefined" ? "" : window.location.href.split("#")[0];
|
|
270
|
+
const link = browserUrl.length > 0 ? `${browserUrl}#surface=${surface}` : `aidevops://surface/${surface}`;
|
|
271
|
+
|
|
272
|
+
if (typeof navigator !== "undefined" && navigator.clipboard) {
|
|
273
|
+
await navigator.clipboard.writeText(link);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
184
277
|
function readCommandPaletteRecentIds(): string[] {
|
|
185
278
|
try {
|
|
186
279
|
const storedValue = window.localStorage.getItem(commandPaletteRecentStorageKey);
|
|
@@ -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}>
|
|
@@ -32,6 +32,8 @@ export const surfaceIconNames = {
|
|
|
32
32
|
globe: true,
|
|
33
33
|
grid: true,
|
|
34
34
|
hardDrive: true,
|
|
35
|
+
hash: true,
|
|
36
|
+
help: true,
|
|
35
37
|
link: true,
|
|
36
38
|
list: true,
|
|
37
39
|
lock: true,
|
|
@@ -48,6 +50,7 @@ export const surfaceIconNames = {
|
|
|
48
50
|
export type SurfaceIconName = keyof typeof surfaceIconNames;
|
|
49
51
|
export const surfaceIds = [
|
|
50
52
|
"overview",
|
|
53
|
+
"help",
|
|
51
54
|
"settings",
|
|
52
55
|
"notifications",
|
|
53
56
|
"admin",
|
|
@@ -138,7 +141,7 @@ export const text = {
|
|
|
138
141
|
aidevops: "aidevops",
|
|
139
142
|
appShell: "App shell",
|
|
140
143
|
apps: "Apps",
|
|
141
|
-
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.",
|
|
142
145
|
appStatusPending: "adapter pending",
|
|
143
146
|
brands: "Brands",
|
|
144
147
|
brandsIntro: "Draft brand inventory for names and website references.",
|
|
@@ -147,6 +150,8 @@ export const text = {
|
|
|
147
150
|
configIntro: "Read-only file explorer for ~/.config/aidevops. Contents stay hidden until redaction rules land.",
|
|
148
151
|
copyPath: "Copy path",
|
|
149
152
|
dashboard: "Dashboard",
|
|
153
|
+
help: "Help",
|
|
154
|
+
helpIntro: "Command shortcuts, navigation hints, and aidevops workflow guidance will collect here.",
|
|
150
155
|
domains: "Domains",
|
|
151
156
|
domainsIntro: "Draft domain ownership inventory across providers.",
|
|
152
157
|
draftOnly: "Local browser draft only. Saving requires a write-action manifest, confirmation, and audit trail.",
|
|
@@ -396,6 +401,7 @@ export const dashboardNavItem: SurfaceNavItem = {
|
|
|
396
401
|
};
|
|
397
402
|
|
|
398
403
|
export const utilityNavItems: SurfaceNavItem[] = [
|
|
404
|
+
{ id: "help", label: text.help, description: "Shortcuts, command palette symbols, and support", icon: "help", badge: text.planned },
|
|
399
405
|
{ id: "settings", label: "Settings", description: "Profile, login, notifications, language, and theme preferences", icon: "settings", badge: text.planned },
|
|
400
406
|
{ id: "notifications", label: "Notifications", description: "Notification inbox and delivery preferences", icon: "message", badge: text.planned },
|
|
401
407
|
{ id: "admin", label: "Admin", description: "Hosted administration controls", icon: "shield", badge: text.planned },
|
|
@@ -414,15 +420,6 @@ export const fileRootBySurface: Partial<Record<SurfaceId, GuiFileRootId>> = {
|
|
|
414
420
|
git: "git",
|
|
415
421
|
};
|
|
416
422
|
|
|
417
|
-
export const appRows = [
|
|
418
|
-
{ name: "aidevops", latest: text.appStatusPending, channel: "setup/update", website: "https://aidevops.sh" },
|
|
419
|
-
{ name: "OpenCode", latest: text.appStatusPending, channel: "runtime", website: text.appStatusPending },
|
|
420
|
-
{ name: "Bun", latest: text.appStatusPending, channel: "toolchain", website: text.appStatusPending },
|
|
421
|
-
{ name: "GitHub CLI", latest: text.appStatusPending, channel: "git", website: text.appStatusPending },
|
|
422
|
-
{ name: "ShellCheck", latest: text.appStatusPending, channel: "quality", website: text.appStatusPending },
|
|
423
|
-
{ name: "Biome", latest: text.appStatusPending, channel: "quality", website: text.appStatusPending },
|
|
424
|
-
];
|
|
425
|
-
|
|
426
423
|
export const installationRows = [
|
|
427
424
|
{ name: "OpenCode runtime", install: true, update: true, scope: "core" },
|
|
428
425
|
{ name: "GUI desktop launcher", install: true, update: true, scope: "local" },
|
|
@@ -576,7 +573,7 @@ export function surfaceRecordCounts(status: GuiStatusData): SurfaceRecordCounts
|
|
|
576
573
|
return {
|
|
577
574
|
agents: status.capabilities.length,
|
|
578
575
|
aiProviders: oauthAccounts,
|
|
579
|
-
apps:
|
|
576
|
+
apps: status.managed_apps.length || status.ai_apps.length,
|
|
580
577
|
config: status.settings.key_count || status.settings.keys.length,
|
|
581
578
|
git: status.local_repos.total || status.local_repos.repos.length,
|
|
582
579
|
hosts: populatedInventoryRowCount("hosts"),
|
|
@@ -38,7 +38,7 @@ export function renderDashboardHtml(status: GuiResponseEnvelope<GuiStatusData>):
|
|
|
38
38
|
.join("");
|
|
39
39
|
const navSections = [
|
|
40
40
|
{ heading: "Development", items: ["Local Repos", "Remote Repos", "Secrets", "AI Providers"] },
|
|
41
|
-
{ heading: "Account", items: ["Settings", "Notifications", "Admin"] },
|
|
41
|
+
{ heading: "Account", items: ["Help", "Settings", "Notifications", "Admin"] },
|
|
42
42
|
{ heading: "Operations", items: ["Dashboard", "Vault", "Agents file explorer", "Config", "Local Setup", "Routines"] },
|
|
43
43
|
{ heading: "Infrastructure", items: ["Devices", "VPNs & Proxies", "Apps", "Installation", "Registrars", "Hosts", "Servers"] },
|
|
44
44
|
{ heading: "Identities", items: ["Brands", "Domains", "Personas"] },
|
|
@@ -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,
|