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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type GuiRouteClassification = "read" | "write" | "destructive";
|
|
2
2
|
|
|
3
|
-
export type GuiOperationId = "setup.status.read" | "capabilities.read" | "filesystem.read" | "vault.status.read";
|
|
3
|
+
export type GuiOperationId = "setup.status.read" | "capabilities.read" | "filesystem.read" | "vault.status.read" | "apps.action.run" | "apps.action.status";
|
|
4
4
|
|
|
5
5
|
export type GuiFileRootId = "agents" | "config" | "localSetup" | "git";
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ export interface GuiResponseEnvelope<TData> {
|
|
|
23
23
|
|
|
24
24
|
export interface GuiRouteManifest {
|
|
25
25
|
route: string;
|
|
26
|
-
method: "GET";
|
|
26
|
+
method: "GET" | "POST";
|
|
27
27
|
operation_id: GuiOperationId;
|
|
28
28
|
classification: GuiRouteClassification;
|
|
29
29
|
source_surface: string;
|
|
@@ -68,6 +68,44 @@ export interface GuiFileExplorerData {
|
|
|
68
68
|
entry_limit: number;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export type GuiAppActionId = "install" | "update" | "reinstall" | "remove";
|
|
72
|
+
|
|
73
|
+
export interface GuiManagedAppActionSummary {
|
|
74
|
+
id: GuiAppActionId;
|
|
75
|
+
label: string;
|
|
76
|
+
enabled: boolean;
|
|
77
|
+
command_preview: string;
|
|
78
|
+
confirmation: "none" | "recommended" | "required";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface GuiManagedAppSummary {
|
|
82
|
+
id: string;
|
|
83
|
+
name: string;
|
|
84
|
+
description: string;
|
|
85
|
+
category: string;
|
|
86
|
+
origin_website_url: string;
|
|
87
|
+
origin_repo_url: string;
|
|
88
|
+
aidevops_install: boolean;
|
|
89
|
+
aidevops_update: boolean;
|
|
90
|
+
installed_version: string;
|
|
91
|
+
latest_version: string;
|
|
92
|
+
install_path_ref: string;
|
|
93
|
+
status: "found" | "missing" | "unchecked";
|
|
94
|
+
actions: GuiManagedAppActionSummary[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface GuiAppActionJobSummary {
|
|
98
|
+
id: string;
|
|
99
|
+
app_id: string;
|
|
100
|
+
action: GuiAppActionId;
|
|
101
|
+
status: "running" | "completed" | "failed" | "rejected";
|
|
102
|
+
command_preview: string;
|
|
103
|
+
started_at: string;
|
|
104
|
+
finished_at: string | null;
|
|
105
|
+
exit_code: number | null;
|
|
106
|
+
output: string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
71
109
|
export interface GuiSecretReference {
|
|
72
110
|
name: string;
|
|
73
111
|
status: "configured" | "missing" | "unchecked";
|
|
@@ -318,6 +356,7 @@ export interface GuiStatusData {
|
|
|
318
356
|
oauth_pool: GuiOAuthPoolSummary;
|
|
319
357
|
setup_targets: GuiSetupTargetSummary[];
|
|
320
358
|
ai_apps: GuiAiAppSummary[];
|
|
359
|
+
managed_apps: GuiManagedAppSummary[];
|
|
321
360
|
vault: GuiVaultStatusData;
|
|
322
361
|
capabilities: GuiCapabilitySummary[];
|
|
323
362
|
secrets: GuiSecretReference[];
|
|
@@ -357,6 +396,28 @@ export const VAULT_STATUS_ROUTE_MANIFEST: GuiRouteManifest = {
|
|
|
357
396
|
redactions: ["secret_values", "credential_paths", "private_key_material", "vault_passphrases", "recovery_material"],
|
|
358
397
|
};
|
|
359
398
|
|
|
399
|
+
export const APP_ACTION_ROUTE_MANIFEST: GuiRouteManifest = {
|
|
400
|
+
route: "/api/apps/:appId/actions/:action",
|
|
401
|
+
method: "POST",
|
|
402
|
+
operation_id: "apps.action.run",
|
|
403
|
+
classification: "write",
|
|
404
|
+
source_surface: "apps",
|
|
405
|
+
adapter: "appActions.startAppAction",
|
|
406
|
+
command_pattern: ["allowlisted", "aidevops", "setup/update", "background-job"],
|
|
407
|
+
redactions: ["secret_values", "credential_paths", "private_key_material"],
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
export const APP_ACTION_STATUS_ROUTE_MANIFEST: GuiRouteManifest = {
|
|
411
|
+
route: "/api/apps/jobs/:jobId",
|
|
412
|
+
method: "GET",
|
|
413
|
+
operation_id: "apps.action.status",
|
|
414
|
+
classification: "read",
|
|
415
|
+
source_surface: "apps",
|
|
416
|
+
adapter: "appActions.readAppActionJob",
|
|
417
|
+
command_pattern: ["background-job", "metadata-and-output-only"],
|
|
418
|
+
redactions: ["secret_values", "credential_paths", "private_key_material"],
|
|
419
|
+
};
|
|
420
|
+
|
|
360
421
|
export const GUI_FILE_ROOTS: readonly GuiFileRootDefinition[] = [
|
|
361
422
|
{
|
|
362
423
|
id: "agents",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GuiResponseEnvelope, GuiStatusData } from "@aidevops/gui-shared";
|
|
2
|
-
import { type ReactElement, useEffect, useState } from "react";
|
|
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
5
|
import type { ConversationMode, FontPreference, FontSizePreference, ShellMode, SurfaceId, ThemePreference } from "./app-model";
|
|
@@ -32,9 +32,14 @@ export const appearanceStorageKeys = {
|
|
|
32
32
|
machineRail: "aidevops-gui-show-machine-rail",
|
|
33
33
|
showBorders: "aidevops-gui-show-borders",
|
|
34
34
|
showNavCounts: "aidevops-gui-show-nav-counts",
|
|
35
|
+
sidebarWidth: "aidevops-gui-sidebar-width",
|
|
35
36
|
theme: "aidevops-gui-theme",
|
|
36
37
|
} as const;
|
|
37
38
|
|
|
39
|
+
const defaultSidebarWidth = 302;
|
|
40
|
+
const minSidebarWidth = 248;
|
|
41
|
+
const maxSidebarWidth = 520;
|
|
42
|
+
|
|
38
43
|
export interface StoredAppearancePreferences {
|
|
39
44
|
accentHue: number;
|
|
40
45
|
fontPreference: FontPreference;
|
|
@@ -76,6 +81,7 @@ export function App(): ReactElement {
|
|
|
76
81
|
const [shellMode, setShellMode] = useState<ShellMode>("devices");
|
|
77
82
|
const [conversationMode, setConversationMode] = useState<ConversationMode>("ai");
|
|
78
83
|
const [selectedLocalRepoIndex, setSelectedLocalRepoIndex] = useState(0);
|
|
84
|
+
const [selectedSessionId, setSelectedSessionId] = useState<string | undefined>();
|
|
79
85
|
const [systemTheme, setSystemTheme] = useState<"light" | "dark">("light");
|
|
80
86
|
const resolvedTheme = themePreference === "system" ? systemTheme : themePreference;
|
|
81
87
|
const activeSurface: SurfaceId = navigation.entries[navigation.index] ?? "overview";
|
|
@@ -184,12 +190,14 @@ export function App(): ReactElement {
|
|
|
184
190
|
fontSizePreference={fontSizePreference}
|
|
185
191
|
fontPreference={fontPreference}
|
|
186
192
|
selectedLocalRepoIndex={selectedLocalRepoIndex}
|
|
193
|
+
selectedSessionId={selectedSessionId}
|
|
187
194
|
setAccentHue={setAccentHue}
|
|
188
195
|
setActiveSurface={setActiveSurface}
|
|
189
196
|
setConversationMode={setConversationMode}
|
|
190
197
|
setFontSizePreference={setFontSizePreference}
|
|
191
198
|
setFontPreference={setFontPreference}
|
|
192
199
|
setSelectedLocalRepoIndex={setSelectedLocalRepoIndex}
|
|
200
|
+
setSelectedSessionId={setSelectedSessionId}
|
|
193
201
|
setShellMode={setShellMode}
|
|
194
202
|
setShowBorders={setShowBorders}
|
|
195
203
|
setShowNavCounts={setShowNavCounts}
|
|
@@ -200,6 +208,7 @@ export function App(): ReactElement {
|
|
|
200
208
|
status={status.data}
|
|
201
209
|
themePreference={themePreference}
|
|
202
210
|
/>
|
|
211
|
+
<SidebarResizeHandle />
|
|
203
212
|
<Workspace
|
|
204
213
|
activeItem={activeItem}
|
|
205
214
|
activeSectionLabel={activeSectionLabel}
|
|
@@ -211,7 +220,12 @@ export function App(): ReactElement {
|
|
|
211
220
|
goBack={goBack}
|
|
212
221
|
goForward={goForward}
|
|
213
222
|
selectedLocalRepoIndex={selectedLocalRepoIndex}
|
|
223
|
+
selectedSessionId={selectedSessionId}
|
|
214
224
|
setActiveSurface={setActiveSurface}
|
|
225
|
+
setConversationMode={setConversationMode}
|
|
226
|
+
setSelectedLocalRepoIndex={setSelectedLocalRepoIndex}
|
|
227
|
+
setSelectedSessionId={setSelectedSessionId}
|
|
228
|
+
setShellMode={setShellMode}
|
|
215
229
|
shellMode={shellMode}
|
|
216
230
|
status={status.data}
|
|
217
231
|
/>
|
|
@@ -220,6 +234,54 @@ export function App(): ReactElement {
|
|
|
220
234
|
);
|
|
221
235
|
}
|
|
222
236
|
|
|
237
|
+
export function clampSidebarWidth(width: number): number {
|
|
238
|
+
return Math.min(maxSidebarWidth, Math.max(minSidebarWidth, Math.round(width)));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function SidebarResizeHandle(): ReactElement {
|
|
242
|
+
const [sidebarWidth, setSidebarWidth] = useState(() => readStoredSidebarWidth());
|
|
243
|
+
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
document.documentElement.style.setProperty("--sidebar-width", `${sidebarWidth}px`);
|
|
246
|
+
persistAppearancePreference(appearanceStorageKeys.sidebarWidth, String(sidebarWidth));
|
|
247
|
+
}, [sidebarWidth]);
|
|
248
|
+
|
|
249
|
+
const startSidebarResize = (event: ReactMouseEvent<HTMLElement>) => {
|
|
250
|
+
const startX = event.clientX;
|
|
251
|
+
const startWidth = sidebarWidth;
|
|
252
|
+
const resizeSidebar = (moveEvent: MouseEvent) => setSidebarWidth(clampSidebarWidth(startWidth + moveEvent.clientX - startX));
|
|
253
|
+
const stopResize = () => {
|
|
254
|
+
document.documentElement.classList.remove("resizing-sidebar");
|
|
255
|
+
window.removeEventListener("mousemove", resizeSidebar);
|
|
256
|
+
window.removeEventListener("mouseup", stopResize);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
document.documentElement.classList.add("resizing-sidebar");
|
|
261
|
+
window.addEventListener("mousemove", resizeSidebar);
|
|
262
|
+
window.addEventListener("mouseup", stopResize);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const resizeSidebarWithKeyboard = (event: ReactKeyboardEvent<HTMLElement>) => {
|
|
266
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
267
|
+
event.preventDefault();
|
|
268
|
+
setSidebarWidth((current) => clampSidebarWidth(current + (event.key === "ArrowRight" ? 16 : -16)));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (event.key === "Home" || event.key === "End") {
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
setSidebarWidth(event.key === "Home" ? minSidebarWidth : maxSidebarWidth);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return <hr aria-label="Resize sidebar" aria-orientation="vertical" aria-valuemax={maxSidebarWidth} aria-valuemin={minSidebarWidth} aria-valuenow={sidebarWidth} className="sidebar-resize-handle" onKeyDown={resizeSidebarWithKeyboard} onMouseDown={startSidebarResize} tabIndex={0} />;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function readStoredSidebarWidth(storage: ReadableAppearanceStorage | undefined = browserLocalStorage()): number {
|
|
281
|
+
const savedWidth = Number.parseInt(storage?.getItem(appearanceStorageKeys.sidebarWidth) ?? "", 10);
|
|
282
|
+
return Number.isFinite(savedWidth) ? clampSidebarWidth(savedWidth) : defaultSidebarWidth;
|
|
283
|
+
}
|
|
284
|
+
|
|
223
285
|
function sendNativeAccentHue(hue: number): void {
|
|
224
286
|
const webkit = (window as WebKitBridgeWindow).webkit;
|
|
225
287
|
webkit?.messageHandlers?.accentHue?.postMessage(hue);
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
FiGrid,
|
|
21
21
|
FiHash,
|
|
22
22
|
FiHardDrive,
|
|
23
|
+
FiHelpCircle,
|
|
23
24
|
FiLink,
|
|
24
25
|
FiLink2,
|
|
25
26
|
FiList,
|
|
@@ -54,6 +55,8 @@ const surfaceIcons: Record<SurfaceIconName, IconType> = {
|
|
|
54
55
|
globe: FiGlobe,
|
|
55
56
|
grid: FiGrid,
|
|
56
57
|
hardDrive: FiHardDrive,
|
|
58
|
+
hash: FiHash,
|
|
59
|
+
help: FiHelpCircle,
|
|
57
60
|
link: FiLink,
|
|
58
61
|
list: FiList,
|
|
59
62
|
lock: FiLock,
|
|
@@ -107,19 +110,21 @@ export function MachineRail({ machine }: { machine?: GuiMachineSummary }) {
|
|
|
107
110
|
);
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
export function Sidebar({ activeSurface, accentHue, conversationMode, fontPreference, fontSizePreference, selectedLocalRepoIndex, setAccentHue, setActiveSurface, setConversationMode, setFontPreference, setFontSizePreference, setSelectedLocalRepoIndex, setShellMode, setShowBorders, setShowNavCounts, setThemePreference, shellMode, showBorders, showNavCounts, status, themePreference }: {
|
|
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 }: {
|
|
111
114
|
activeSurface: SurfaceId;
|
|
112
115
|
accentHue: number;
|
|
113
116
|
conversationMode: ConversationMode;
|
|
114
117
|
fontPreference: FontPreference;
|
|
115
118
|
fontSizePreference: FontSizePreference;
|
|
116
119
|
selectedLocalRepoIndex: number;
|
|
120
|
+
selectedSessionId: string | undefined;
|
|
117
121
|
setAccentHue: (hue: number) => void;
|
|
118
122
|
setActiveSurface: (surface: SurfaceId) => void;
|
|
119
123
|
setConversationMode: (mode: ConversationMode) => void;
|
|
120
124
|
setFontPreference: (font: FontPreference) => void;
|
|
121
125
|
setFontSizePreference: (size: FontSizePreference) => void;
|
|
122
126
|
setSelectedLocalRepoIndex: (index: number) => void;
|
|
127
|
+
setSelectedSessionId: (id: string | undefined) => void;
|
|
123
128
|
setShellMode: (mode: ShellMode) => void;
|
|
124
129
|
setShowBorders: (show: boolean) => void;
|
|
125
130
|
setShowNavCounts: (show: boolean) => void;
|
|
@@ -151,8 +156,10 @@ export function Sidebar({ activeSurface, accentHue, conversationMode, fontPrefer
|
|
|
151
156
|
</> : <ConversationSidebar
|
|
152
157
|
conversationMode={conversationMode}
|
|
153
158
|
selectedLocalRepoIndex={selectedLocalRepoIndex}
|
|
159
|
+
selectedSessionId={selectedSessionId}
|
|
154
160
|
setConversationMode={setConversationMode}
|
|
155
161
|
setSelectedLocalRepoIndex={setSelectedLocalRepoIndex}
|
|
162
|
+
setSelectedSessionId={setSelectedSessionId}
|
|
156
163
|
status={status}
|
|
157
164
|
/>}
|
|
158
165
|
</nav>
|
|
@@ -200,11 +207,13 @@ function IconSwitch<TValue extends string>({ ariaLabel, options, value }: {
|
|
|
200
207
|
);
|
|
201
208
|
}
|
|
202
209
|
|
|
203
|
-
function ConversationSidebar({ conversationMode, selectedLocalRepoIndex, setConversationMode, setSelectedLocalRepoIndex, status }: {
|
|
210
|
+
function ConversationSidebar({ conversationMode, selectedLocalRepoIndex, selectedSessionId, setConversationMode, setSelectedLocalRepoIndex, setSelectedSessionId, status }: {
|
|
204
211
|
conversationMode: ConversationMode;
|
|
205
212
|
selectedLocalRepoIndex: number;
|
|
213
|
+
selectedSessionId: string | undefined;
|
|
206
214
|
setConversationMode: (mode: ConversationMode) => void;
|
|
207
215
|
setSelectedLocalRepoIndex: (index: number) => void;
|
|
216
|
+
setSelectedSessionId: (id: string | undefined) => void;
|
|
208
217
|
status: GuiStatusData;
|
|
209
218
|
}) {
|
|
210
219
|
const repos = status.local_repos.repos;
|
|
@@ -212,6 +221,7 @@ function ConversationSidebar({ conversationMode, selectedLocalRepoIndex, setConv
|
|
|
212
221
|
const sessions = selectedRepo === undefined
|
|
213
222
|
? []
|
|
214
223
|
: status.opencode_sessions.sessions.filter((session) => session.repo_path_ref === selectedRepo.path_ref);
|
|
224
|
+
const activeSessionId = sessions.some((session) => session.id_ref === selectedSessionId) ? selectedSessionId : sessions[0]?.id_ref;
|
|
215
225
|
const canSelectPreviousRepo = selectedLocalRepoIndex > 0;
|
|
216
226
|
const canSelectNextRepo = selectedLocalRepoIndex < repos.length - 1;
|
|
217
227
|
|
|
@@ -223,19 +233,18 @@ function ConversationSidebar({ conversationMode, selectedLocalRepoIndex, setConv
|
|
|
223
233
|
<section className="repo-session-selector" aria-label={text.localRepoSelector}>
|
|
224
234
|
<span className="repo-selector-heading" id="local-repo-selector-label">{text.localRepos}</span>
|
|
225
235
|
<div className="selector-with-stepper repo-selector-row">
|
|
226
|
-
<button aria-label="Previous local repo" className="selector-step-button" disabled={!canSelectPreviousRepo} onClick={() => setSelectedLocalRepoIndex(Math.max(0, selectedLocalRepoIndex - 1))} type="button"><FiChevronLeft aria-hidden="true" /></button>
|
|
227
|
-
<select aria-labelledby="local-repo-selector-label" onChange={(event) => setSelectedLocalRepoIndex(Number.parseInt(event.currentTarget.value, 10))} value={Math.min(selectedLocalRepoIndex, Math.max(0, repos.length - 1))}>
|
|
236
|
+
<button aria-label="Previous local repo" className="selector-step-button" disabled={!canSelectPreviousRepo} onClick={() => { setSelectedLocalRepoIndex(Math.max(0, selectedLocalRepoIndex - 1)); setSelectedSessionId(undefined); }} type="button"><FiChevronLeft aria-hidden="true" /></button>
|
|
237
|
+
<select aria-labelledby="local-repo-selector-label" onChange={(event) => { setSelectedLocalRepoIndex(Number.parseInt(event.currentTarget.value, 10)); setSelectedSessionId(undefined); }} value={Math.min(selectedLocalRepoIndex, Math.max(0, repos.length - 1))}>
|
|
228
238
|
{repos.length === 0 ? <option value={0}>No local repos</option> : repos.map((repo, index) => <option key={repo.path_ref} value={index}>{repo.name}</option>)}
|
|
229
239
|
</select>
|
|
230
|
-
<button aria-label="Next local repo" className="selector-step-button" disabled={!canSelectNextRepo} onClick={() => setSelectedLocalRepoIndex(Math.min(repos.length - 1, selectedLocalRepoIndex + 1))} type="button"><FiChevronRight aria-hidden="true" /></button>
|
|
240
|
+
<button aria-label="Next local repo" className="selector-step-button" disabled={!canSelectNextRepo} onClick={() => { setSelectedLocalRepoIndex(Math.min(repos.length - 1, selectedLocalRepoIndex + 1)); setSelectedSessionId(undefined); }} type="button"><FiChevronRight aria-hidden="true" /></button>
|
|
231
241
|
</div>
|
|
232
242
|
</section>
|
|
233
243
|
<section className="sidebar-group session-history-list">
|
|
234
|
-
<h2>{text.sessionHistory}</h2>
|
|
235
244
|
{selectedRepo ? <ul>
|
|
236
245
|
{sessions.length === 0
|
|
237
246
|
? <li><button className="surface-link active" type="button"><span className="surface-icon" aria-hidden="true"><FiHash /></span><span className="surface-copy"><strong>{selectedRepo.name}</strong><small>No OpenCode sessions found for this repo yet.</small></span><em>{text.planned}</em></button></li>
|
|
238
|
-
: sessions.map((session
|
|
247
|
+
: sessions.map((session) => <li key={session.id_ref}><button className={activeSessionId === session.id_ref ? "surface-link active" : "surface-link"} onClick={() => setSelectedSessionId(session.id_ref)} type="button"><span className="surface-icon" aria-hidden="true"><FiHash /></span><span className="surface-copy"><strong>{session.title}</strong><small>{session.updated_at}</small></span></button></li>)}
|
|
239
248
|
</ul> : <p className="empty-sidebar-state">No local repos discovered.</p>}
|
|
240
249
|
</section>
|
|
241
250
|
</>}
|
|
@@ -248,13 +257,11 @@ function PeopleChannelList() {
|
|
|
248
257
|
<section aria-label="People channels">
|
|
249
258
|
<div className="notice compact-notice">{text.simplexReady}</div>
|
|
250
259
|
<section className="sidebar-group session-history-list">
|
|
251
|
-
<h2>{text.teams}</h2>
|
|
252
260
|
<ul>
|
|
253
261
|
{["aidevops", "clients", "ops"].map((team) => <li key={team}><button className="surface-link" type="button"><span className="surface-icon" aria-hidden="true"><FiHash /></span><span className="surface-copy"><strong>{team}</strong><small>SimpleX team channel placeholder</small></span></button></li>)}
|
|
254
262
|
</ul>
|
|
255
263
|
</section>
|
|
256
264
|
<section className="sidebar-group session-history-list">
|
|
257
|
-
<h2>{text.directMessages}</h2>
|
|
258
265
|
<ul>
|
|
259
266
|
{["Marcus", "AI DevOps"].map((person) => <li key={person}><button className="surface-link" type="button"><span className="surface-icon" aria-hidden="true"><FiMessageSquare /></span><span className="surface-copy"><strong>{person}</strong><small>encrypted DM placeholder</small></span></button></li>)}
|
|
260
267
|
</ul>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* jshint esversion: 11 */
|
|
2
2
|
import { type ReactElement, type ReactNode, useEffect, useState } from "react";
|
|
3
|
-
import { FiBell, FiChevronLeft, FiChevronRight, FiCommand,
|
|
3
|
+
import { FiBell, FiChevronLeft, FiChevronRight, FiCommand, FiGlobe, FiHash, FiHelpCircle, FiLogOut, FiMessageSquare, FiSearch, FiSettings, FiShield, FiTerminal, FiUser } from "react-icons/fi";
|
|
4
4
|
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, 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,23 +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;
|
|
32
|
+
setConversationMode: (mode: ConversationMode) => void;
|
|
33
|
+
setSelectedLocalRepoIndex: (index: number) => void;
|
|
34
|
+
setSelectedSessionId: (id: string | undefined) => void;
|
|
35
|
+
setShellMode: (mode: ShellMode) => void;
|
|
31
36
|
shellMode: ShellMode;
|
|
32
37
|
status: GuiStatusData;
|
|
33
38
|
}) {
|
|
34
39
|
return (
|
|
35
40
|
<section className="app-inset" aria-label={text.workspaceLabel}>
|
|
36
|
-
<WorkspaceHeader activeItem={activeItem} activeSectionLabel={activeSectionLabel} activeSurface={activeSurface} canGoBack={canGoBack} canGoForward={canGoForward} goBack={goBack} goForward={goForward} setActiveSurface={setActiveSurface} 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} />
|
|
37
42
|
<div className="workspace-scroll">
|
|
38
43
|
{shellMode === "sessions"
|
|
39
|
-
? <ConversationWorkspace conversationMode={conversationMode} selectedLocalRepoIndex={selectedLocalRepoIndex} status={status} />
|
|
44
|
+
? <ConversationWorkspace conversationMode={conversationMode} selectedLocalRepoIndex={selectedLocalRepoIndex} selectedSessionId={selectedSessionId} status={status} />
|
|
40
45
|
: <SurfaceContent activeItem={activeItem} activeSurface={activeSurface} fileRoot={fileRoot} status={status} />}
|
|
41
46
|
</div>
|
|
42
47
|
</section>
|
|
43
48
|
);
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, goBack, goForward, setActiveSurface, status }: {
|
|
51
|
+
function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, goBack, goForward, setActiveSurface, setConversationMode, setSelectedLocalRepoIndex, setSelectedSessionId, setShellMode, status }: {
|
|
47
52
|
activeItem: SurfaceNavItem;
|
|
48
53
|
activeSectionLabel: string;
|
|
49
54
|
activeSurface: SurfaceId;
|
|
@@ -52,6 +57,10 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
|
|
|
52
57
|
goBack: () => void;
|
|
53
58
|
goForward: () => void;
|
|
54
59
|
setActiveSurface: (surface: SurfaceId) => void;
|
|
60
|
+
setConversationMode: (mode: ConversationMode) => void;
|
|
61
|
+
setSelectedLocalRepoIndex: (index: number) => void;
|
|
62
|
+
setSelectedSessionId: (id: string | undefined) => void;
|
|
63
|
+
setShellMode: (mode: ShellMode) => void;
|
|
55
64
|
status: GuiStatusData;
|
|
56
65
|
}): ReactElement {
|
|
57
66
|
const [assistantOpen, setAssistantOpen] = useState(false);
|
|
@@ -64,31 +73,35 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
|
|
|
64
73
|
|
|
65
74
|
useEffect(() => {
|
|
66
75
|
const openCommandPalette = (event: KeyboardEvent) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (shortcutQuery !== undefined && !isEditableShortcutTarget(event.target)) {
|
|
76
|
-
event.preventDefault();
|
|
77
|
-
setCommandInitialQuery(shortcutQuery);
|
|
78
|
-
setCommandOpen(true);
|
|
79
|
-
}
|
|
76
|
+
handleWorkspaceShortcut(event, {
|
|
77
|
+
goBack,
|
|
78
|
+
goForward,
|
|
79
|
+
openCommand: (query) => {
|
|
80
|
+
setCommandInitialQuery(query);
|
|
81
|
+
setCommandOpen(true);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
80
84
|
};
|
|
81
85
|
|
|
82
86
|
window.addEventListener("keydown", openCommandPalette);
|
|
83
87
|
return () => window.removeEventListener("keydown", openCommandPalette);
|
|
84
|
-
}, []);
|
|
88
|
+
}, [goBack, goForward]);
|
|
85
89
|
|
|
86
90
|
const closeMenus = () => {
|
|
87
91
|
setNotificationsOpen(false);
|
|
88
92
|
setProfileOpen(false);
|
|
89
93
|
};
|
|
90
94
|
|
|
91
|
-
const
|
|
95
|
+
const openItem = ({ conversationMode, repoPathRef, sessionId, shellMode, surface }: CommandPaletteSelection) => {
|
|
96
|
+
const repoIndex = status.local_repos.repos.findIndex((repo) => repo.path_ref === repoPathRef);
|
|
97
|
+
setShellMode(shellMode ?? "devices");
|
|
98
|
+
if (repoIndex >= 0) {
|
|
99
|
+
setSelectedLocalRepoIndex(repoIndex);
|
|
100
|
+
}
|
|
101
|
+
setSelectedSessionId(sessionId);
|
|
102
|
+
if (conversationMode) {
|
|
103
|
+
setConversationMode(conversationMode);
|
|
104
|
+
}
|
|
92
105
|
setActiveSurface(surface);
|
|
93
106
|
closeMenus();
|
|
94
107
|
setCommandOpen(false);
|
|
@@ -116,28 +129,45 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
|
|
|
116
129
|
</div>
|
|
117
130
|
<div className="header-actions">
|
|
118
131
|
<div className="header-action-menu">
|
|
132
|
+
<button aria-label="Open help" className="header-icon-button" onClick={() => openItem({ surface: "help" })} title="Help (?)" type="button">
|
|
133
|
+
<FiHelpCircle aria-hidden="true" />
|
|
134
|
+
</button>
|
|
119
135
|
<button aria-expanded={notificationsOpen} aria-label="Open notifications" className="header-icon-button" onClick={() => { setNotificationsOpen((current) => !current); setProfileOpen(false); }} type="button">
|
|
120
136
|
<FiBell aria-hidden="true" />
|
|
121
137
|
<span className="notification-dot" aria-hidden="true" />
|
|
122
138
|
</button>
|
|
123
|
-
{notificationsOpen ? <NotificationsMenu openSurface={
|
|
139
|
+
{notificationsOpen ? <NotificationsMenu openSurface={(surface) => openItem({ surface })} /> : null}
|
|
124
140
|
</div>
|
|
125
|
-
<button aria-pressed={assistantOpen} aria-label="Toggle AI Assistant" className={assistantOpen ? "header-icon-button active" : "header-icon-button"} onClick={() => setAssistantOpen((current) => !current)} type="button">
|
|
126
|
-
<
|
|
141
|
+
<button aria-pressed={assistantOpen} aria-label="Toggle AI Assistant" className={assistantOpen ? "header-icon-button active" : "header-icon-button"} onClick={() => setAssistantOpen((current) => !current)} title="AI sessions (_)" type="button">
|
|
142
|
+
<FiTerminal aria-hidden="true" />
|
|
127
143
|
</button>
|
|
128
144
|
<div className="header-action-menu">
|
|
129
145
|
<button aria-expanded={profileOpen} aria-label={`Open profile menu for ${userName}`} className="profile-avatar-button" onClick={() => { setProfileOpen((current) => !current); setNotificationsOpen(false); }} title={userName} type="button">
|
|
130
146
|
<span>{userInitials}</span>
|
|
131
147
|
</button>
|
|
132
|
-
{profileOpen ? <ProfileMenu openSurface={
|
|
148
|
+
{profileOpen ? <ProfileMenu openSurface={(surface) => openItem({ surface })} userName={userName} /> : null}
|
|
133
149
|
</div>
|
|
134
150
|
</div>
|
|
135
151
|
{assistantOpen ? <AssistantPanel activeSurface={activeSurface} userName={userName} /> : null}
|
|
136
|
-
{commandOpen ? <CommandPalette close={() => setCommandOpen(false)} initialQuery={commandInitialQuery}
|
|
152
|
+
{commandOpen ? <CommandPalette activeSurface={activeSurface} close={() => setCommandOpen(false)} initialQuery={commandInitialQuery} openItem={openItem} status={status} /> : null}
|
|
137
153
|
</header>
|
|
138
154
|
);
|
|
139
155
|
}
|
|
140
156
|
|
|
157
|
+
function handleWorkspaceShortcut(event: KeyboardEvent, actions: { goBack: () => void; goForward: () => void; openCommand: (query: string) => void }): void {
|
|
158
|
+
const shortcutQuery = commandPaletteShortcutQuery(event);
|
|
159
|
+
const isEditableTarget = isEditableShortcutTarget(event.target);
|
|
160
|
+
const commandQuery = (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k" ? "" : shortcutQuery;
|
|
161
|
+
|
|
162
|
+
if (commandQuery !== undefined && !isEditableTarget) {
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
actions.openCommand(commandQuery);
|
|
165
|
+
} else if (event.key === "Backspace" && !isEditableTarget) {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
(event.shiftKey ? actions.goForward : actions.goBack)();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
141
171
|
function isEditableShortcutTarget(target: EventTarget | null): boolean {
|
|
142
172
|
if (!(target instanceof HTMLElement)) {
|
|
143
173
|
return false;
|
|
@@ -156,15 +186,16 @@ function NotificationsMenu({ openSurface }: { openSurface: (surface: SurfaceId)
|
|
|
156
186
|
);
|
|
157
187
|
}
|
|
158
188
|
|
|
159
|
-
function ConversationWorkspace({ conversationMode, selectedLocalRepoIndex, status }: {
|
|
189
|
+
function ConversationWorkspace({ conversationMode, selectedLocalRepoIndex, selectedSessionId, status }: {
|
|
160
190
|
conversationMode: ConversationMode;
|
|
161
191
|
selectedLocalRepoIndex: number;
|
|
192
|
+
selectedSessionId: string | undefined;
|
|
162
193
|
status: GuiStatusData;
|
|
163
194
|
}) {
|
|
164
195
|
const selectedRepo = status.local_repos.repos[selectedLocalRepoIndex] ?? status.local_repos.repos[0];
|
|
165
196
|
const selectedSession = selectedRepo === undefined
|
|
166
197
|
? undefined
|
|
167
|
-
: 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);
|
|
168
199
|
const title = conversationMode === "ai" ? selectedRepo?.name ?? text.opencodeSessions : text.teams;
|
|
169
200
|
|
|
170
201
|
return (
|
|
@@ -247,6 +278,7 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, status }: {
|
|
|
247
278
|
const vaultCollection = vaultCollectionForSurface(status.vault, activeSurface);
|
|
248
279
|
const staticSurfaces: Partial<Record<SurfaceId, ReactNode>> = {
|
|
249
280
|
overview: <OverviewSurface status={status} />,
|
|
281
|
+
help: <HelpSurface />,
|
|
250
282
|
settings: <SettingsSurface status={status} />,
|
|
251
283
|
notifications: <NotificationsSurface />,
|
|
252
284
|
admin: <AdminSurface />,
|
|
@@ -274,7 +306,7 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, status }: {
|
|
|
274
306
|
maintenance: <PlannedSurface label={text.maintenance} detail={text.projectWorkIntro} />,
|
|
275
307
|
performance: <PlannedSurface label={text.performance} detail={text.projectWorkIntro} />,
|
|
276
308
|
reports: <PlannedSurface label={text.reports} detail={text.projectWorkIntro} />,
|
|
277
|
-
apps: <AppsSurface />,
|
|
309
|
+
apps: <AppsSurface status={status} />,
|
|
278
310
|
installation: <InstallationSurface />,
|
|
279
311
|
projects: <ProjectsSurface status={status} />,
|
|
280
312
|
security: <SecuritySurface status={status} />,
|
|
@@ -300,6 +332,40 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, status }: {
|
|
|
300
332
|
return staticSurfaces[activeSurface] ?? null;
|
|
301
333
|
}
|
|
302
334
|
|
|
335
|
+
function HelpSurface(): ReactElement {
|
|
336
|
+
const shortcuts = [
|
|
337
|
+
["#", "Channels"],
|
|
338
|
+
["_", "AI sessions"],
|
|
339
|
+
["&", "Infrastructure"],
|
|
340
|
+
["?", "Help"],
|
|
341
|
+
[">", "New terminal command"],
|
|
342
|
+
["+", "Add commands"],
|
|
343
|
+
["-", "Remove commands"],
|
|
344
|
+
["/", "Slash commands"],
|
|
345
|
+
["=", "Comms"],
|
|
346
|
+
["~", "Settings and config"],
|
|
347
|
+
["!", "Notifications"],
|
|
348
|
+
[".", "Agents"],
|
|
349
|
+
["*", "Secrets and passwords"],
|
|
350
|
+
[":", "Emoji picker"],
|
|
351
|
+
["^", "Copy current page link"],
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<section className="settings-surface help-surface">
|
|
356
|
+
<div className="planned-card">
|
|
357
|
+
<h2>Help</h2>
|
|
358
|
+
<p>Use the command palette shortcuts from anywhere outside text inputs. Recent selections appear first until you keep typing to filter.</p>
|
|
359
|
+
</div>
|
|
360
|
+
<ul className="shortcut-list">
|
|
361
|
+
{shortcuts.map(([shortcut, label]) => <li key={shortcut}><kbd>{shortcut}</kbd><span>{label}</span></li>)}
|
|
362
|
+
<li><kbd>Backspace</kbd><span>Previous page</span></li>
|
|
363
|
+
<li><kbd>Shift Backspace</kbd><span>Next page</span></li>
|
|
364
|
+
</ul>
|
|
365
|
+
</section>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
303
369
|
function SettingsSurface({ status }: { status: GuiStatusData }): ReactElement {
|
|
304
370
|
return (
|
|
305
371
|
<section className="settings-surface">
|