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.
@@ -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 }: { conversationMode?: ConversationMode; shellMode?: ShellMode; surface: SurfaceId }) => {
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(normalizedQuery))
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 { type ReactElement, useState } from "react";
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 { appRows, installationRows, text } from "./app-model";
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="data-table">
21
- <div className="data-row header-row"><span>{text.name}</span><span>{text.latest}</span><span>{text.channel}</span><span>{text.website}</span></div>
22
- {appRows.map((row) => (
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. Version and homepage adapters are planned.",
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: appRows.length,
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,