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 CHANGED
@@ -58,7 +58,7 @@ The result: an AI operations platform that manages projects across every busines
58
58
  [![Copyright](https://img.shields.io/badge/Copyright-Marcus%20Quinn%202025--2026-blue.svg)](https://github.com/marcusquinn)
59
59
 
60
60
  <!-- Release & Version Info -->
61
- [![Version](https://img.shields.io/badge/Version-3.24.2-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
61
+ [![Version](https://img.shields.io/badge/Version-3.25.1-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
62
62
  [![npm version](https://img.shields.io/npm/v/aidevops)](https://www.npmjs.com/package/aidevops)
63
63
  [![Homebrew](https://img.shields.io/badge/homebrew-marcusquinn%2Ftap-orange)](https://github.com/marcusquinn/homebrew-tap)
64
64
  [![GitHub repository](https://img.shields.io/badge/github-repository-181717.svg?logo=github)](https://github.com/marcusquinn/aidevops)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.24.2
1
+ 3.25.1
package/aidevops.sh CHANGED
@@ -5,7 +5,7 @@
5
5
  # AI DevOps Framework CLI
6
6
  # Usage: aidevops <command> [options]
7
7
  #
8
- # Version: 3.24.2
8
+ # Version: 3.25.1
9
9
 
10
10
  set -euo pipefail
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.24.2",
3
+ "version": "3.25.1",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -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: readLocalReposSetupSummary(reposPath),
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
- canGoBack={canGoBack}
177
- canGoForward={canGoForward}
183
+ conversationMode={conversationMode}
178
184
  fontSizePreference={fontSizePreference}
179
185
  fontPreference={fontPreference}
180
- goBack={goBack}
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 activeItem={activeItem} activeSectionLabel={activeSectionLabel} activeSurface={activeSurface} fileRoot={fileRoot} setActiveSurface={setActiveSurface} status={status.data} />
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, canGoBack, canGoForward, fontPreference, fontSizePreference, goBack, goForward, setAccentHue, setActiveSurface, setFontPreference, setFontSizePreference, setShowBorders, setShowNavCounts, setThemePreference, showBorders, showNavCounts, status, themePreference }: {
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
- canGoBack: boolean;
113
- canGoForward: boolean;
113
+ conversationMode: ConversationMode;
114
114
  fontPreference: FontPreference;
115
115
  fontSizePreference: FontSizePreference;
116
- goBack: () => void;
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 canGoBack={canGoBack} canGoForward={canGoForward} goBack={goBack} goForward={goForward} />
143
+ <SidebarHeader setShellMode={setShellMode} shellMode={shellMode} />
141
144
  <nav className="sidebar-content">
142
- <SidebarModeTabs mode={sidebarMode} setMode={setSidebarMode} />
143
- <ul className="sidebar-top-link">
144
- <SidebarItem activeSurface={activeSurface} item={dashboardNavItem} setActiveSurface={setActiveSurface} showCount={false} status={status} />
145
- </ul>
146
- {visibleGroups.map((group) => <SidebarGroup activeSurface={activeSurface} group={group} key={group.label} recordCounts={recordCounts} setActiveSurface={setActiveSurface} showNavCounts={showNavCounts} status={status} />)}
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 SidebarModeTabs({ mode, setMode }: {
167
- mode: SidebarMode;
168
- setMode: (mode: SidebarMode) => void;
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: Array<{ label: string; value: SidebarMode }> = [
171
- { label: text.devops, value: "devops" },
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({ canGoBack, canGoForward, goBack, goForward }: {
250
- canGoBack: boolean;
251
- canGoForward: boolean;
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
- <div className="sidebar-history-controls">
263
- <button aria-label="Previous surface" disabled={!canGoBack} onClick={goBack} type="button"><FiChevronLeft /></button>
264
- <button aria-label="Next surface" disabled={!canGoForward} onClick={goForward} type="button"><FiChevronRight /></button>
265
- </div>
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
- <label className="font-control">
389
- <span>{text.font}</span>
390
- <select
391
- onChange={(event) => setFontPreference(event.currentTarget.value as FontPreference)}
392
- style={{ fontFamily: selectedFontFamily }}
393
- value={fontPreference}
394
- >
395
- {fontOptions.map((option) => (
396
- <option key={option.value} style={{ fontFamily: option.fontFamily }} value={option.value}>
397
- {option.label}
398
- </option>
399
- ))}
400
- </select>
401
- </label>
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
- <SurfaceContent activeItem={activeItem} activeSurface={activeSurface} fileRoot={fileRoot} status={status} />
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.24.2
15
+ # Version: 3.25.1
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)