aidevops 3.25.1 → 3.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.25.1-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
61
+ [![Version](https://img.shields.io/badge/Version-3.26.0-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.25.1
1
+ 3.26.0
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.25.1
8
+ # Version: 3.26.0
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.25.1",
3
+ "version": "3.26.0",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -221,14 +221,14 @@ function ConversationSidebar({ conversationMode, selectedLocalRepoIndex, setConv
221
221
  {conversationMode === "people" ? <PeopleChannelList /> : <>
222
222
  <button className="new-session-button" disabled title="Creating sessions needs an audited OpenCode write route." type="button">{text.newSession}</button>
223
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))}>
224
+ <span className="repo-selector-heading" id="local-repo-selector-label">{text.localRepos}</span>
225
+ <div className="selector-with-stepper repo-selector-row">
226
+ <button aria-label="Previous local repo" className="selector-step-button" disabled={!canSelectPreviousRepo} onClick={() => setSelectedLocalRepoIndex(Math.max(0, selectedLocalRepoIndex - 1))} type="button"><FiChevronLeft aria-hidden="true" /></button>
227
+ <select aria-labelledby="local-repo-selector-label" onChange={(event) => setSelectedLocalRepoIndex(Number.parseInt(event.currentTarget.value, 10))} value={Math.min(selectedLocalRepoIndex, Math.max(0, repos.length - 1))}>
228
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
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>
230
+ <button aria-label="Next local repo" className="selector-step-button" disabled={!canSelectNextRepo} onClick={() => setSelectedLocalRepoIndex(Math.min(repos.length - 1, selectedLocalRepoIndex + 1))} type="button"><FiChevronRight aria-hidden="true" /></button>
231
+ </div>
232
232
  </section>
233
233
  <section className="sidebar-group session-history-list">
234
234
  <h2>{text.sessionHistory}</h2>
@@ -1,5 +1,5 @@
1
1
  /* jshint esversion: 11 */
2
- import { type ReactElement, type ReactNode, useEffect, useMemo, useState } from "react";
2
+ import { type ReactElement, type ReactNode, useEffect, useMemo, useRef, useState } from "react";
3
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";
@@ -54,6 +54,7 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
54
54
  status: GuiStatusData;
55
55
  }): ReactElement {
56
56
  const [assistantOpen, setAssistantOpen] = useState(false);
57
+ const [commandInitialQuery, setCommandInitialQuery] = useState("");
57
58
  const [commandOpen, setCommandOpen] = useState(false);
58
59
  const [notificationsOpen, setNotificationsOpen] = useState(false);
59
60
  const [profileOpen, setProfileOpen] = useState(false);
@@ -64,6 +65,14 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
64
65
  const openCommandPalette = (event: KeyboardEvent) => {
65
66
  if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
66
67
  event.preventDefault();
68
+ setCommandInitialQuery("");
69
+ setCommandOpen(true);
70
+ return;
71
+ }
72
+
73
+ if (event.key === "/" && !isEditableShortcutTarget(event.target)) {
74
+ event.preventDefault();
75
+ setCommandInitialQuery("/");
67
76
  setCommandOpen(true);
68
77
  }
69
78
  };
@@ -92,16 +101,18 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
92
101
  <h1>{activeItem.label}</h1>
93
102
  </div>
94
103
  </div>
95
- <div className="header-actions">
104
+ <div className="header-center-controls">
96
105
  <div className="sidebar-history-controls workspace-history-controls">
97
106
  <button aria-label="Previous surface" disabled={!canGoBack} onClick={goBack} type="button"><FiChevronLeft /></button>
98
107
  <button aria-label="Next surface" disabled={!canGoForward} onClick={goForward} type="button"><FiChevronRight /></button>
99
108
  </div>
100
- <button className="workspace-search command-trigger" onClick={() => setCommandOpen(true)} type="button">
109
+ <button className="workspace-search command-trigger" onClick={() => { setCommandInitialQuery(""); setCommandOpen(true); }} type="button">
101
110
  <FiSearch aria-hidden="true" />
102
111
  <span>⌘K</span>
103
112
  <strong>{text.searchPlaceholder}</strong>
104
113
  </button>
114
+ </div>
115
+ <div className="header-actions">
105
116
  <div className="header-action-menu">
106
117
  <button aria-expanded={notificationsOpen} aria-label="Open notifications" className="header-icon-button" onClick={() => { setNotificationsOpen((current) => !current); setProfileOpen(false); }} type="button">
107
118
  <FiBell aria-hidden="true" />
@@ -120,11 +131,19 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
120
131
  </div>
121
132
  </div>
122
133
  {assistantOpen ? <AssistantPanel activeSurface={activeSurface} userName={userName} /> : null}
123
- {commandOpen ? <CommandPalette close={() => setCommandOpen(false)} openSurface={openSurface} /> : null}
134
+ {commandOpen ? <CommandPalette close={() => setCommandOpen(false)} initialQuery={commandInitialQuery} openSurface={openSurface} status={status} /> : null}
124
135
  </header>
125
136
  );
126
137
  }
127
138
 
139
+ function isEditableShortcutTarget(target: EventTarget | null): boolean {
140
+ if (!(target instanceof HTMLElement)) {
141
+ return false;
142
+ }
143
+
144
+ return target.isContentEditable || ["INPUT", "SELECT", "TEXTAREA"].includes(target.tagName);
145
+ }
146
+
128
147
  function NotificationsMenu({ openSurface }: { openSurface: (surface: SurfaceId) => void }): ReactElement {
129
148
  return (
130
149
  <div className="popover-menu notifications-menu" role="menu">
@@ -216,14 +235,79 @@ function AssistantPanel({ activeSurface, userName }: { activeSurface: SurfaceId;
216
235
  );
217
236
  }
218
237
 
219
- function CommandPalette({ close, openSurface }: { close: () => void; openSurface: (surface: SurfaceId) => void }): ReactElement {
220
- const [query, setQuery] = useState("");
238
+ interface CommandPaletteItem {
239
+ description: string;
240
+ icon: SurfaceNavItem["icon"];
241
+ id: string;
242
+ label: string;
243
+ searchText: string;
244
+ surface: SurfaceId;
245
+ tag: string;
246
+ }
247
+
248
+ const slashCommandItems: CommandPaletteItem[] = [
249
+ { id: "slash-add-device", label: "/add-device", description: "Start device pairing setup", icon: "device", searchText: "/add-device add device pair machine", surface: "devices", tag: "Command" },
250
+ { id: "slash-add-local-repo", label: "/add-local-repo", description: "Add a local repository path", icon: "folder", searchText: "/add-local-repo add local repo git", surface: "git", tag: "Command" },
251
+ { id: "slash-add-remote-repo", label: "/add-remote-repo", description: "Register a remote repository", icon: "git", searchText: "/add-remote-repo add remote repo", surface: "projects", tag: "Command" },
252
+ { id: "slash-add-secret", label: "/add-secret", description: "Add a protected secret reference", icon: "shield", searchText: "/add-secret add secret vault", surface: "security", tag: "Command" },
253
+ { id: "slash-add-ai-provider", label: "/add-ai-provider", description: "Connect an AI provider account", icon: "users", searchText: "/add-ai-provider add ai provider oauth", surface: "aiProviders", tag: "Command" },
254
+ ];
255
+
256
+ const plannedChannelItems: CommandPaletteItem[] = [
257
+ { id: "channel-devops", label: "#devops", description: "Chat channel for aidevops operations", icon: "message", searchText: "#devops devops channel chat", surface: "messagingAccounts", tag: "Channel" },
258
+ { id: "channel-comms", label: "#comms", description: "Chat channel for communications", icon: "message", searchText: "#comms comms channel chat", surface: "messagingAccounts", tag: "Channel" },
259
+ ];
260
+
261
+ function CommandPalette({ close, initialQuery, openSurface, status }: { close: () => void; initialQuery: string; openSurface: (surface: SurfaceId) => void; status: GuiStatusData }): ReactElement {
262
+ const [query, setQuery] = useState(initialQuery);
263
+ const inputRef = useRef<HTMLInputElement>(null);
221
264
  const matches = useMemo(() => {
222
265
  const normalizedQuery = query.trim().toLowerCase();
223
- return orderedNavItems
224
- .filter((item) => normalizedQuery.length === 0 || `${item.label} ${item.description}`.toLowerCase().includes(normalizedQuery))
266
+ const surfaceItems: CommandPaletteItem[] = orderedNavItems.map((item) => ({
267
+ description: item.description,
268
+ icon: item.icon,
269
+ id: `surface-${item.id}`,
270
+ label: item.label,
271
+ searchText: `${item.label} ${item.description}`,
272
+ surface: item.id,
273
+ tag: "Surface",
274
+ }));
275
+ const sessionItems: CommandPaletteItem[] = status.opencode_sessions.sessions.map((session) => ({
276
+ description: `AI session in ${session.repo_path_ref}`,
277
+ icon: "terminal",
278
+ id: `session-${session.id_ref}`,
279
+ label: `#${session.title}`,
280
+ searchText: `#${session.title} ${session.title} ${session.repo_path_ref} ${session.agent} ${session.model}`,
281
+ surface: "git",
282
+ tag: "AI session",
283
+ }));
284
+ const directMessageItems: CommandPaletteItem[] = [{
285
+ description: "Direct message thread placeholder",
286
+ icon: "users",
287
+ id: "dm-local-user",
288
+ label: `@${status.machine.username || "local-user"}`,
289
+ searchText: `@${status.machine.username || "local-user"} direct message dm person local user`,
290
+ surface: "messagingAccounts",
291
+ tag: "Direct message",
292
+ }];
293
+ const items = [...surfaceItems, ...sessionItems, ...plannedChannelItems, ...directMessageItems, ...slashCommandItems];
294
+ const prefix = normalizedQuery.charAt(0);
295
+ const scopedItems = prefix === "/"
296
+ ? slashCommandItems
297
+ : prefix === "@"
298
+ ? directMessageItems
299
+ : prefix === "#"
300
+ ? [...sessionItems, ...plannedChannelItems]
301
+ : items;
302
+
303
+ return scopedItems
304
+ .filter((item) => normalizedQuery.length === 0 || item.searchText.toLowerCase().includes(normalizedQuery))
225
305
  .slice(0, 8);
226
- }, [query]);
306
+ }, [query, status.machine.username, status.opencode_sessions.sessions]);
307
+
308
+ useEffect(() => {
309
+ inputRef.current?.focus();
310
+ }, []);
227
311
 
228
312
  useEffect(() => {
229
313
  const closeOnEscape = (event: KeyboardEvent) => {
@@ -242,14 +326,14 @@ function CommandPalette({ close, openSurface }: { close: () => void; openSurface
242
326
  <section aria-label="Command palette" className="command-palette">
243
327
  <label className="command-input-row">
244
328
  <FiSearch aria-hidden="true" />
245
- <input onChange={(event) => setQuery(event.currentTarget.value)} placeholder="Search commands and surfaces" value={query} />
329
+ <input ref={inputRef} onChange={(event) => setQuery(event.currentTarget.value)} placeholder="Search commands, #sessions, #channels, @people, or /actions" value={query} />
246
330
  </label>
247
331
  <ul>
248
332
  {matches.map((item) => (
249
333
  <li key={item.id}>
250
- <button onClick={() => openSurface(item.id)} type="button">
334
+ <button onClick={() => openSurface(item.surface)} type="button">
251
335
  <span className="surface-icon" aria-hidden="true"><SurfaceGlyph icon={item.icon} /></span>
252
- <span><strong>{item.label}</strong><small>{item.description}</small></span>
336
+ <span><strong>{item.label}</strong><small>{item.tag} · {item.description}</small></span>
253
337
  </button>
254
338
  </li>
255
339
  ))}
@@ -211,6 +211,7 @@ code {
211
211
  .theme-control-heading,
212
212
  .appearance-panel-tab,
213
213
  .header-title,
214
+ .header-center-controls,
214
215
  .header-actions,
215
216
  .workspace-search,
216
217
  .version-pill,
@@ -377,10 +378,8 @@ code {
377
378
  }
378
379
 
379
380
  .repo-session-selector {
380
- align-items: end;
381
381
  display: grid;
382
- gap: 8px;
383
- grid-template-columns: auto minmax(0, 1fr) auto;
382
+ gap: 7px;
384
383
  }
385
384
 
386
385
  .selector-with-stepper {
@@ -420,19 +419,18 @@ code {
420
419
  width: 18px;
421
420
  }
422
421
 
423
- .repo-session-selector label {
424
- display: grid;
425
- gap: 5px;
426
- min-width: 0;
427
- }
428
-
429
- .repo-session-selector label span {
422
+ .repo-selector-heading {
430
423
  color: var(--text-muted);
431
424
  font-size: 0.72rem;
432
425
  font-weight: 900;
426
+ text-align: center;
433
427
  text-transform: uppercase;
434
428
  }
435
429
 
430
+ .repo-selector-row {
431
+ align-items: center;
432
+ }
433
+
436
434
  .repo-session-selector select {
437
435
  background: var(--surface-muted);
438
436
  border: 1px solid var(--border);
@@ -801,9 +799,9 @@ code {
801
799
  background: color-mix(in srgb, var(--surface-elevated) 94%, transparent);
802
800
  border-bottom: 1px solid var(--border);
803
801
  display: flex;
804
- gap: 16px;
805
- min-height: 58px;
806
- padding: 10px 14px;
802
+ gap: 12px;
803
+ min-height: 54px;
804
+ padding: 8px 14px;
807
805
  position: sticky;
808
806
  top: 0;
809
807
  z-index: 2;
@@ -811,7 +809,7 @@ code {
811
809
 
812
810
  .header-title {
813
811
  flex: 0 0 auto;
814
- gap: 10px;
812
+ gap: 8px;
815
813
  min-width: 0;
816
814
  }
817
815
 
@@ -819,12 +817,14 @@ code {
819
817
  color: var(--text-muted);
820
818
  font-size: 0.76rem;
821
819
  font-weight: 900;
820
+ line-height: 1.15;
822
821
  margin: 0;
823
822
  text-transform: uppercase;
824
823
  }
825
824
 
826
825
  .header-title h1 {
827
826
  font-size: 1rem;
827
+ line-height: 1.15;
828
828
  margin: 0;
829
829
  }
830
830
 
@@ -840,15 +840,25 @@ code {
840
840
  align-items: center;
841
841
  color: var(--accent);
842
842
  display: inline-flex;
843
- height: 34px;
843
+ height: 32px;
844
844
  justify-content: center;
845
- width: 34px;
845
+ width: 32px;
846
846
  }
847
847
 
848
848
  .workspace-surface-icon svg {
849
- height: 24px;
849
+ height: 23px;
850
850
  stroke-width: 2.2;
851
- width: 24px;
851
+ width: 23px;
852
+ }
853
+
854
+ .header-center-controls {
855
+ gap: 10px;
856
+ left: 50%;
857
+ max-width: min(560px, 48vw);
858
+ min-width: 0;
859
+ position: absolute;
860
+ transform: translateX(-50%);
861
+ width: min(560px, 48vw);
852
862
  }
853
863
 
854
864
  .header-actions {
@@ -871,7 +881,7 @@ code {
871
881
 
872
882
  .command-trigger {
873
883
  justify-content: flex-start;
874
- max-width: 340px;
884
+ max-width: none;
875
885
  }
876
886
 
877
887
  .command-trigger strong {
@@ -1055,12 +1065,11 @@ code {
1055
1065
  }
1056
1066
 
1057
1067
  .command-palette-backdrop {
1058
- align-items: flex-start;
1068
+ align-items: center;
1059
1069
  background: rgb(0 0 0 / 28%);
1060
1070
  display: flex;
1061
1071
  inset: 0;
1062
1072
  justify-content: center;
1063
- padding-top: 12vh;
1064
1073
  position: fixed;
1065
1074
  z-index: 20;
1066
1075
  }
@@ -1083,6 +1092,7 @@ code {
1083
1092
  max-width: min(680px, calc(100vw - 32px));
1084
1093
  padding: 14px;
1085
1094
  position: relative;
1095
+ transform: translateY(-2vh);
1086
1096
  width: 100%;
1087
1097
  }
1088
1098
 
@@ -2102,12 +2112,20 @@ code {
2102
2112
  }
2103
2113
 
2104
2114
  .workspace-header,
2115
+ .header-center-controls,
2105
2116
  .header-actions,
2106
2117
  .split-heading {
2107
2118
  align-items: stretch;
2108
2119
  flex-direction: column;
2109
2120
  }
2110
2121
 
2122
+ .header-center-controls {
2123
+ max-width: none;
2124
+ position: static;
2125
+ transform: none;
2126
+ width: 100%;
2127
+ }
2128
+
2111
2129
  .workspace-search {
2112
2130
  min-width: 0;
2113
2131
  width: 100%;
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.25.1
15
+ # Version: 3.26.0
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)