aidevops 3.27.0 → 3.28.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.27.0-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
61
+ [![Version](https://img.shields.io/badge/Version-3.28.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.27.0
1
+ 3.28.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.27.0
8
+ # Version: 3.28.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.27.0",
3
+ "version": "3.28.0",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -212,6 +212,8 @@ export function App(): ReactElement {
212
212
  goForward={goForward}
213
213
  selectedLocalRepoIndex={selectedLocalRepoIndex}
214
214
  setActiveSurface={setActiveSurface}
215
+ setConversationMode={setConversationMode}
216
+ setShellMode={setShellMode}
215
217
  shellMode={shellMode}
216
218
  status={status.data}
217
219
  />
@@ -20,6 +20,7 @@ import {
20
20
  FiGrid,
21
21
  FiHash,
22
22
  FiHardDrive,
23
+ FiHelpCircle,
23
24
  FiLink,
24
25
  FiLink2,
25
26
  FiList,
@@ -54,6 +55,8 @@ const surfaceIcons: Record<SurfaceIconName, IconType> = {
54
55
  globe: FiGlobe,
55
56
  grid: FiGrid,
56
57
  hardDrive: FiHardDrive,
58
+ hash: FiHash,
59
+ help: FiHelpCircle,
57
60
  link: FiLink,
58
61
  list: FiList,
59
62
  lock: FiLock,
@@ -1,6 +1,6 @@
1
1
  /* jshint esversion: 11 */
2
2
  import { type ReactElement, type ReactNode, useEffect, useState } from "react";
3
- import { FiBell, FiChevronLeft, FiChevronRight, FiCommand, FiCpu, FiGlobe, FiHash, FiLogOut, FiMessageSquare, FiSearch, FiSettings, FiShield, FiUser } from "react-icons/fi";
3
+ import { FiBell, FiChevronLeft, FiChevronRight, FiCommand, FiGlobe, FiHash, FiHelpCircle, FiLogOut, FiMessageSquare, FiSearch, FiSettings, FiShield, FiTerminal, FiUser } from "react-icons/fi";
4
4
  import type { GuiFileRootId, GuiStatusData } from "../../gui-shared/src";
5
5
  import { SurfaceGlyph } from "./AppNavigation";
6
6
  import type { ConversationMode, ShellMode, SurfaceId, SurfaceNavItem } from "./app-model";
@@ -16,7 +16,7 @@ const communityLinks = {
16
16
  x: "https://x.com/marcuswquinn",
17
17
  } as const;
18
18
 
19
- export function Workspace({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, conversationMode, fileRoot, goBack, goForward, selectedLocalRepoIndex, setActiveSurface, shellMode, status }: {
19
+ export function Workspace({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, conversationMode, fileRoot, goBack, goForward, selectedLocalRepoIndex, setActiveSurface, setConversationMode, setShellMode, shellMode, status }: {
20
20
  activeItem: SurfaceNavItem;
21
21
  activeSectionLabel: string;
22
22
  activeSurface: SurfaceId;
@@ -28,12 +28,14 @@ export function Workspace({ activeItem, activeSectionLabel, activeSurface, canGo
28
28
  goForward: () => void;
29
29
  selectedLocalRepoIndex: number;
30
30
  setActiveSurface: (surface: SurfaceId) => void;
31
+ setConversationMode: (mode: ConversationMode) => void;
32
+ setShellMode: (mode: ShellMode) => void;
31
33
  shellMode: ShellMode;
32
34
  status: GuiStatusData;
33
35
  }) {
34
36
  return (
35
37
  <section className="app-inset" aria-label={text.workspaceLabel}>
36
- <WorkspaceHeader activeItem={activeItem} activeSectionLabel={activeSectionLabel} activeSurface={activeSurface} canGoBack={canGoBack} canGoForward={canGoForward} goBack={goBack} goForward={goForward} setActiveSurface={setActiveSurface} status={status} />
38
+ <WorkspaceHeader activeItem={activeItem} activeSectionLabel={activeSectionLabel} activeSurface={activeSurface} canGoBack={canGoBack} canGoForward={canGoForward} goBack={goBack} goForward={goForward} setActiveSurface={setActiveSurface} setConversationMode={setConversationMode} setShellMode={setShellMode} status={status} />
37
39
  <div className="workspace-scroll">
38
40
  {shellMode === "sessions"
39
41
  ? <ConversationWorkspace conversationMode={conversationMode} selectedLocalRepoIndex={selectedLocalRepoIndex} status={status} />
@@ -43,7 +45,7 @@ export function Workspace({ activeItem, activeSectionLabel, activeSurface, canGo
43
45
  );
44
46
  }
45
47
 
46
- function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, goBack, goForward, setActiveSurface, status }: {
48
+ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoBack, canGoForward, goBack, goForward, setActiveSurface, setConversationMode, setShellMode, status }: {
47
49
  activeItem: SurfaceNavItem;
48
50
  activeSectionLabel: string;
49
51
  activeSurface: SurfaceId;
@@ -52,6 +54,8 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
52
54
  goBack: () => void;
53
55
  goForward: () => void;
54
56
  setActiveSurface: (surface: SurfaceId) => void;
57
+ setConversationMode: (mode: ConversationMode) => void;
58
+ setShellMode: (mode: ShellMode) => void;
55
59
  status: GuiStatusData;
56
60
  }): ReactElement {
57
61
  const [assistantOpen, setAssistantOpen] = useState(false);
@@ -64,31 +68,30 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
64
68
 
65
69
  useEffect(() => {
66
70
  const openCommandPalette = (event: KeyboardEvent) => {
67
- if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
68
- event.preventDefault();
69
- setCommandInitialQuery("");
70
- setCommandOpen(true);
71
- return;
72
- }
73
-
74
- const shortcutQuery = commandPaletteShortcutQuery(event);
75
- if (shortcutQuery !== undefined && !isEditableShortcutTarget(event.target)) {
76
- event.preventDefault();
77
- setCommandInitialQuery(shortcutQuery);
78
- setCommandOpen(true);
79
- }
71
+ handleWorkspaceShortcut(event, {
72
+ goBack,
73
+ goForward,
74
+ openCommand: (query) => {
75
+ setCommandInitialQuery(query);
76
+ setCommandOpen(true);
77
+ },
78
+ });
80
79
  };
81
80
 
82
81
  window.addEventListener("keydown", openCommandPalette);
83
82
  return () => window.removeEventListener("keydown", openCommandPalette);
84
- }, []);
83
+ }, [goBack, goForward]);
85
84
 
86
85
  const closeMenus = () => {
87
86
  setNotificationsOpen(false);
88
87
  setProfileOpen(false);
89
88
  };
90
89
 
91
- const openSurface = (surface: SurfaceId) => {
90
+ const openItem = ({ conversationMode, shellMode, surface }: { conversationMode?: ConversationMode; shellMode?: ShellMode; surface: SurfaceId }) => {
91
+ setShellMode(shellMode ?? "devices");
92
+ if (conversationMode) {
93
+ setConversationMode(conversationMode);
94
+ }
92
95
  setActiveSurface(surface);
93
96
  closeMenus();
94
97
  setCommandOpen(false);
@@ -116,28 +119,45 @@ function WorkspaceHeader({ activeItem, activeSectionLabel, activeSurface, canGoB
116
119
  </div>
117
120
  <div className="header-actions">
118
121
  <div className="header-action-menu">
122
+ <button aria-label="Open help" className="header-icon-button" onClick={() => openItem({ surface: "help" })} title="Help (?)" type="button">
123
+ <FiHelpCircle aria-hidden="true" />
124
+ </button>
119
125
  <button aria-expanded={notificationsOpen} aria-label="Open notifications" className="header-icon-button" onClick={() => { setNotificationsOpen((current) => !current); setProfileOpen(false); }} type="button">
120
126
  <FiBell aria-hidden="true" />
121
127
  <span className="notification-dot" aria-hidden="true" />
122
128
  </button>
123
- {notificationsOpen ? <NotificationsMenu openSurface={openSurface} /> : null}
129
+ {notificationsOpen ? <NotificationsMenu openSurface={(surface) => openItem({ surface })} /> : null}
124
130
  </div>
125
- <button aria-pressed={assistantOpen} aria-label="Toggle AI Assistant" className={assistantOpen ? "header-icon-button active" : "header-icon-button"} onClick={() => setAssistantOpen((current) => !current)} type="button">
126
- <FiCpu aria-hidden="true" />
131
+ <button aria-pressed={assistantOpen} aria-label="Toggle AI Assistant" className={assistantOpen ? "header-icon-button active" : "header-icon-button"} onClick={() => setAssistantOpen((current) => !current)} title="AI sessions (_)" type="button">
132
+ <FiTerminal aria-hidden="true" />
127
133
  </button>
128
134
  <div className="header-action-menu">
129
135
  <button aria-expanded={profileOpen} aria-label={`Open profile menu for ${userName}`} className="profile-avatar-button" onClick={() => { setProfileOpen((current) => !current); setNotificationsOpen(false); }} title={userName} type="button">
130
136
  <span>{userInitials}</span>
131
137
  </button>
132
- {profileOpen ? <ProfileMenu openSurface={openSurface} userName={userName} /> : null}
138
+ {profileOpen ? <ProfileMenu openSurface={(surface) => openItem({ surface })} userName={userName} /> : null}
133
139
  </div>
134
140
  </div>
135
141
  {assistantOpen ? <AssistantPanel activeSurface={activeSurface} userName={userName} /> : null}
136
- {commandOpen ? <CommandPalette close={() => setCommandOpen(false)} initialQuery={commandInitialQuery} openSurface={openSurface} status={status} /> : null}
142
+ {commandOpen ? <CommandPalette activeSurface={activeSurface} close={() => setCommandOpen(false)} initialQuery={commandInitialQuery} openItem={openItem} status={status} /> : null}
137
143
  </header>
138
144
  );
139
145
  }
140
146
 
147
+ function handleWorkspaceShortcut(event: KeyboardEvent, actions: { goBack: () => void; goForward: () => void; openCommand: (query: string) => void }): void {
148
+ const shortcutQuery = commandPaletteShortcutQuery(event);
149
+ const isEditableTarget = isEditableShortcutTarget(event.target);
150
+ const commandQuery = (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k" ? "" : shortcutQuery;
151
+
152
+ if (commandQuery !== undefined && !isEditableTarget) {
153
+ event.preventDefault();
154
+ actions.openCommand(commandQuery);
155
+ } else if (event.key === "Backspace" && !isEditableTarget) {
156
+ event.preventDefault();
157
+ (event.shiftKey ? actions.goForward : actions.goBack)();
158
+ }
159
+ }
160
+
141
161
  function isEditableShortcutTarget(target: EventTarget | null): boolean {
142
162
  if (!(target instanceof HTMLElement)) {
143
163
  return false;
@@ -247,6 +267,7 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, status }: {
247
267
  const vaultCollection = vaultCollectionForSurface(status.vault, activeSurface);
248
268
  const staticSurfaces: Partial<Record<SurfaceId, ReactNode>> = {
249
269
  overview: <OverviewSurface status={status} />,
270
+ help: <HelpSurface />,
250
271
  settings: <SettingsSurface status={status} />,
251
272
  notifications: <NotificationsSurface />,
252
273
  admin: <AdminSurface />,
@@ -300,6 +321,40 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, status }: {
300
321
  return staticSurfaces[activeSurface] ?? null;
301
322
  }
302
323
 
324
+ function HelpSurface(): ReactElement {
325
+ const shortcuts = [
326
+ ["#", "Channels"],
327
+ ["_", "AI sessions"],
328
+ ["&", "Infrastructure"],
329
+ ["?", "Help"],
330
+ [">", "New terminal command"],
331
+ ["+", "Add commands"],
332
+ ["-", "Remove commands"],
333
+ ["/", "Slash commands"],
334
+ ["=", "Comms"],
335
+ ["~", "Settings and config"],
336
+ ["!", "Notifications"],
337
+ [".", "Agents"],
338
+ ["*", "Secrets and passwords"],
339
+ [":", "Emoji picker"],
340
+ ["^", "Copy current page link"],
341
+ ];
342
+
343
+ return (
344
+ <section className="settings-surface help-surface">
345
+ <div className="planned-card">
346
+ <h2>Help</h2>
347
+ <p>Use the command palette shortcuts from anywhere outside text inputs. Recent selections appear first until you keep typing to filter.</p>
348
+ </div>
349
+ <ul className="shortcut-list">
350
+ {shortcuts.map(([shortcut, label]) => <li key={shortcut}><kbd>{shortcut}</kbd><span>{label}</span></li>)}
351
+ <li><kbd>Backspace</kbd><span>Previous page</span></li>
352
+ <li><kbd>Shift Backspace</kbd><span>Next page</span></li>
353
+ </ul>
354
+ </section>
355
+ );
356
+ }
357
+
303
358
  function SettingsSurface({ status }: { status: GuiStatusData }): ReactElement {
304
359
  return (
305
360
  <section className="settings-surface">
@@ -3,22 +3,26 @@ import { type ReactElement, useCallback, useEffect, useMemo, useRef, useState }
3
3
  import { createPortal } from "react-dom";
4
4
  import { FiSearch } from "react-icons/fi";
5
5
  import { SurfaceGlyph } from "./AppNavigation";
6
- import type { SurfaceId, SurfaceNavItem } from "./app-model";
7
- import { orderedNavItems } from "./app-model";
6
+ import type { ConversationMode, ShellMode, SurfaceId, SurfaceIconName, SurfaceNavItem } from "./app-model";
7
+ import { navGroups, orderedNavItems, utilityNavItems } from "./app-model";
8
8
  import type { GuiStatusData } from "../../gui-shared/src";
9
9
 
10
10
  interface CommandPaletteItem {
11
+ action?: "copy-current-link";
11
12
  description: string;
12
- icon: SurfaceNavItem["icon"];
13
+ conversationMode?: ConversationMode;
14
+ icon: SurfaceIconName;
15
+ iconGlyph?: string;
13
16
  id: string;
14
17
  label: string;
15
18
  searchText: string;
19
+ shellMode?: ShellMode;
16
20
  surface: SurfaceId;
17
21
  tag: string;
18
22
  }
19
23
 
20
24
  const commandPaletteRecentStorageKey = "aidevops-gui-command-palette-recents";
21
- const commandPaletteRecentLimit = 7;
25
+ const commandPaletteRecentLimit = 5;
22
26
 
23
27
  const slashCommandItems: CommandPaletteItem[] = [
24
28
  { 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" },
@@ -28,13 +32,41 @@ const slashCommandItems: CommandPaletteItem[] = [
28
32
  { 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" },
29
33
  ];
30
34
 
35
+ const addCommandItems: CommandPaletteItem[] = slashCommandItems.map((item) => {
36
+ const label = item.label.replace("/", "+");
37
+
38
+ return { ...item, id: item.id.replace("slash-", "plus-"), label, searchText: `${label} ${item.searchText}`, tag: "Add" };
39
+ });
40
+
41
+ const removeCommandItems: CommandPaletteItem[] = [
42
+ { id: "remove-device", label: "-device", description: "Remove a paired device placeholder", icon: "device", searchText: "-device remove device unpair machine", surface: "devices", tag: "Remove" },
43
+ { id: "remove-local-repo", label: "-local-repo", description: "Remove a local repository reference placeholder", icon: "folder", searchText: "-local-repo remove local repo git", surface: "git", tag: "Remove" },
44
+ { id: "remove-secret", label: "-secret", description: "Remove a protected secret reference placeholder", icon: "shield", searchText: "-secret remove secret vault password", surface: "security", tag: "Remove" },
45
+ ];
46
+
47
+ const terminalCommandItems: CommandPaletteItem[] = [
48
+ { id: "terminal-new-command", label: "> new terminal command", description: "Send a command to a new terminal session", icon: "terminal", searchText: "> terminal command new session shell", surface: "localSetup", tag: "Terminal" },
49
+ ];
50
+
51
+ const emojiItems: CommandPaletteItem[] = [
52
+ { id: "emoji-thumbs-up", label: ":thumbsup:", description: "Copy or insert 👍", icon: "message", iconGlyph: "👍", searchText: ":thumbsup thumbs up approve emoji", surface: "messagingAccounts", tag: "Emoji" },
53
+ { id: "emoji-check", label: ":white_check_mark:", description: "Copy or insert ✅", icon: "message", iconGlyph: "✅", searchText: ":white_check_mark check done emoji", surface: "messagingAccounts", tag: "Emoji" },
54
+ { id: "emoji-eyes", label: ":eyes:", description: "Copy or insert 👀", icon: "message", iconGlyph: "👀", searchText: ":eyes review look emoji", surface: "messagingAccounts", tag: "Emoji" },
55
+ ];
56
+
31
57
  const plannedChannelItems: CommandPaletteItem[] = [
32
- { id: "channel-devops", label: "#devops", description: "Chat channel for aidevops operations", icon: "message", searchText: "#devops devops channel chat", surface: "messagingAccounts", tag: "Channel" },
33
- { id: "channel-comms", label: "#comms", description: "Chat channel for communications", icon: "message", searchText: "#comms comms channel chat", surface: "messagingAccounts", tag: "Channel" },
58
+ { id: "channel-devops", label: "#devops", description: "Chat channel for aidevops operations", icon: "hash", searchText: "#devops devops channel chat", surface: "messagingAccounts", shellMode: "sessions", conversationMode: "people", tag: "Channel" },
59
+ { id: "channel-comms", label: "#comms", description: "Chat channel for communications", icon: "hash", searchText: "#comms comms channel chat", surface: "messagingAccounts", shellMode: "sessions", conversationMode: "people", tag: "Channel" },
34
60
  ];
35
61
 
36
- export function CommandPalette({ close, initialQuery, openSurface, status }: { close: () => void; initialQuery: string; openSurface: (surface: SurfaceId) => void; status: GuiStatusData }): ReactElement {
37
- const allItems = useMemo(() => commandPaletteItems(status), [status]);
62
+ export interface CommandPaletteSelection {
63
+ conversationMode?: ConversationMode;
64
+ shellMode?: ShellMode;
65
+ surface: SurfaceId;
66
+ }
67
+
68
+ export function CommandPalette({ activeSurface, close, initialQuery, openItem, status }: { activeSurface: SurfaceId; close: () => void; initialQuery: string; openItem: (selection: CommandPaletteSelection) => void; status: GuiStatusData }): ReactElement {
69
+ const allItems = useMemo(() => commandPaletteItems(status, activeSurface), [activeSurface, status]);
38
70
  const [query, setQuery] = useState(initialQuery);
39
71
  const [activeIndex, setActiveIndex] = useState(0);
40
72
  const [recentIds, setRecentIds] = useState<string[]>(() => readCommandPaletteRecentIds());
@@ -45,9 +77,12 @@ export function CommandPalette({ close, initialQuery, openSurface, status }: { c
45
77
  if (item) {
46
78
  const nextRecentIds = rememberCommandPaletteItemId(item.id, recentIds);
47
79
  setRecentIds(nextRecentIds);
48
- openSurface(item.surface);
80
+ if (item.action === "copy-current-link") {
81
+ void copyCurrentSurfaceLink(item.surface);
82
+ }
83
+ openItem({ conversationMode: item.conversationMode, shellMode: item.shellMode, surface: item.surface });
49
84
  }
50
- }, [openSurface, recentIds]);
85
+ }, [openItem, recentIds]);
51
86
 
52
87
  useEffect(() => {
53
88
  inputRef.current?.focus();
@@ -83,13 +118,13 @@ export function CommandPalette({ close, initialQuery, openSurface, status }: { c
83
118
  <section aria-label="Command palette" className="command-palette">
84
119
  <label className="command-input-row">
85
120
  <FiSearch aria-hidden="true" />
86
- <input aria-activedescendant={matches[activeIndex]?.id} aria-controls="command-palette-results" aria-expanded="true" aria-autocomplete="list" ref={inputRef} onChange={(event) => { setQuery(event.currentTarget.value); setActiveIndex(0); }} placeholder="Search commands, #sessions, #channels, @people, or /actions" role="combobox" value={query} />
121
+ <input aria-activedescendant={matches[activeIndex]?.id} aria-controls="command-palette-results" aria-expanded="true" aria-autocomplete="list" ref={inputRef} onChange={(event) => { setQuery(event.currentTarget.value); setActiveIndex(0); }} placeholder="Search /commands, #channels, _sessions, &infra, ?help, .agents" role="combobox" value={query} />
87
122
  </label>
88
123
  <ul id="command-palette-results">
89
124
  {matches.map((item, index) => (
90
125
  <li key={item.id}>
91
126
  <button className={index === activeIndex ? "active" : undefined} id={item.id} onClick={() => selectItem(item)} type="button">
92
- <span className="surface-icon" aria-hidden="true"><SurfaceGlyph icon={item.icon} /></span>
127
+ <span className="surface-icon" aria-hidden="true">{item.iconGlyph ?? <SurfaceGlyph icon={item.icon} />}</span>
93
128
  <span><strong>{item.label}</strong><small>{item.tag} · {item.description}</small></span>
94
129
  </button>
95
130
  </li>
@@ -106,9 +141,11 @@ export function commandPaletteShortcutQuery(event: Pick<KeyboardEvent, "key" | "
106
141
  return undefined;
107
142
  }
108
143
 
109
- return new Map([["/", "/"], ["@", "@"], ["#", "#"], [".", ""]]).get(event.key);
144
+ return new Map(commandPaletteShortcutEntries).get(event.key);
110
145
  }
111
146
 
147
+ export const commandPaletteShortcutEntries = ["#", "_", "&", "?", ">", "+", "-", "/", "=", "~", "!", ".", "*", ":", "^"].map((shortcut) => [shortcut, shortcut] as const);
148
+
112
149
  export function orderCommandItemsByRecency<T extends { id: string }>(items: T[], recentIds: string[]): T[] {
113
150
  const recentItems = recentIds
114
151
  .map((id) => items.find((item) => item.id === id))
@@ -124,12 +161,12 @@ export function rememberCommandPaletteItemId(itemId: string, currentIds: string[
124
161
  return nextIds;
125
162
  }
126
163
 
127
- function commandPaletteMatches(items: CommandPaletteItem[], query: string, recentIds: string[]): CommandPaletteItem[] {
164
+ export function commandPaletteMatches(items: CommandPaletteItem[], query: string, recentIds: string[]): CommandPaletteItem[] {
128
165
  const normalizedQuery = query.trim().toLowerCase();
129
166
  const prefix = normalizedQuery.charAt(0);
130
167
  const scopedItems = itemsForCommandPrefix(items, prefix);
131
168
 
132
- if (normalizedQuery.length === 0 || ["/", "@", "#"].includes(normalizedQuery)) {
169
+ if (normalizedQuery.length === 0 || commandPaletteShortcutEntries.some(([shortcut]) => shortcut === normalizedQuery)) {
133
170
  return orderCommandItemsByRecency(scopedItems, recentIds).slice(0, 8);
134
171
  }
135
172
 
@@ -138,7 +175,7 @@ function commandPaletteMatches(items: CommandPaletteItem[], query: string, recen
138
175
  .slice(0, 8);
139
176
  }
140
177
 
141
- function commandPaletteItems(status: GuiStatusData): CommandPaletteItem[] {
178
+ function commandPaletteItems(status: GuiStatusData, activeSurface: SurfaceId): CommandPaletteItem[] {
142
179
  const surfaceItems: CommandPaletteItem[] = orderedNavItems.map((item) => ({
143
180
  description: item.description,
144
181
  icon: item.icon,
@@ -146,15 +183,24 @@ function commandPaletteItems(status: GuiStatusData): CommandPaletteItem[] {
146
183
  label: item.label,
147
184
  searchText: `${item.label} ${item.description}`,
148
185
  surface: item.id,
149
- tag: "Surface",
186
+ tag: surfaceTagForItem(item),
150
187
  }));
188
+ const helpItems: CommandPaletteItem[] = [
189
+ { id: "help-shortcuts", label: "? shortcuts", description: "Open command palette and keyboard shortcut help", icon: "help", searchText: "? shortcuts help command palette keyboard", surface: "help", tag: "Help" },
190
+ ];
191
+ const currentLinkItems: CommandPaletteItem[] = [
192
+ { action: "copy-current-link", id: "copy-current-link", label: `^ copy ${activeSurface} link`, description: "Copy a link that can reopen this page in the web or local app", icon: "link", searchText: `^ link copy current page url ${activeSurface} aidevops://surface/${activeSurface}`, surface: activeSurface, tag: "Link" },
193
+ ];
151
194
  const sessionItems: CommandPaletteItem[] = status.opencode_sessions.sessions.map((session) => ({
152
195
  description: `AI session in ${session.repo_path_ref}`,
153
196
  icon: "terminal",
197
+ iconGlyph: "_",
154
198
  id: `session-${session.id_ref}`,
155
- label: `#${session.title}`,
156
- searchText: `#${session.title} ${session.title} ${session.repo_path_ref} ${session.agent} ${session.model}`,
199
+ label: `_${session.title}`,
200
+ searchText: `_${session.title} ${session.title} ${session.repo_path_ref} ${session.agent} ${session.model}`,
157
201
  surface: "git",
202
+ shellMode: "sessions",
203
+ conversationMode: "ai",
158
204
  tag: "AI session",
159
205
  }));
160
206
  const directMessageItems: CommandPaletteItem[] = [{
@@ -167,20 +213,60 @@ function commandPaletteItems(status: GuiStatusData): CommandPaletteItem[] {
167
213
  tag: "Direct message",
168
214
  }];
169
215
 
170
- return [...surfaceItems, ...sessionItems, ...plannedChannelItems, ...directMessageItems, ...slashCommandItems];
216
+ return [...surfaceItems, ...helpItems, ...sessionItems, ...plannedChannelItems, ...directMessageItems, ...slashCommandItems, ...addCommandItems, ...removeCommandItems, ...terminalCommandItems, ...emojiItems, ...currentLinkItems];
171
217
  }
172
218
 
173
219
  function itemsForCommandPrefix(items: CommandPaletteItem[], prefix: string): CommandPaletteItem[] {
174
220
  const tagsByPrefix = new Map<string, string[]>([
175
221
  ["/", ["Command"]],
176
222
  ["@", ["Direct message"]],
177
- ["#", ["AI session", "Channel"]],
223
+ ["#", ["Channel"]],
224
+ ["_", ["AI session"]],
225
+ ["&", ["Infrastructure"]],
226
+ ["?", ["Help"]],
227
+ [">", ["Terminal"]],
228
+ ["+", ["Add"]],
229
+ ["-", ["Remove"]],
230
+ ["=", ["Comms"]],
231
+ ["~", ["Settings"]],
232
+ ["!", ["Notifications"]],
233
+ [".", ["Agent"]],
234
+ ["*", ["Secret"]],
235
+ [":", ["Emoji"]],
236
+ ["^", ["Link"]],
178
237
  ]);
179
238
  const allowedTags = tagsByPrefix.get(prefix) ?? ["Surface"];
180
239
 
181
240
  return items.filter((item) => allowedTags.includes(item.tag));
182
241
  }
183
242
 
243
+ function surfaceTagForItem(item: SurfaceNavItem): string {
244
+ const explicitTags: Partial<Record<SurfaceId, string>> = {
245
+ agents: "Agent",
246
+ config: "Settings",
247
+ help: "Help",
248
+ notifications: "Notifications",
249
+ projectConfig: "Settings",
250
+ security: "Secret",
251
+ settings: "Settings",
252
+ vault: "Secret",
253
+ };
254
+ const infrastructureIds = navGroups.find((group) => group.label === "Infrastructure")?.items.map((groupItem) => groupItem.id) ?? [];
255
+ const commsIds = navGroups.filter((group) => group.mode === "comms").flatMap((group) => group.items.map((groupItem) => groupItem.id));
256
+ const utilityFallback = utilityNavItems.some((utilityItem) => utilityItem.id === item.id) ? "Surface" : undefined;
257
+
258
+ return explicitTags[item.id] ?? (infrastructureIds.includes(item.id) ? "Infrastructure" : undefined) ?? (commsIds.includes(item.id) ? "Comms" : undefined) ?? utilityFallback ?? "Surface";
259
+ }
260
+
261
+ async function copyCurrentSurfaceLink(surface: SurfaceId): Promise<void> {
262
+ const browserUrl = typeof window === "undefined" ? "" : window.location.href.split("#")[0];
263
+ const link = browserUrl.length > 0 ? `${browserUrl}#surface=${surface}` : `aidevops://surface/${surface}`;
264
+
265
+ if (typeof navigator !== "undefined" && navigator.clipboard) {
266
+ await navigator.clipboard.writeText(link);
267
+ }
268
+ }
269
+
184
270
  function readCommandPaletteRecentIds(): string[] {
185
271
  try {
186
272
  const storedValue = window.localStorage.getItem(commandPaletteRecentStorageKey);
@@ -32,6 +32,8 @@ export const surfaceIconNames = {
32
32
  globe: true,
33
33
  grid: true,
34
34
  hardDrive: true,
35
+ hash: true,
36
+ help: true,
35
37
  link: true,
36
38
  list: true,
37
39
  lock: true,
@@ -48,6 +50,7 @@ export const surfaceIconNames = {
48
50
  export type SurfaceIconName = keyof typeof surfaceIconNames;
49
51
  export const surfaceIds = [
50
52
  "overview",
53
+ "help",
51
54
  "settings",
52
55
  "notifications",
53
56
  "admin",
@@ -147,6 +150,8 @@ export const text = {
147
150
  configIntro: "Read-only file explorer for ~/.config/aidevops. Contents stay hidden until redaction rules land.",
148
151
  copyPath: "Copy path",
149
152
  dashboard: "Dashboard",
153
+ help: "Help",
154
+ helpIntro: "Command shortcuts, navigation hints, and aidevops workflow guidance will collect here.",
150
155
  domains: "Domains",
151
156
  domainsIntro: "Draft domain ownership inventory across providers.",
152
157
  draftOnly: "Local browser draft only. Saving requires a write-action manifest, confirmation, and audit trail.",
@@ -396,6 +401,7 @@ export const dashboardNavItem: SurfaceNavItem = {
396
401
  };
397
402
 
398
403
  export const utilityNavItems: SurfaceNavItem[] = [
404
+ { id: "help", label: text.help, description: "Shortcuts, command palette symbols, and support", icon: "help", badge: text.planned },
399
405
  { id: "settings", label: "Settings", description: "Profile, login, notifications, language, and theme preferences", icon: "settings", badge: text.planned },
400
406
  { id: "notifications", label: "Notifications", description: "Notification inbox and delivery preferences", icon: "message", badge: text.planned },
401
407
  { id: "admin", label: "Admin", description: "Hosted administration controls", icon: "shield", badge: text.planned },
@@ -38,7 +38,7 @@ export function renderDashboardHtml(status: GuiResponseEnvelope<GuiStatusData>):
38
38
  .join("");
39
39
  const navSections = [
40
40
  { heading: "Development", items: ["Local Repos", "Remote Repos", "Secrets", "AI Providers"] },
41
- { heading: "Account", items: ["Settings", "Notifications", "Admin"] },
41
+ { heading: "Account", items: ["Help", "Settings", "Notifications", "Admin"] },
42
42
  { heading: "Operations", items: ["Dashboard", "Vault", "Agents file explorer", "Config", "Local Setup", "Routines"] },
43
43
  { heading: "Infrastructure", items: ["Devices", "VPNs & Proxies", "Apps", "Installation", "Registrars", "Hosts", "Servers"] },
44
44
  { heading: "Identities", items: ["Brands", "Domains", "Personas"] },
@@ -1352,6 +1352,38 @@ code {
1352
1352
  padding: 14px;
1353
1353
  }
1354
1354
 
1355
+ .shortcut-list {
1356
+ display: grid;
1357
+ gap: 8px;
1358
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
1359
+ list-style: none;
1360
+ margin: 0;
1361
+ padding: 0;
1362
+ }
1363
+
1364
+ .shortcut-list li {
1365
+ align-items: center;
1366
+ background: var(--surface-elevated);
1367
+ border: 1px solid var(--border);
1368
+ border-radius: 16px;
1369
+ display: flex;
1370
+ gap: 12px;
1371
+ padding: 12px 14px;
1372
+ }
1373
+
1374
+ .shortcut-list kbd {
1375
+ align-items: center;
1376
+ background: var(--surface-muted);
1377
+ border: 1px solid var(--border);
1378
+ border-radius: 10px;
1379
+ color: var(--accent);
1380
+ display: inline-flex;
1381
+ font-weight: 900;
1382
+ justify-content: center;
1383
+ min-width: 34px;
1384
+ padding: 4px 8px;
1385
+ }
1386
+
1355
1387
  .notification-list span {
1356
1388
  color: var(--text-muted);
1357
1389
  }
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.27.0
15
+ # Version: 3.28.0
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)