aidevops 3.24.2 → 3.25.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/status-adapter.ts +88 -1
- package/packages/gui-shared/src/contracts.ts +17 -0
- package/packages/gui-shared/src/fixtures.ts +6 -0
- package/packages/gui-web/src/App.tsx +29 -6
- package/packages/gui-web/src/AppNavigation.tsx +161 -43
- package/packages/gui-web/src/AppWorkspace.tsx +71 -6
- package/packages/gui-web/src/app-model.ts +12 -0
- package/packages/gui-web/src/status-client.ts +1 -0
- package/packages/gui-web/src/styles.css +222 -0
- package/setup.sh +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ The result: an AI operations platform that manages projects across every busines
|
|
|
58
58
|
[](https://github.com/marcusquinn)
|
|
59
59
|
|
|
60
60
|
<!-- Release & Version Info -->
|
|
61
|
-
[](https://github.com/marcusquinn/aidevops/releases)
|
|
62
62
|
[](https://www.npmjs.com/package/aidevops)
|
|
63
63
|
[](https://github.com/marcusquinn/homebrew-tap)
|
|
64
64
|
[](https://github.com/marcusquinn/aidevops)
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.25.1
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,11 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { hostname, networkInterfaces, userInfo } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
5
6
|
import {
|
|
7
|
+
type GuiLocalRepoSetupSummary,
|
|
8
|
+
type GuiOpenCodeSessionRegistrySummary,
|
|
9
|
+
type GuiOpenCodeSessionSummary,
|
|
6
10
|
assertNoSecretSentinels,
|
|
7
11
|
createEnvelope,
|
|
8
12
|
VAULT_STATUS_ROUTE_MANIFEST,
|
|
@@ -51,11 +55,13 @@ export function readStatus(
|
|
|
51
55
|
const settingsPathRef = "~/.config/aidevops/settings.json";
|
|
52
56
|
const reposPathRef = "~/.config/aidevops/repos.json";
|
|
53
57
|
const oauthPoolPathRef = "~/.aidevops/oauth-pool.json";
|
|
58
|
+
const opencodeDbPathRef = "~/.local/share/opencode/opencode.db";
|
|
54
59
|
const agentsPathRef = "~/.aidevops/agents";
|
|
55
60
|
const vaultPathRef = statusFixture.vault.path_ref;
|
|
56
61
|
const settingsPath = expandHome(settingsPathRef);
|
|
57
62
|
const reposPath = expandHome(reposPathRef);
|
|
58
63
|
const oauthPoolPath = expandHome(oauthPoolPathRef);
|
|
64
|
+
const opencodeDbPath = expandHome(opencodeDbPathRef);
|
|
59
65
|
const aidevopsVersion = existsSync(versionPath)
|
|
60
66
|
? readFileSync(versionPath, "utf8").trim()
|
|
61
67
|
: statusFixture.aidevops_version;
|
|
@@ -63,12 +69,15 @@ export function readStatus(
|
|
|
63
69
|
const restartRequired = installedVersion !== aidevopsVersion;
|
|
64
70
|
const setupTargets = readSetupTargets(aidevopsVersion || "unknown", installedVersion || "unknown");
|
|
65
71
|
const aiApps = readAiApps(aidevopsVersion || "unknown", installedVersion || "unknown");
|
|
72
|
+
const localRepos = readLocalReposSetupSummary(reposPath);
|
|
73
|
+
const opencodeSessions = readOpenCodeSessions(opencodeDbPath, opencodeDbPathRef, localRepos.repos);
|
|
66
74
|
const vault = readVaultSummary(repoRoot);
|
|
67
75
|
const sourcePathRefs = [
|
|
68
76
|
agentsPathRef,
|
|
69
77
|
settingsPathRef,
|
|
70
78
|
reposPathRef,
|
|
71
79
|
oauthPoolPathRef,
|
|
80
|
+
opencodeDbPathRef,
|
|
72
81
|
vaultPathRef,
|
|
73
82
|
"VERSION",
|
|
74
83
|
...setupTargets.map((target) => target.path_ref),
|
|
@@ -114,7 +123,8 @@ export function readStatus(
|
|
|
114
123
|
],
|
|
115
124
|
settings: readSettingsSummary(settingsPath, settingsPathRef),
|
|
116
125
|
repos: readRepoRegistrySummary(reposPath, reposPathRef),
|
|
117
|
-
local_repos:
|
|
126
|
+
local_repos: localRepos,
|
|
127
|
+
opencode_sessions: opencodeSessions,
|
|
118
128
|
oauth_pool: readOAuthPoolSummary(oauthPoolPath, oauthPoolPathRef),
|
|
119
129
|
setup_targets: setupTargets,
|
|
120
130
|
ai_apps: aiApps,
|
|
@@ -419,6 +429,83 @@ function resolveBinary(binary: string): string | null {
|
|
|
419
429
|
}
|
|
420
430
|
}
|
|
421
431
|
|
|
432
|
+
interface OpenCodeSessionRow {
|
|
433
|
+
id: string;
|
|
434
|
+
directory: string;
|
|
435
|
+
title: string;
|
|
436
|
+
time_updated: number;
|
|
437
|
+
model: string | null;
|
|
438
|
+
agent: string | null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function readOpenCodeSessions(dbPath: string, dbPathRef: string, repos: GuiLocalRepoSetupSummary[]): GuiOpenCodeSessionRegistrySummary {
|
|
442
|
+
if (!existsSync(dbPath)) {
|
|
443
|
+
return {
|
|
444
|
+
path_ref: dbPathRef,
|
|
445
|
+
health: "missing",
|
|
446
|
+
value_policy: "metadata_only_no_message_payloads",
|
|
447
|
+
sessions: [],
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
const database = new Database(dbPath, { readonly: true });
|
|
453
|
+
try {
|
|
454
|
+
const rows = database.query(`
|
|
455
|
+
SELECT id, directory, title, time_updated, model, agent
|
|
456
|
+
FROM session
|
|
457
|
+
WHERE time_archived IS NULL
|
|
458
|
+
ORDER BY time_updated DESC
|
|
459
|
+
LIMIT 200
|
|
460
|
+
`).all() as OpenCodeSessionRow[];
|
|
461
|
+
const sessions = rows
|
|
462
|
+
.map((row) => opencodeSessionFromRow(row, repos))
|
|
463
|
+
.filter((session): session is GuiOpenCodeSessionSummary => session !== null);
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
path_ref: dbPathRef,
|
|
467
|
+
health: "present",
|
|
468
|
+
value_policy: "metadata_only_no_message_payloads",
|
|
469
|
+
sessions,
|
|
470
|
+
};
|
|
471
|
+
} finally {
|
|
472
|
+
database.close();
|
|
473
|
+
}
|
|
474
|
+
} catch {
|
|
475
|
+
return {
|
|
476
|
+
path_ref: dbPathRef,
|
|
477
|
+
health: "invalid",
|
|
478
|
+
value_policy: "metadata_only_no_message_payloads",
|
|
479
|
+
sessions: [],
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function opencodeSessionFromRow(row: OpenCodeSessionRow, repos: GuiLocalRepoSetupSummary[]): GuiOpenCodeSessionSummary | null {
|
|
485
|
+
const repo = repos.find((candidate) => row.directory === expandHome(candidate.path_ref) || row.directory.startsWith(`${expandHome(candidate.path_ref)}/`));
|
|
486
|
+
|
|
487
|
+
if (!repo) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
id_ref: row.id,
|
|
493
|
+
repo_path_ref: repo.path_ref,
|
|
494
|
+
title: row.title.trim().length > 0 ? row.title : "Untitled session",
|
|
495
|
+
updated_at: formatUnixMillis(row.time_updated),
|
|
496
|
+
model: row.model ?? "unknown",
|
|
497
|
+
agent: row.agent ?? "unknown",
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function formatUnixMillis(value: number): string {
|
|
502
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
503
|
+
return "unknown";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return new Date(value).toISOString();
|
|
507
|
+
}
|
|
508
|
+
|
|
422
509
|
function readBinaryVersion(binaryPath: string, args: string[]): string {
|
|
423
510
|
if (args.length === 0) {
|
|
424
511
|
return "unknown";
|
|
@@ -131,6 +131,22 @@ export interface GuiLocalReposSetupSummary {
|
|
|
131
131
|
repos: GuiLocalRepoSetupSummary[];
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
export interface GuiOpenCodeSessionSummary {
|
|
135
|
+
id_ref: string;
|
|
136
|
+
repo_path_ref: string;
|
|
137
|
+
title: string;
|
|
138
|
+
updated_at: string;
|
|
139
|
+
model: string;
|
|
140
|
+
agent: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface GuiOpenCodeSessionRegistrySummary {
|
|
144
|
+
path_ref: string;
|
|
145
|
+
health: "present" | "missing" | "invalid" | "unchecked";
|
|
146
|
+
value_policy: "metadata_only_no_message_payloads";
|
|
147
|
+
sessions: GuiOpenCodeSessionSummary[];
|
|
148
|
+
}
|
|
149
|
+
|
|
134
150
|
export type GuiAiProviderId = "anthropic" | "openai" | "cursor" | "google";
|
|
135
151
|
|
|
136
152
|
export interface GuiOAuthPoolAccountSummary {
|
|
@@ -298,6 +314,7 @@ export interface GuiStatusData {
|
|
|
298
314
|
settings: GuiSettingsSummary;
|
|
299
315
|
repos: GuiRepoRegistrySummary;
|
|
300
316
|
local_repos: GuiLocalReposSetupSummary;
|
|
317
|
+
opencode_sessions: GuiOpenCodeSessionRegistrySummary;
|
|
301
318
|
oauth_pool: GuiOAuthPoolSummary;
|
|
302
319
|
setup_targets: GuiSetupTargetSummary[];
|
|
303
320
|
ai_apps: GuiAiAppSummary[];
|
|
@@ -68,6 +68,12 @@ export const statusFixture: GuiStatusData = {
|
|
|
68
68
|
excluded_worktrees: 0,
|
|
69
69
|
repos: [],
|
|
70
70
|
},
|
|
71
|
+
opencode_sessions: {
|
|
72
|
+
path_ref: "~/.local/share/opencode/opencode.db",
|
|
73
|
+
health: "unchecked",
|
|
74
|
+
value_policy: "metadata_only_no_message_payloads",
|
|
75
|
+
sessions: [],
|
|
76
|
+
},
|
|
71
77
|
oauth_pool: {
|
|
72
78
|
path_ref: "~/.aidevops/oauth-pool.json",
|
|
73
79
|
health: "unchecked",
|
|
@@ -2,7 +2,7 @@ import type { GuiResponseEnvelope, GuiStatusData } from "@aidevops/gui-shared";
|
|
|
2
2
|
import { type ReactElement, useEffect, useState } from "react";
|
|
3
3
|
import { MachineRail, Sidebar } from "./AppNavigation";
|
|
4
4
|
import { Workspace } from "./AppWorkspace";
|
|
5
|
-
import type { FontPreference, FontSizePreference, SurfaceId, ThemePreference } from "./app-model";
|
|
5
|
+
import type { ConversationMode, FontPreference, FontSizePreference, ShellMode, SurfaceId, ThemePreference } from "./app-model";
|
|
6
6
|
import { DEFAULT_ACCENT_HUE, DEFAULT_FONT, DEFAULT_FONT_SIZE, fileRootBySurface, findSurface, findSurfaceSectionLabel, fontFamilyForPreference, fontSizeForPreference, getSystemTheme, isFontPreference, isFontSizePreference } from "./app-model";
|
|
7
7
|
import { fetchStatus, mockedStatus } from "./status-client";
|
|
8
8
|
|
|
@@ -73,6 +73,9 @@ export function App(): ReactElement {
|
|
|
73
73
|
const [machineRailVisible, setMachineRailVisible] = useState(storedAppearancePreferences.machineRailVisible);
|
|
74
74
|
const [showNavCounts, setShowNavCounts] = useState(storedAppearancePreferences.showNavCounts);
|
|
75
75
|
const [showBorders, setShowBorders] = useState(storedAppearancePreferences.showBorders);
|
|
76
|
+
const [shellMode, setShellMode] = useState<ShellMode>("devices");
|
|
77
|
+
const [conversationMode, setConversationMode] = useState<ConversationMode>("ai");
|
|
78
|
+
const [selectedLocalRepoIndex, setSelectedLocalRepoIndex] = useState(0);
|
|
76
79
|
const [systemTheme, setSystemTheme] = useState<"light" | "dark">("light");
|
|
77
80
|
const resolvedTheme = themePreference === "system" ? systemTheme : themePreference;
|
|
78
81
|
const activeSurface: SurfaceId = navigation.entries[navigation.index] ?? "overview";
|
|
@@ -166,6 +169,10 @@ export function App(): ReactElement {
|
|
|
166
169
|
persistAppearancePreference(appearanceStorageKeys.machineRail, String(machineRailVisible));
|
|
167
170
|
}, [machineRailVisible]);
|
|
168
171
|
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
setSelectedLocalRepoIndex((current) => Math.min(current, Math.max(0, status.data.local_repos.repos.length - 1)));
|
|
174
|
+
}, [status.data.local_repos.repos.length]);
|
|
175
|
+
|
|
169
176
|
return (
|
|
170
177
|
<main className={machineRailVisible ? "app-shell" : "app-shell machine-rail-collapsed"}>
|
|
171
178
|
<div className="desktop-titlebar-tagline" aria-hidden="true">Your data protected. Your systems managed. Your creations published.</div>
|
|
@@ -173,25 +180,41 @@ export function App(): ReactElement {
|
|
|
173
180
|
<Sidebar
|
|
174
181
|
activeSurface={activeSurface}
|
|
175
182
|
accentHue={accentHue}
|
|
176
|
-
|
|
177
|
-
canGoForward={canGoForward}
|
|
183
|
+
conversationMode={conversationMode}
|
|
178
184
|
fontSizePreference={fontSizePreference}
|
|
179
185
|
fontPreference={fontPreference}
|
|
180
|
-
|
|
181
|
-
goForward={goForward}
|
|
186
|
+
selectedLocalRepoIndex={selectedLocalRepoIndex}
|
|
182
187
|
setAccentHue={setAccentHue}
|
|
183
188
|
setActiveSurface={setActiveSurface}
|
|
189
|
+
setConversationMode={setConversationMode}
|
|
184
190
|
setFontSizePreference={setFontSizePreference}
|
|
185
191
|
setFontPreference={setFontPreference}
|
|
192
|
+
setSelectedLocalRepoIndex={setSelectedLocalRepoIndex}
|
|
193
|
+
setShellMode={setShellMode}
|
|
186
194
|
setShowBorders={setShowBorders}
|
|
187
195
|
setShowNavCounts={setShowNavCounts}
|
|
188
196
|
setThemePreference={setThemePreference}
|
|
197
|
+
shellMode={shellMode}
|
|
189
198
|
showBorders={showBorders}
|
|
190
199
|
showNavCounts={showNavCounts}
|
|
191
200
|
status={status.data}
|
|
192
201
|
themePreference={themePreference}
|
|
193
202
|
/>
|
|
194
|
-
<Workspace
|
|
203
|
+
<Workspace
|
|
204
|
+
activeItem={activeItem}
|
|
205
|
+
activeSectionLabel={activeSectionLabel}
|
|
206
|
+
activeSurface={activeSurface}
|
|
207
|
+
canGoBack={canGoBack}
|
|
208
|
+
canGoForward={canGoForward}
|
|
209
|
+
conversationMode={conversationMode}
|
|
210
|
+
fileRoot={fileRoot}
|
|
211
|
+
goBack={goBack}
|
|
212
|
+
goForward={goForward}
|
|
213
|
+
selectedLocalRepoIndex={selectedLocalRepoIndex}
|
|
214
|
+
setActiveSurface={setActiveSurface}
|
|
215
|
+
shellMode={shellMode}
|
|
216
|
+
status={status.data}
|
|
217
|
+
/>
|
|
195
218
|
<DesktopStatusBar status={status.data} />
|
|
196
219
|
</main>
|
|
197
220
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* jshint esversion: 11 */
|
|
2
2
|
import type { GuiMachineSummary, GuiStatusData } from "@aidevops/gui-shared";
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
3
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
4
4
|
import type { IconType } from "react-icons";
|
|
5
5
|
import {
|
|
6
6
|
FiBookmark,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
FiGitBranch,
|
|
19
19
|
FiGlobe,
|
|
20
20
|
FiGrid,
|
|
21
|
+
FiHash,
|
|
21
22
|
FiHardDrive,
|
|
22
23
|
FiLink,
|
|
23
24
|
FiLink2,
|
|
@@ -34,7 +35,7 @@ import {
|
|
|
34
35
|
FiTerminal,
|
|
35
36
|
FiUsers,
|
|
36
37
|
} from "react-icons/fi";
|
|
37
|
-
import type { FontPreference, FontSizePreference, SidebarMode, SurfaceIconName, SurfaceId, SurfaceNavGroup, SurfaceNavItem, ThemePreference } from "./app-model";
|
|
38
|
+
import type { ConversationMode, FontPreference, FontSizePreference, ShellMode, SidebarMode, SurfaceIconName, SurfaceId, SurfaceNavGroup, SurfaceNavItem, ThemePreference } from "./app-model";
|
|
38
39
|
import { DEFAULT_ACCENT_HUE, dashboardNavItem, fontFamilyForPreference, fontOptions, fontSizeOptions, navGroups, sidebarModeForSurface, surfaceRecordCounts, text } from "./app-model";
|
|
39
40
|
import { VaultPadlock, vaultCollectionForSurface } from "./VaultBadges";
|
|
40
41
|
|
|
@@ -106,22 +107,24 @@ export function MachineRail({ machine }: { machine?: GuiMachineSummary }) {
|
|
|
106
107
|
);
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
export function Sidebar({ activeSurface, accentHue,
|
|
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 }: {
|
|
110
111
|
activeSurface: SurfaceId;
|
|
111
112
|
accentHue: number;
|
|
112
|
-
|
|
113
|
-
canGoForward: boolean;
|
|
113
|
+
conversationMode: ConversationMode;
|
|
114
114
|
fontPreference: FontPreference;
|
|
115
115
|
fontSizePreference: FontSizePreference;
|
|
116
|
-
|
|
117
|
-
goForward: () => void;
|
|
116
|
+
selectedLocalRepoIndex: number;
|
|
118
117
|
setAccentHue: (hue: number) => void;
|
|
119
118
|
setActiveSurface: (surface: SurfaceId) => void;
|
|
119
|
+
setConversationMode: (mode: ConversationMode) => void;
|
|
120
120
|
setFontPreference: (font: FontPreference) => void;
|
|
121
121
|
setFontSizePreference: (size: FontSizePreference) => void;
|
|
122
|
+
setSelectedLocalRepoIndex: (index: number) => void;
|
|
123
|
+
setShellMode: (mode: ShellMode) => void;
|
|
122
124
|
setShowBorders: (show: boolean) => void;
|
|
123
125
|
setShowNavCounts: (show: boolean) => void;
|
|
124
126
|
setThemePreference: (theme: ThemePreference) => void;
|
|
127
|
+
shellMode: ShellMode;
|
|
125
128
|
showBorders: boolean;
|
|
126
129
|
showNavCounts: boolean;
|
|
127
130
|
status: GuiStatusData;
|
|
@@ -137,13 +140,21 @@ export function Sidebar({ activeSurface, accentHue, canGoBack, canGoForward, fon
|
|
|
137
140
|
|
|
138
141
|
return (
|
|
139
142
|
<aside className="app-sidebar" aria-label={text.navigationLabel}>
|
|
140
|
-
<SidebarHeader
|
|
143
|
+
<SidebarHeader setShellMode={setShellMode} shellMode={shellMode} />
|
|
141
144
|
<nav className="sidebar-content">
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
{shellMode === "devices" ? <>
|
|
146
|
+
<SidebarModeTabs mode={sidebarMode} setMode={(mode) => setSidebarMode(mode as SidebarMode)} />
|
|
147
|
+
<ul className="sidebar-top-link">
|
|
148
|
+
<SidebarItem activeSurface={activeSurface} item={dashboardNavItem} setActiveSurface={setActiveSurface} showCount={false} status={status} />
|
|
149
|
+
</ul>
|
|
150
|
+
{visibleGroups.map((group) => <SidebarGroup activeSurface={activeSurface} group={group} key={group.label} recordCounts={recordCounts} setActiveSurface={setActiveSurface} showNavCounts={showNavCounts} status={status} />)}
|
|
151
|
+
</> : <ConversationSidebar
|
|
152
|
+
conversationMode={conversationMode}
|
|
153
|
+
selectedLocalRepoIndex={selectedLocalRepoIndex}
|
|
154
|
+
setConversationMode={setConversationMode}
|
|
155
|
+
setSelectedLocalRepoIndex={setSelectedLocalRepoIndex}
|
|
156
|
+
status={status}
|
|
157
|
+
/>}
|
|
147
158
|
</nav>
|
|
148
159
|
<SidebarFooter
|
|
149
160
|
accentHue={accentHue}
|
|
@@ -163,14 +174,102 @@ export function Sidebar({ activeSurface, accentHue, canGoBack, canGoForward, fon
|
|
|
163
174
|
);
|
|
164
175
|
}
|
|
165
176
|
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
177
|
+
function IconSwitch<TValue extends string>({ ariaLabel, options, value }: {
|
|
178
|
+
ariaLabel: string;
|
|
179
|
+
options: Array<{ icon: ReactNode; label: string; onSelect: () => void; value: TValue }>;
|
|
180
|
+
value: TValue;
|
|
181
|
+
}) {
|
|
182
|
+
return (
|
|
183
|
+
<div className="icon-switch" role="tablist" aria-label={ariaLabel}>
|
|
184
|
+
{options.map((option) => (
|
|
185
|
+
<button
|
|
186
|
+
aria-label={option.label}
|
|
187
|
+
aria-selected={value === option.value}
|
|
188
|
+
className={value === option.value ? "active" : ""}
|
|
189
|
+
key={option.value}
|
|
190
|
+
onClick={option.onSelect}
|
|
191
|
+
role="tab"
|
|
192
|
+
title={option.label}
|
|
193
|
+
type="button"
|
|
194
|
+
>
|
|
195
|
+
{option.icon}
|
|
196
|
+
<span>{option.label}</span>
|
|
197
|
+
</button>
|
|
198
|
+
))}
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function ConversationSidebar({ conversationMode, selectedLocalRepoIndex, setConversationMode, setSelectedLocalRepoIndex, status }: {
|
|
204
|
+
conversationMode: ConversationMode;
|
|
205
|
+
selectedLocalRepoIndex: number;
|
|
206
|
+
setConversationMode: (mode: ConversationMode) => void;
|
|
207
|
+
setSelectedLocalRepoIndex: (index: number) => void;
|
|
208
|
+
status: GuiStatusData;
|
|
209
|
+
}) {
|
|
210
|
+
const repos = status.local_repos.repos;
|
|
211
|
+
const selectedRepo = repos[selectedLocalRepoIndex] ?? repos[0];
|
|
212
|
+
const sessions = selectedRepo === undefined
|
|
213
|
+
? []
|
|
214
|
+
: status.opencode_sessions.sessions.filter((session) => session.repo_path_ref === selectedRepo.path_ref);
|
|
215
|
+
const canSelectPreviousRepo = selectedLocalRepoIndex > 0;
|
|
216
|
+
const canSelectNextRepo = selectedLocalRepoIndex < repos.length - 1;
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<section className="conversation-panel" aria-label={text.opencodeSessions}>
|
|
220
|
+
<SidebarModeTabs mode={conversationMode} setMode={setConversationMode} />
|
|
221
|
+
{conversationMode === "people" ? <PeopleChannelList /> : <>
|
|
222
|
+
<button className="new-session-button" disabled title="Creating sessions needs an audited OpenCode write route." type="button">{text.newSession}</button>
|
|
223
|
+
<section className="repo-session-selector" aria-label={text.localRepoSelector}>
|
|
224
|
+
<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>
|
|
225
|
+
<label>
|
|
226
|
+
<span>{text.localRepos}</span>
|
|
227
|
+
<select onChange={(event) => setSelectedLocalRepoIndex(Number.parseInt(event.currentTarget.value, 10))} value={Math.min(selectedLocalRepoIndex, Math.max(0, repos.length - 1))}>
|
|
228
|
+
{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
|
+
</select>
|
|
230
|
+
</label>
|
|
231
|
+
<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>
|
|
232
|
+
</section>
|
|
233
|
+
<section className="sidebar-group session-history-list">
|
|
234
|
+
<h2>{text.sessionHistory}</h2>
|
|
235
|
+
{selectedRepo ? <ul>
|
|
236
|
+
{sessions.length === 0
|
|
237
|
+
? <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, index) => <li key={session.id_ref}><button className={index === 0 ? "surface-link active" : "surface-link"} 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
|
+
</ul> : <p className="empty-sidebar-state">No local repos discovered.</p>}
|
|
240
|
+
</section>
|
|
241
|
+
</>}
|
|
242
|
+
</section>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function PeopleChannelList() {
|
|
247
|
+
return (
|
|
248
|
+
<section aria-label="People channels">
|
|
249
|
+
<div className="notice compact-notice">{text.simplexReady}</div>
|
|
250
|
+
<section className="sidebar-group session-history-list">
|
|
251
|
+
<h2>{text.teams}</h2>
|
|
252
|
+
<ul>
|
|
253
|
+
{["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
|
+
</ul>
|
|
255
|
+
</section>
|
|
256
|
+
<section className="sidebar-group session-history-list">
|
|
257
|
+
<h2>{text.directMessages}</h2>
|
|
258
|
+
<ul>
|
|
259
|
+
{["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
|
+
</ul>
|
|
261
|
+
</section>
|
|
262
|
+
</section>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function SidebarModeTabs<TMode extends SidebarMode | ConversationMode>({ mode, setMode }: {
|
|
267
|
+
mode: TMode;
|
|
268
|
+
setMode: (mode: TMode) => void;
|
|
169
269
|
}) {
|
|
170
|
-
const modes
|
|
171
|
-
{ label: text.
|
|
172
|
-
{ label: text.comms, value: "comms" }
|
|
173
|
-
];
|
|
270
|
+
const modes = (mode === "ai" || mode === "people"
|
|
271
|
+
? [{ label: text.ai, value: "ai" }, { label: text.people, value: "people" }]
|
|
272
|
+
: [{ label: text.devops, value: "devops" }, { label: text.comms, value: "comms" }]) as Array<{ label: string; value: TMode }>;
|
|
174
273
|
|
|
175
274
|
return (
|
|
176
275
|
<div className="sidebar-mode-tabs" role="tablist" aria-label="Sidebar sections">
|
|
@@ -246,11 +345,9 @@ function SidebarItem({ activeSurface, item, recordCount, setActiveSurface, showC
|
|
|
246
345
|
);
|
|
247
346
|
}
|
|
248
347
|
|
|
249
|
-
function SidebarHeader({
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
goBack: () => void;
|
|
253
|
-
goForward: () => void;
|
|
348
|
+
function SidebarHeader({ setShellMode, shellMode }: {
|
|
349
|
+
setShellMode: (mode: ShellMode) => void;
|
|
350
|
+
shellMode: ShellMode;
|
|
254
351
|
}) {
|
|
255
352
|
return (
|
|
256
353
|
<header className="sidebar-header">
|
|
@@ -259,10 +356,14 @@ function SidebarHeader({ canGoBack, canGoForward, goBack, goForward }: {
|
|
|
259
356
|
<span className="terminal-mark" aria-hidden="true">›_</span>
|
|
260
357
|
<strong>{text.aidevops}</strong>
|
|
261
358
|
</div>
|
|
262
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
359
|
+
<IconSwitch
|
|
360
|
+
ariaLabel="Navigation scope"
|
|
361
|
+
options={[
|
|
362
|
+
{ icon: <FiMonitor aria-hidden="true" />, label: text.devices, onSelect: () => setShellMode("devices"), value: "devices" },
|
|
363
|
+
{ icon: <FiHash aria-hidden="true" />, label: text.opencodeSessions, onSelect: () => setShellMode("sessions"), value: "sessions" },
|
|
364
|
+
]}
|
|
365
|
+
value={shellMode}
|
|
366
|
+
/>
|
|
266
367
|
</div>
|
|
267
368
|
</header>
|
|
268
369
|
);
|
|
@@ -285,8 +386,20 @@ function SidebarFooter({ accentHue, fontPreference, fontSizePreference, setAccen
|
|
|
285
386
|
const [appearanceOpen, setAppearanceOpen] = useState(true);
|
|
286
387
|
const AppearanceChevron = appearanceOpen ? FiChevronDown : FiChevronUp;
|
|
287
388
|
const selectedFontFamily = fontFamilyForPreference(fontPreference);
|
|
389
|
+
const fontIndex = Math.max(0, fontOptions.findIndex((option) => option.value === fontPreference));
|
|
288
390
|
const fontSizeIndex = Math.max(0, fontSizeOptions.findIndex((option) => option.value === fontSizePreference));
|
|
391
|
+
const canSelectPreviousFont = fontIndex > 0;
|
|
392
|
+
const canSelectNextFont = fontIndex < fontOptions.length - 1;
|
|
289
393
|
const [hueInput, setHueInput] = useState(() => String(accentHue));
|
|
394
|
+
const setFontByIndex = (index: number) => {
|
|
395
|
+
const nextFont = fontOptions[index]?.value;
|
|
396
|
+
|
|
397
|
+
if (nextFont === undefined) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
setFontPreference(nextFont);
|
|
402
|
+
};
|
|
290
403
|
const updateAccentHue = (value: number) => {
|
|
291
404
|
if (!Number.isFinite(value)) {
|
|
292
405
|
return;
|
|
@@ -385,20 +498,25 @@ function SidebarFooter({ accentHue, fontPreference, fontSizePreference, setAccen
|
|
|
385
498
|
{fontSizeOptions.map((option) => <span className={option.value === fontSizePreference ? "active" : ""} key={option.value}>{option.label}</span>)}
|
|
386
499
|
</div>
|
|
387
500
|
</div>
|
|
388
|
-
<
|
|
389
|
-
<span>{text.font}</span>
|
|
390
|
-
<
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
501
|
+
<div className="font-control">
|
|
502
|
+
<span id="appearance-font-selector-label">{text.font}</span>
|
|
503
|
+
<div className="selector-with-stepper font-selector-row">
|
|
504
|
+
<button aria-label="Previous font" className="selector-step-button" disabled={!canSelectPreviousFont} onClick={() => setFontByIndex(fontIndex - 1)} type="button"><FiChevronLeft aria-hidden="true" /></button>
|
|
505
|
+
<select
|
|
506
|
+
aria-labelledby="appearance-font-selector-label"
|
|
507
|
+
onChange={(event) => setFontPreference(event.currentTarget.value as FontPreference)}
|
|
508
|
+
style={{ fontFamily: selectedFontFamily }}
|
|
509
|
+
value={fontPreference}
|
|
510
|
+
>
|
|
511
|
+
{fontOptions.map((option) => (
|
|
512
|
+
<option key={option.value} style={{ fontFamily: option.fontFamily }} value={option.value}>
|
|
513
|
+
{option.label}
|
|
514
|
+
</option>
|
|
515
|
+
))}
|
|
516
|
+
</select>
|
|
517
|
+
<button aria-label="Next font" className="selector-step-button" disabled={!canSelectNextFont} onClick={() => setFontByIndex(fontIndex + 1)} type="button"><FiChevronRight aria-hidden="true" /></button>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
402
520
|
</div>
|
|
403
521
|
: null}
|
|
404
522
|
</section>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/* jshint esversion: 11 */
|
|
2
2
|
import { type ReactElement, type ReactNode, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { FiBell, FiCommand, FiCpu, FiGlobe, FiLogOut, FiMessageSquare, FiSearch, FiSettings, FiShield, FiUser } from "react-icons/fi";
|
|
3
|
+
import { FiBell, FiChevronLeft, FiChevronRight, FiCommand, FiCpu, FiGlobe, FiHash, FiLogOut, FiMessageSquare, FiSearch, FiSettings, FiShield, FiUser } from "react-icons/fi";
|
|
4
4
|
import type { GuiFileRootId, GuiStatusData } from "../../gui-shared/src";
|
|
5
5
|
import { SurfaceGlyph } from "./AppNavigation";
|
|
6
|
-
import type { SurfaceId, SurfaceNavItem } from "./app-model";
|
|
6
|
+
import type { ConversationMode, ShellMode, SurfaceId, SurfaceNavItem } from "./app-model";
|
|
7
7
|
import { inventorySurfaceConfigs, orderedNavItems, text } from "./app-model";
|
|
8
8
|
import { FileExplorerSurface } from "./FileExplorerSurface";
|
|
9
9
|
import { AppsSurface, EditableInventorySurface, InstallationSurface } from "./InventorySurfaces";
|
|
@@ -15,28 +15,41 @@ const communityLinks = {
|
|
|
15
15
|
x: "https://x.com/marcuswquinn",
|
|
16
16
|
} as const;
|
|
17
17
|
|
|
18
|
-
export function Workspace({ activeItem, activeSectionLabel, activeSurface, fileRoot, setActiveSurface, status }: {
|
|
18
|
+
export function Workspace({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, conversationMode, fileRoot, goBack, goForward, selectedLocalRepoIndex, setActiveSurface, shellMode, status }: {
|
|
19
19
|
activeItem: SurfaceNavItem;
|
|
20
20
|
activeSectionLabel: string;
|
|
21
21
|
activeSurface: SurfaceId;
|
|
22
|
+
canGoBack: boolean;
|
|
23
|
+
canGoForward: boolean;
|
|
24
|
+
conversationMode: ConversationMode;
|
|
22
25
|
fileRoot: GuiFileRootId | undefined;
|
|
26
|
+
goBack: () => void;
|
|
27
|
+
goForward: () => void;
|
|
28
|
+
selectedLocalRepoIndex: number;
|
|
23
29
|
setActiveSurface: (surface: SurfaceId) => void;
|
|
30
|
+
shellMode: ShellMode;
|
|
24
31
|
status: GuiStatusData;
|
|
25
32
|
}) {
|
|
26
33
|
return (
|
|
27
34
|
<section className="app-inset" aria-label={text.workspaceLabel}>
|
|
28
|
-
<WorkspaceHeader activeItem={activeItem} activeSectionLabel={activeSectionLabel} activeSurface={activeSurface} setActiveSurface={setActiveSurface} status={status} />
|
|
35
|
+
<WorkspaceHeader activeItem={activeItem} activeSectionLabel={activeSectionLabel} activeSurface={activeSurface} canGoBack={canGoBack} canGoForward={canGoForward} goBack={goBack} goForward={goForward} setActiveSurface={setActiveSurface} status={status} />
|
|
29
36
|
<div className="workspace-scroll">
|
|
30
|
-
|
|
37
|
+
{shellMode === "sessions"
|
|
38
|
+
? <ConversationWorkspace conversationMode={conversationMode} selectedLocalRepoIndex={selectedLocalRepoIndex} status={status} />
|
|
39
|
+
: <SurfaceContent activeItem={activeItem} activeSurface={activeSurface} fileRoot={fileRoot} status={status} />}
|
|
31
40
|
</div>
|
|
32
41
|
</section>
|
|
33
42
|
);
|
|
34
43
|
}
|
|
35
44
|
|
|
36
|
-
function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, setActiveSurface, status }: {
|
|
45
|
+
function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, goBack, goForward, setActiveSurface, status }: {
|
|
37
46
|
activeItem: SurfaceNavItem;
|
|
38
47
|
activeSectionLabel: string;
|
|
39
48
|
activeSurface: SurfaceId;
|
|
49
|
+
canGoBack: boolean;
|
|
50
|
+
canGoForward: boolean;
|
|
51
|
+
goBack: () => void;
|
|
52
|
+
goForward: () => void;
|
|
40
53
|
setActiveSurface: (surface: SurfaceId) => void;
|
|
41
54
|
status: GuiStatusData;
|
|
42
55
|
}): ReactElement {
|
|
@@ -80,6 +93,10 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, setAct
|
|
|
80
93
|
</div>
|
|
81
94
|
</div>
|
|
82
95
|
<div className="header-actions">
|
|
96
|
+
<div className="sidebar-history-controls workspace-history-controls">
|
|
97
|
+
<button aria-label="Previous surface" disabled={!canGoBack} onClick={goBack} type="button"><FiChevronLeft /></button>
|
|
98
|
+
<button aria-label="Next surface" disabled={!canGoForward} onClick={goForward} type="button"><FiChevronRight /></button>
|
|
99
|
+
</div>
|
|
83
100
|
<button className="workspace-search command-trigger" onClick={() => setCommandOpen(true)} type="button">
|
|
84
101
|
<FiSearch aria-hidden="true" />
|
|
85
102
|
<span>⌘K</span>
|
|
@@ -118,6 +135,54 @@ function NotificationsMenu({ openSurface }: { openSurface: (surface: SurfaceId)
|
|
|
118
135
|
);
|
|
119
136
|
}
|
|
120
137
|
|
|
138
|
+
function ConversationWorkspace({ conversationMode, selectedLocalRepoIndex, status }: {
|
|
139
|
+
conversationMode: ConversationMode;
|
|
140
|
+
selectedLocalRepoIndex: number;
|
|
141
|
+
status: GuiStatusData;
|
|
142
|
+
}) {
|
|
143
|
+
const selectedRepo = status.local_repos.repos[selectedLocalRepoIndex] ?? status.local_repos.repos[0];
|
|
144
|
+
const selectedSession = selectedRepo === undefined
|
|
145
|
+
? undefined
|
|
146
|
+
: status.opencode_sessions.sessions.find((session) => session.repo_path_ref === selectedRepo.path_ref);
|
|
147
|
+
const title = conversationMode === "ai" ? selectedRepo?.name ?? text.opencodeSessions : text.teams;
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<section className="chat-surface" aria-label={conversationMode === "ai" ? text.opencodeSessions : "People chat"}>
|
|
151
|
+
<div className="chat-thread-panel">
|
|
152
|
+
<header className="chat-thread-header">
|
|
153
|
+
<div>
|
|
154
|
+
<p className="eyebrow">{conversationMode === "ai" ? text.opencodeSessions : "SimpleX channels"}</p>
|
|
155
|
+
<h2><FiHash aria-hidden="true" /> {title}</h2>
|
|
156
|
+
</div>
|
|
157
|
+
<span className="count-pill">{text.readOnly}</span>
|
|
158
|
+
</header>
|
|
159
|
+
<div className="chat-message-list">
|
|
160
|
+
{conversationMode === "ai" ? <>
|
|
161
|
+
<ChatBubble speaker="assistant" title={selectedSession?.title ?? "AI session bridge"} body={selectedSession ? `Most recent OpenCode session metadata: ${selectedSession.model} via ${selectedSession.agent}, updated ${selectedSession.updated_at}. Message payloads stay out of the status API until the turbostarter/ai chat bridge lands.` : "OpenCode session creation and continuation need an audited write route. This panel is ready for the turbostarter/ai chat surface once that adapter is connected."} />
|
|
162
|
+
<ChatBubble speaker="user" title={selectedRepo?.name ?? "Local repo"} body={selectedRepo ? `Selected repo: ${selectedRepo.path_ref}. Session metadata is grouped per local repo and sorted newest first from ${status.opencode_sessions.path_ref}.` : "No local repos were discovered yet."} />
|
|
163
|
+
</> : <>
|
|
164
|
+
<ChatBubble speaker="assistant" title="SimpleX transport" body={text.simplexReady} />
|
|
165
|
+
<ChatBubble speaker="user" title="People channel" body="Teams and direct messages share the same Slack-like channel layout while protected message payloads remain behind Vault policy." />
|
|
166
|
+
</>}
|
|
167
|
+
</div>
|
|
168
|
+
<form className="chat-composer" aria-label="Chat composer">
|
|
169
|
+
<textarea disabled placeholder={text.chatInputPlaceholder} />
|
|
170
|
+
<button disabled type="button">Send</button>
|
|
171
|
+
</form>
|
|
172
|
+
</div>
|
|
173
|
+
</section>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function ChatBubble({ body, speaker, title }: { body: string; speaker: "assistant" | "user"; title: string }) {
|
|
178
|
+
return (
|
|
179
|
+
<article className={`chat-bubble ${speaker}`}>
|
|
180
|
+
<strong>{title}</strong>
|
|
181
|
+
<p>{body}</p>
|
|
182
|
+
</article>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
121
186
|
function ProfileMenu({ openSurface, userName }: { openSurface: (surface: SurfaceId) => void; userName: string }): ReactElement {
|
|
122
187
|
return (
|
|
123
188
|
<div className="popover-menu profile-menu" role="menu">
|
|
@@ -2,6 +2,8 @@ import type { GuiFileRootId, GuiStatusData } from "@aidevops/gui-shared";
|
|
|
2
2
|
|
|
3
3
|
export type ThemePreference = "system" | "light" | "dark";
|
|
4
4
|
export type SidebarMode = "devops" | "comms";
|
|
5
|
+
export type ShellMode = "devices" | "sessions";
|
|
6
|
+
export type ConversationMode = "ai" | "people";
|
|
5
7
|
export type FontPreference =
|
|
6
8
|
| "IBM Plex Mono"
|
|
7
9
|
| "IBM Plex Sans"
|
|
@@ -208,6 +210,16 @@ export const text = {
|
|
|
208
210
|
plannedHomes: "Planned homes",
|
|
209
211
|
plannedNotice: "This surface is a local UI placeholder. Persistence and actions need explicit trust-boundary work.",
|
|
210
212
|
comms: "Comms",
|
|
213
|
+
ai: "AI",
|
|
214
|
+
people: "People",
|
|
215
|
+
newSession: "New Session",
|
|
216
|
+
sessionHistory: "Session history",
|
|
217
|
+
opencodeSessions: "OpenCode sessions",
|
|
218
|
+
simplexReady: "SimpleX transport placeholder: encrypted channel adapters will mount here behind Vault and trust-boundary checks.",
|
|
219
|
+
teams: "Teams",
|
|
220
|
+
directMessages: "Direct Messages",
|
|
221
|
+
chatInputPlaceholder: "Write a message when audited session write routes are enabled.",
|
|
222
|
+
localRepoSelector: "Local repo selector",
|
|
211
223
|
performance: "Performance",
|
|
212
224
|
projectWorkIntro: "Document folders for inboxes, campaigns, cases, configuration, feedback, knowledge, maintenance, performance, and reports are planned.",
|
|
213
225
|
projectWork: "Documents",
|
|
@@ -82,6 +82,7 @@ function normalizeStatusEnvelope(envelope: GuiResponseEnvelope<Partial<GuiStatus
|
|
|
82
82
|
settings: { ...statusFixture.settings, ...data.settings },
|
|
83
83
|
repos: { ...statusFixture.repos, ...data.repos, repos: data.repos?.repos ?? statusFixture.repos.repos },
|
|
84
84
|
local_repos: { ...statusFixture.local_repos, ...data.local_repos, repos: data.local_repos?.repos ?? statusFixture.local_repos.repos },
|
|
85
|
+
opencode_sessions: { ...statusFixture.opencode_sessions, ...data.opencode_sessions, sessions: data.opencode_sessions?.sessions ?? statusFixture.opencode_sessions.sessions },
|
|
85
86
|
oauth_pool: { ...statusFixture.oauth_pool, ...data.oauth_pool, providers: data.oauth_pool?.providers ?? statusFixture.oauth_pool.providers },
|
|
86
87
|
setup_targets: data.setup_targets ?? statusFixture.setup_targets,
|
|
87
88
|
ai_apps: data.ai_apps ?? statusFixture.ai_apps,
|
|
@@ -206,6 +206,7 @@ code {
|
|
|
206
206
|
.brand-lockup,
|
|
207
207
|
.sidebar-titlebar,
|
|
208
208
|
.sidebar-history-controls,
|
|
209
|
+
.icon-switch,
|
|
209
210
|
.sidebar-mode-tabs,
|
|
210
211
|
.theme-control-heading,
|
|
211
212
|
.appearance-panel-tab,
|
|
@@ -279,6 +280,40 @@ code {
|
|
|
279
280
|
width: 26px;
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
.icon-switch {
|
|
284
|
+
background: color-mix(in srgb, var(--surface-muted) 72%, transparent);
|
|
285
|
+
border: 1px solid var(--border);
|
|
286
|
+
border-radius: 999px;
|
|
287
|
+
gap: 2px;
|
|
288
|
+
padding: 2px;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.icon-switch button {
|
|
292
|
+
align-items: center;
|
|
293
|
+
background: transparent;
|
|
294
|
+
border: 0;
|
|
295
|
+
border-radius: 999px;
|
|
296
|
+
color: var(--text-secondary);
|
|
297
|
+
display: inline-flex;
|
|
298
|
+
gap: 4px;
|
|
299
|
+
height: 28px;
|
|
300
|
+
justify-content: center;
|
|
301
|
+
min-width: 32px;
|
|
302
|
+
padding: 0 8px;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.icon-switch button span {
|
|
306
|
+
display: none;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.icon-switch button.active,
|
|
310
|
+
.icon-switch button:hover,
|
|
311
|
+
.icon-switch button:focus-visible {
|
|
312
|
+
background: var(--surface-elevated);
|
|
313
|
+
color: var(--accent);
|
|
314
|
+
outline: none;
|
|
315
|
+
}
|
|
316
|
+
|
|
282
317
|
.sidebar-history-controls button:hover:not(:disabled),
|
|
283
318
|
.sidebar-history-controls button:focus-visible {
|
|
284
319
|
background: var(--surface-elevated);
|
|
@@ -326,6 +361,97 @@ code {
|
|
|
326
361
|
outline: none;
|
|
327
362
|
}
|
|
328
363
|
|
|
364
|
+
.conversation-panel {
|
|
365
|
+
display: grid;
|
|
366
|
+
gap: 12px;
|
|
367
|
+
padding: 0 6px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.new-session-button {
|
|
371
|
+
background: color-mix(in srgb, var(--accent) 15%, transparent);
|
|
372
|
+
border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
|
|
373
|
+
border-radius: 12px;
|
|
374
|
+
color: var(--accent);
|
|
375
|
+
font-weight: 900;
|
|
376
|
+
padding: 10px 12px;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.repo-session-selector {
|
|
380
|
+
align-items: end;
|
|
381
|
+
display: grid;
|
|
382
|
+
gap: 8px;
|
|
383
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.selector-with-stepper {
|
|
387
|
+
align-items: center;
|
|
388
|
+
display: grid;
|
|
389
|
+
gap: 8px;
|
|
390
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.repo-session-selector .selector-step-button,
|
|
394
|
+
.selector-step-button {
|
|
395
|
+
align-items: center;
|
|
396
|
+
background: var(--surface-muted);
|
|
397
|
+
border: 1px solid var(--border);
|
|
398
|
+
border-radius: 999px;
|
|
399
|
+
color: var(--text-secondary);
|
|
400
|
+
display: inline-flex;
|
|
401
|
+
height: 40px;
|
|
402
|
+
justify-content: center;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.selector-step-button {
|
|
406
|
+
padding: 0;
|
|
407
|
+
width: 40px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.selector-step-button:hover:not(:disabled),
|
|
411
|
+
.selector-step-button:focus-visible {
|
|
412
|
+
border-color: color-mix(in srgb, var(--accent) 40%, transparent);
|
|
413
|
+
color: var(--accent);
|
|
414
|
+
outline: none;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.selector-step-button svg {
|
|
418
|
+
height: 18px;
|
|
419
|
+
stroke-width: 2.5;
|
|
420
|
+
width: 18px;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.repo-session-selector label {
|
|
424
|
+
display: grid;
|
|
425
|
+
gap: 5px;
|
|
426
|
+
min-width: 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.repo-session-selector label span {
|
|
430
|
+
color: var(--text-muted);
|
|
431
|
+
font-size: 0.72rem;
|
|
432
|
+
font-weight: 900;
|
|
433
|
+
text-transform: uppercase;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.repo-session-selector select {
|
|
437
|
+
background: var(--surface-muted);
|
|
438
|
+
border: 1px solid var(--border);
|
|
439
|
+
border-radius: 10px;
|
|
440
|
+
color: var(--text-primary);
|
|
441
|
+
min-height: 40px;
|
|
442
|
+
min-width: 0;
|
|
443
|
+
padding: 8px;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.session-history-list .surface-link {
|
|
447
|
+
min-height: 46px;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.empty-sidebar-state {
|
|
451
|
+
color: var(--text-muted);
|
|
452
|
+
margin: 8px;
|
|
453
|
+
}
|
|
454
|
+
|
|
329
455
|
.sidebar-group {
|
|
330
456
|
margin-bottom: 14px;
|
|
331
457
|
}
|
|
@@ -654,11 +780,16 @@ code {
|
|
|
654
780
|
border: 1px solid var(--border);
|
|
655
781
|
border-radius: 12px;
|
|
656
782
|
color: var(--text-primary);
|
|
783
|
+
min-height: 40px;
|
|
657
784
|
min-width: 0;
|
|
658
785
|
padding: 9px 10px;
|
|
659
786
|
width: 100%;
|
|
660
787
|
}
|
|
661
788
|
|
|
789
|
+
.font-selector-row {
|
|
790
|
+
align-items: center;
|
|
791
|
+
}
|
|
792
|
+
|
|
662
793
|
.app-inset {
|
|
663
794
|
display: grid;
|
|
664
795
|
grid-template-rows: auto minmax(0, 1fr);
|
|
@@ -752,6 +883,10 @@ code {
|
|
|
752
883
|
white-space: nowrap;
|
|
753
884
|
}
|
|
754
885
|
|
|
886
|
+
.workspace-history-controls {
|
|
887
|
+
flex: 0 0 auto;
|
|
888
|
+
}
|
|
889
|
+
|
|
755
890
|
.workspace-search span,
|
|
756
891
|
.workspace-search input {
|
|
757
892
|
color: var(--text-muted);
|
|
@@ -1015,6 +1150,93 @@ code {
|
|
|
1015
1150
|
gap: 16px;
|
|
1016
1151
|
}
|
|
1017
1152
|
|
|
1153
|
+
.chat-surface {
|
|
1154
|
+
display: grid;
|
|
1155
|
+
min-height: 100%;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
.chat-thread-panel {
|
|
1159
|
+
background: var(--surface-elevated);
|
|
1160
|
+
border: 1px solid var(--border);
|
|
1161
|
+
border-radius: 20px;
|
|
1162
|
+
display: grid;
|
|
1163
|
+
grid-template-rows: auto minmax(320px, 1fr) auto;
|
|
1164
|
+
min-height: 620px;
|
|
1165
|
+
overflow: hidden;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
.chat-thread-header {
|
|
1169
|
+
align-items: center;
|
|
1170
|
+
border-bottom: 1px solid var(--border);
|
|
1171
|
+
display: flex;
|
|
1172
|
+
justify-content: space-between;
|
|
1173
|
+
padding: 18px;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
.chat-thread-header h2 {
|
|
1177
|
+
align-items: center;
|
|
1178
|
+
display: flex;
|
|
1179
|
+
gap: 8px;
|
|
1180
|
+
margin: 0;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.chat-message-list {
|
|
1184
|
+
align-content: start;
|
|
1185
|
+
display: grid;
|
|
1186
|
+
gap: 12px;
|
|
1187
|
+
padding: 18px;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
.chat-bubble {
|
|
1191
|
+
border: 1px solid var(--border);
|
|
1192
|
+
border-radius: 16px;
|
|
1193
|
+
display: grid;
|
|
1194
|
+
gap: 6px;
|
|
1195
|
+
max-width: min(680px, 90%);
|
|
1196
|
+
padding: 14px;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
.chat-bubble.assistant {
|
|
1200
|
+
background: var(--surface-muted);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
.chat-bubble.user {
|
|
1204
|
+
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
|
1205
|
+
justify-self: end;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.chat-bubble p {
|
|
1209
|
+
color: var(--text-secondary);
|
|
1210
|
+
margin: 0;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
.chat-composer {
|
|
1214
|
+
border-top: 1px solid var(--border);
|
|
1215
|
+
display: grid;
|
|
1216
|
+
gap: 10px;
|
|
1217
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
1218
|
+
padding: 14px;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
.chat-composer textarea {
|
|
1222
|
+
background: var(--surface-muted);
|
|
1223
|
+
border: 1px solid var(--border);
|
|
1224
|
+
border-radius: 14px;
|
|
1225
|
+
color: var(--text-primary);
|
|
1226
|
+
min-height: 54px;
|
|
1227
|
+
padding: 12px;
|
|
1228
|
+
resize: vertical;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
.chat-composer button {
|
|
1232
|
+
background: color-mix(in srgb, var(--accent) 16%, transparent);
|
|
1233
|
+
border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
|
|
1234
|
+
border-radius: 14px;
|
|
1235
|
+
color: var(--accent);
|
|
1236
|
+
font-weight: 900;
|
|
1237
|
+
padding: 0 18px;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1018
1240
|
.hero-panel,
|
|
1019
1241
|
.panel,
|
|
1020
1242
|
.metric-card,
|
package/setup.sh
CHANGED
|
@@ -12,7 +12,7 @@ shopt -s inherit_errexit 2>/dev/null || true
|
|
|
12
12
|
# AI Assistant Server Access Framework Setup Script
|
|
13
13
|
# Helps developers set up the framework for their infrastructure
|
|
14
14
|
#
|
|
15
|
-
# Version: 3.
|
|
15
|
+
# Version: 3.25.1
|
|
16
16
|
#
|
|
17
17
|
# Quick Install:
|
|
18
18
|
# npm install -g aidevops && aidevops update (recommended)
|