aidevops 3.29.7 → 3.29.8

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.29.7-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
61
+ [![Version](https://img.shields.io/badge/Version-3.29.8-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.29.7
1
+ 3.29.8
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.29.7
8
+ # Version: 3.29.8
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.29.7",
3
+ "version": "3.29.8",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -1,11 +1,12 @@
1
1
  /* jshint esversion: 11 */
2
2
  import { type ReactElement, type ReactNode, useEffect, useState } from "react";
3
- import { FiAlertTriangle, FiBell, FiCheckCircle, FiChevronLeft, FiChevronRight, FiCommand, FiGlobe, FiHash, FiHelpCircle, FiInfo, FiLogOut, FiMessageSquare, FiSearch, FiSettings, FiShield, FiTerminal, FiTool, FiUser } from "react-icons/fi";
3
+ import { FiAlertTriangle, FiBell, FiCheckCircle, FiChevronLeft, FiChevronRight, FiCommand, FiFileText, FiGlobe, FiHash, FiHelpCircle, FiInfo, FiLogOut, FiMessageSquare, FiPaperclip, FiSearch, FiSettings, FiShield, FiTerminal, FiTool, FiUser } from "react-icons/fi";
4
4
  import type { GuiFileRootId, GuiNotificationSummary, GuiStatusData } from "../../gui-shared/src";
5
5
  import { SurfaceGlyph } from "./AppNavigation";
6
6
  import type { ConversationMode, ShellMode, SurfaceId, SurfaceNavItem } from "./app-model";
7
7
  import { inventorySurfaceConfigs, surfaceIds, text } from "./app-model";
8
8
  import { CommandPalette, type CommandPaletteSelection, commandPaletteShortcutQuery } from "./CommandPalette";
9
+ import { CommsConversationSurface } from "./CommsConversationSurface";
9
10
  import { FileExplorerSurface } from "./FileExplorerSurface";
10
11
  import { AppsSurface, EditableInventorySurface, InstallationSurface } from "./InventorySurfaces";
11
12
  import { AiProvidersSurface, LocalReposSurface, LockedVaultGate, OverviewSurface, PlannedSurface, ProjectsSurface, SecuritySurface, VaultSurface } from "./StatusSurfaces";
@@ -213,39 +214,110 @@ function ConversationWorkspace({ conversationMode, selectedLocalRepoIndex, selec
213
214
  status: GuiStatusData;
214
215
  }) {
215
216
  const selectedRepo = status.local_repos.repos[selectedLocalRepoIndex] ?? status.local_repos.repos[0];
216
- const selectedSession = selectedRepo === undefined
217
- ? undefined
218
- : status.opencode_sessions.sessions.find((session) => session.id_ref === selectedSessionId && session.repo_path_ref === selectedRepo.path_ref) ?? status.opencode_sessions.sessions.find((session) => session.repo_path_ref === selectedRepo.path_ref);
217
+ const selectedSession = selectedRepo === undefined ? undefined : sessionForRepo(status, selectedRepo.path_ref, selectedSessionId);
219
218
  const title = conversationMode === "ai" ? selectedRepo?.name ?? text.opencodeSessions : text.teams;
220
219
 
220
+ if (conversationMode === "ai") {
221
+ return <AiSessionsSurface selectedRepoIndex={selectedLocalRepoIndex} selectedSessionId={selectedSession?.id_ref} status={status} />;
222
+ }
223
+
221
224
  return (
222
- <section className="chat-surface" aria-label={conversationMode === "ai" ? text.opencodeSessions : "People chat"}>
223
- <div className="chat-thread-panel">
225
+ <CommsConversationSurface mode={title === text.teams ? "people" : "channels"} />
226
+ );
227
+ }
228
+
229
+ function AiSessionsSurface({ selectedRepoIndex, selectedSessionId, status }: { selectedRepoIndex: number; selectedSessionId?: string; status: GuiStatusData }): ReactElement {
230
+ const selectedRepo = status.local_repos?.repos?.[selectedRepoIndex] ?? status.local_repos?.repos?.[0];
231
+ const selectedSession = selectedRepo === undefined ? undefined : sessionForRepo(status, selectedRepo.path_ref, selectedSessionId);
232
+ const providerOptions = status.oauth_pool?.providers?.filter((provider) => provider.configured || provider.total > 0) ?? [];
233
+ const sessionTitle = selectedSession?.title ?? "AI session bridge";
234
+ const sessionMeta = selectedSession ? `${selectedSession.model} via ${selectedSession.agent}` : "No OpenCode session metadata selected";
235
+
236
+ return (
237
+ <section className="chat-surface ai-sessions-surface" aria-label={text.aiSessions} data-tour="ai-sessions-surface">
238
+ <aside className="chat-thread-panel" aria-label="AI session controls" data-tour="ai-session-list">
224
239
  <header className="chat-thread-header">
225
240
  <div>
226
- <p className="eyebrow">{conversationMode === "ai" ? text.opencodeSessions : "SimpleX channels"}</p>
227
- <h2><FiHash aria-hidden="true" /> {title}</h2>
241
+ <p className="eyebrow">{text.aiSessions}</p>
242
+ <h2><FiTerminal aria-hidden="true" /> {selectedRepo?.name ?? "Local workspace"}</h2>
228
243
  </div>
229
244
  <span className="count-pill">{text.readOnly}</span>
230
245
  </header>
231
- <div className="chat-message-list">
232
- {conversationMode === "ai" ? <>
233
- <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."} />
234
- <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."} />
235
- </> : <>
236
- <ChatBubble speaker="assistant" title="SimpleX transport" body={text.simplexReady} />
237
- <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." />
238
- </>}
246
+ <div className="notice compact-notice">New, rename, pin, archive, delete, share, and export are visible but disabled until audited AI session write routes land.</div>
247
+ <div className="sidebar-history-controls">
248
+ {sessionActions.map((action) => <button disabled key={action} title={`${action} needs the AI session write adapter`} type="button">{action}</button>)}
249
+ </div>
250
+ <label className="settings-form">
251
+ <span>Model/provider</span>
252
+ <select
253
+ disabled
254
+ defaultValue={providerOptions[0]?.provider ?? "local"}
255
+ title="Model switching needs the Turbostarter AI transport adapter."
256
+ >
257
+ {providerOptions.length === 0 ? (
258
+ <option value="local">Provider setup required</option>
259
+ ) : (
260
+ providerOptions.map((provider) => (
261
+ <option key={provider.provider} value={provider.provider}>
262
+ {provider.provider} ({provider.available}/{provider.total})
263
+ </option>
264
+ ))
265
+ )}
266
+ </select>
267
+ </label>
268
+ </aside>
269
+ <div className="chat-thread-panel" data-tour="ai-session-transcript">
270
+ <header className="chat-thread-header">
271
+ <div>
272
+ <p className="eyebrow">{sessionMeta}</p>
273
+ <h2><FiHash aria-hidden="true" /> {sessionTitle}</h2>
274
+ </div>
275
+ <button disabled title="Convert to worker task needs dispatch backend support" type="button">Create worker task</button>
276
+ </header>
277
+ <div className="chat-message-list" data-tour="message-scroller">
278
+ <MessageMarker label="Session status" detail="MessageScroller-compatible transcript; auto-scroll must pause when the reader scrolls away from the latest message." />
279
+ <ChatBubble
280
+ speaker="assistant"
281
+ title={sessionTitle}
282
+ body={
283
+ selectedSession
284
+ ? `Most recent OpenCode session metadata: ${sessionMeta}, updated ${selectedSession.updated_at}. Message payloads stay out of the status API until the Turbostarter AI chat bridge lands.`
285
+ : "OpenCode session creation and continuation need an audited write route. This surface is ready for the Turbostarter AI adapter once connected."
286
+ }
287
+ />
288
+ <AttachmentCard label="Context attachment" detail={selectedRepo ? `Selected repo context: ${selectedRepo.path_ref}` : "No local repos were discovered yet."} />
289
+ <MessageMarker label="Reasoning" detail="Reasoning disclosure, tool status, sources, retries, errors, and token usage render here when the AI transport supplies those parts." />
290
+ <ToolStatusCard />
239
291
  </div>
240
- <form className="chat-composer" aria-label="Chat composer">
292
+ <form className="chat-composer ai-composer" aria-label="AI prompt composer" data-tour="ai-composer">
293
+ <button disabled title="Attachments need the audited upload/context adapter" type="button"><FiPaperclip aria-hidden="true" /> Attach</button>
241
294
  <textarea disabled placeholder={text.chatInputPlaceholder} />
242
- <button disabled type="button">Send</button>
295
+ <button disabled title="Sending needs the Turbostarter AI transport adapter" type="button">Send</button>
243
296
  </form>
244
297
  </div>
245
298
  </section>
246
299
  );
247
300
  }
248
301
 
302
+ const sessionActions = ["New", "Rename", "Pin", "Archive", "Delete", "Share", "Export"] as const;
303
+
304
+ function sessionForRepo(status: GuiStatusData, repoPathRef: string, selectedSessionId: string | undefined) {
305
+ const sessions = status.opencode_sessions?.sessions ?? [];
306
+ return sessions.find((session) => session.id_ref === selectedSessionId && session.repo_path_ref === repoPathRef) ?? sessions.find((session) => session.repo_path_ref === repoPathRef);
307
+ }
308
+
309
+ function MessageMarker({ detail, label }: { detail: string; label: string }) {
310
+ return <article className="chat-bubble assistant"><strong>{label}</strong><p>{detail}</p></article>;
311
+ }
312
+
313
+ function AttachmentCard({ detail, label }: { detail: string; label: string }) {
314
+ return <article className="chat-bubble user"><strong><FiFileText aria-hidden="true" /> {label}</strong><p>{detail}</p></article>;
315
+ }
316
+
317
+ function ToolStatusCard() {
318
+ return <article className="chat-bubble assistant"><strong>Tool status</strong><p>Tool calls, source citations, retries, errors, and usage metadata are reserved for the shared conversation part model.</p></article>;
319
+ }
320
+
249
321
  function ChatBubble({ body, speaker, title }: { body: string; speaker: "assistant" | "user"; title: string }) {
250
322
  return (
251
323
  <article className={`chat-bubble ${speaker}`}>
@@ -304,9 +376,9 @@ function SurfaceContent({ activeItem, activeSurface, fileRoot, openSurface, stat
304
376
  notifications: <NotificationsSurface openSurface={openSurface} status={status} />,
305
377
  admin: <AdminSurface />,
306
378
  vault: <VaultSurface status={status} />,
307
- aiSessions: <PlannedSurface label={text.aiSessions} detail={text.aiSessionsIntro} />,
308
- channels: <PlannedSurface label={text.channels} detail={text.channelsIntro} />,
309
- directMessages: <PlannedSurface label={text.directMessages} detail={text.directMessagesIntro} />,
379
+ aiSessions: <AiSessionsSurface selectedRepoIndex={0} status={status} />,
380
+ channels: <CommsConversationSurface mode="channels" />,
381
+ directMessages: <CommsConversationSurface mode="directMessages" />,
310
382
  workers: <PlannedSurface label={text.workers} detail={text.workersIntro} />,
311
383
  repos: <PlannedSurface label={text.repos} detail={text.reposIntro} />,
312
384
  deployments: <PlannedSurface label={text.deployments} detail={text.deploymentsIntro} />,
@@ -0,0 +1,93 @@
1
+ import { FiAtSign, FiHash, FiMessageSquare, FiSearch } from "react-icons/fi";
2
+ import { sortConversationMessageParts, sortConversationMessages, type GuiConversationMessage, type GuiConversationMessagePart, type GuiConversationThread } from "../../gui-shared/src";
3
+ import { text } from "./app-model";
4
+ import { conversationThreads } from "./conversation-fixtures";
5
+
6
+ export function CommsConversationSurface({ mode }: { mode: "channels" | "directMessages" | "people" }) {
7
+ const conversations = mode === "directMessages" ? conversationThreads.filter((thread) => thread.conversation.type === "dm" || thread.conversation.type === "group_dm") : conversationThreads.filter((thread) => thread.conversation.type === "channel");
8
+ const selectedThread = conversations[0] ?? conversationThreads[0];
9
+ const partsByMessage = new Map<string, GuiConversationMessagePart[]>();
10
+
11
+ for (const part of sortConversationMessageParts(selectedThread.parts)) {
12
+ partsByMessage.set(part.message_id, [...(partsByMessage.get(part.message_id) ?? []), part]);
13
+ }
14
+
15
+ return (
16
+ <section className="chat-surface comms-surface" aria-label={mode === "directMessages" ? text.directMessages : text.channels} data-tour="comms-conversations">
17
+ <aside className="chat-thread-panel conversation-list-panel" aria-label="Conversation list">
18
+ <header className="chat-thread-header">
19
+ <div>
20
+ <p className="eyebrow">Unified conversation model</p>
21
+ <h2>{mode === "directMessages" ? <FiAtSign aria-hidden="true" /> : <FiHash aria-hidden="true" />} {mode === "directMessages" ? text.directMessages : text.channels}</h2>
22
+ </div>
23
+ <button disabled title="Creating conversations needs encrypted transport and audited write routes" type="button">Create</button>
24
+ </header>
25
+ <label className="conversation-search"><FiSearch aria-hidden="true" /><span className="sr-only">Search conversations</span><input disabled placeholder="Search channels, DMs, mentions" /></label>
26
+ <div className="notice compact-notice">{text.simplexReady}</div>
27
+ <ul className="conversation-list">
28
+ {conversations.map((thread) => <ConversationListItem key={thread.conversation.id} thread={thread} />)}
29
+ </ul>
30
+ </aside>
31
+ <div className="chat-thread-panel conversation-detail-panel">
32
+ <header className="chat-thread-header">
33
+ <div>
34
+ <p className="eyebrow">{selectedThread.conversation.scope.workspace_ref} · {selectedThread.conversation.status}</p>
35
+ <h2>{selectedThread.conversation.type === "channel" ? <FiHash aria-hidden="true" /> : <FiMessageSquare aria-hidden="true" />} {selectedThread.conversation.title}</h2>
36
+ </div>
37
+ <span className="count-pill">{participantSummary(selectedThread)}</span>
38
+ </header>
39
+ <div className="participant-strip">
40
+ {selectedThread.participants.map((participant) => <span key={participant.id}>{participant.display_name}<small>{participant.kind}</small></span>)}
41
+ </div>
42
+ <div className="chat-message-list" data-tour="comms-message-timeline">
43
+ {sortConversationMessages(selectedThread.messages).map((message) => <ConversationMessageRow key={message.id} message={message} parts={partsByMessage.get(message.id) ?? []} thread={selectedThread} />)}
44
+ </div>
45
+ <form className="chat-composer" aria-label="Conversation composer">
46
+ <textarea disabled placeholder={text.chatInputPlaceholder} />
47
+ <button disabled title="Sending messages needs the encrypted transport adapter" type="button">Send</button>
48
+ </form>
49
+ </div>
50
+ </section>
51
+ );
52
+ }
53
+
54
+ function ConversationListItem({ thread }: { thread: GuiConversationThread }) {
55
+ const latestMessage = sortConversationMessages(thread.messages).at(-1);
56
+ const unread = unreadCount(thread);
57
+
58
+ return (
59
+ <li>
60
+ <button className="surface-link" type="button">
61
+ <span className="surface-icon" aria-hidden="true">{thread.conversation.type === "channel" ? <FiHash /> : <FiMessageSquare />}</span>
62
+ <span className="surface-copy"><strong>{thread.conversation.title}</strong><small>{latestMessage ? latestMessage.created_at : "No messages yet"}</small></span>
63
+ {unread > 0 ? <em>{unread}</em> : null}
64
+ </button>
65
+ </li>
66
+ );
67
+ }
68
+
69
+ function ConversationMessageRow({ message, parts, thread }: { message: GuiConversationMessage; parts: GuiConversationMessagePart[]; thread: GuiConversationThread }) {
70
+ const sender = thread.participants.find((participant) => participant.id === message.sender_participant_id);
71
+ const reactions = thread.reactions.filter((reaction) => reaction.message_id === message.id);
72
+ const speaker = message.sender_kind === "human" ? "user" : "assistant";
73
+
74
+ return (
75
+ <article className={`chat-bubble ${speaker}`} data-sender-kind={message.sender_kind}>
76
+ <strong>{sender?.display_name ?? message.sender_kind}</strong>
77
+ {parts.map((part) => <p key={part.id}>{part.text ?? part.kind}</p>)}
78
+ <small>{message.status} · {message.created_at}</small>
79
+ {reactions.length > 0 ? <div className="reaction-row">{reactions.map((reaction) => <span key={reaction.id}>{reaction.reaction}</span>)}</div> : null}
80
+ </article>
81
+ );
82
+ }
83
+
84
+ function participantSummary(thread: GuiConversationThread): string {
85
+ const active = thread.participants.filter((participant) => participant.membership_state === "active").length;
86
+ return `${active} members`;
87
+ }
88
+
89
+ function unreadCount(thread: GuiConversationThread): number {
90
+ const lastSequence = Math.max(0, ...thread.messages.map((message) => message.sequence));
91
+ const localRead = thread.read_states[0]?.last_read_sequence ?? 0;
92
+ return Math.max(0, lastSequence - localRead);
93
+ }
@@ -0,0 +1,38 @@
1
+ import type { GuiConversationThread } from "../../gui-shared/src";
2
+
3
+ export const conversationThreads: GuiConversationThread[] = [
4
+ {
5
+ conversation: { id: "channel-general", type: "channel", title: "general", scope: { tenant_ref: "local", workspace_ref: "aidevops", repo_ref: null }, source_ref: "seed:channels/general", status: "read_only", created_at: "2026-06-27T00:00:00Z", updated_at: "2026-06-27T12:20:00Z" },
6
+ participants: [
7
+ { id: "participant-local", conversation_id: "channel-general", kind: "human", display_name: "Local user", identity_ref: "local:user", agent_ref: null, worker_ref: null, membership_state: "active", joined_at: "2026-06-27T00:00:00Z" },
8
+ { id: "participant-ai", conversation_id: "channel-general", kind: "ai_assistant", display_name: "AI DevOps", identity_ref: null, agent_ref: "aidevops", worker_ref: null, membership_state: "active", joined_at: "2026-06-27T00:00:00Z" },
9
+ { id: "participant-system", conversation_id: "channel-general", kind: "system_bot", display_name: "System", identity_ref: null, agent_ref: null, worker_ref: null, membership_state: "active", joined_at: "2026-06-27T00:00:00Z" },
10
+ ],
11
+ messages: [
12
+ { id: "message-general-1", conversation_id: "channel-general", sender_participant_id: "participant-system", sender_kind: "system", sequence: 1, status: "sent", usage: null, created_at: "2026-06-27T12:00:00Z", edited_at: null },
13
+ { id: "message-general-2", conversation_id: "channel-general", sender_participant_id: "participant-ai", sender_kind: "ai_assistant", sequence: 2, status: "delivered", usage: { provider_ref: "local", model_ref: "workflow-summary", input_tokens: 0, output_tokens: 0, total_tokens: 0, cost_ref: null }, created_at: "2026-06-27T12:15:00Z", edited_at: null },
14
+ ],
15
+ parts: [
16
+ { id: "part-general-1", message_id: "message-general-1", kind: "event_marker", ordinal: 1, text: "#general is ready for repo, deployment, review, and incident coordination.", payload_json: null, file_ref: null, source_ref: "seed" },
17
+ { id: "part-general-2", message_id: "message-general-2", kind: "text", ordinal: 1, text: "Mention @AI DevOps or a worker to turn a thread into an audited task once write routes land.", payload_json: null, file_ref: null, source_ref: null },
18
+ ],
19
+ reactions: [{ id: "reaction-general-1", message_id: "message-general-2", participant_id: "participant-local", reaction: "ack", created_at: "2026-06-27T12:16:00Z" }],
20
+ read_states: [{ conversation_id: "channel-general", participant_id: "participant-local", last_read_message_id: "message-general-1", last_read_sequence: 1, updated_at: "2026-06-27T12:10:00Z" }],
21
+ },
22
+ {
23
+ conversation: { id: "channel-workers", type: "channel", title: "worker-feed", scope: { tenant_ref: "local", workspace_ref: "aidevops", repo_ref: "current" }, source_ref: "seed:channels/worker-feed", status: "read_only", created_at: "2026-06-27T00:00:00Z", updated_at: "2026-06-27T12:30:00Z" },
24
+ participants: [{ id: "participant-worker", conversation_id: "channel-workers", kind: "worker", display_name: "Worker queue", identity_ref: null, agent_ref: null, worker_ref: "workers", membership_state: "active", joined_at: "2026-06-27T00:00:00Z" }],
25
+ messages: [{ id: "message-workers-1", conversation_id: "channel-workers", sender_participant_id: "participant-worker", sender_kind: "worker", sequence: 1, status: "delivered", usage: null, created_at: "2026-06-27T12:30:00Z", edited_at: null }],
26
+ parts: [{ id: "part-workers-1", message_id: "message-workers-1", kind: "event_marker", ordinal: 1, text: "Worker events for reviews, deployments, incidents, and releases collect here.", payload_json: null, file_ref: null, source_ref: "seed" }],
27
+ reactions: [],
28
+ read_states: [{ conversation_id: "channel-workers", participant_id: "participant-local", last_read_message_id: null, last_read_sequence: 0, updated_at: "2026-06-27T12:00:00Z" }],
29
+ },
30
+ {
31
+ conversation: { id: "dm-ai-devops", type: "dm", title: "AI DevOps", scope: { tenant_ref: "local", workspace_ref: "aidevops", repo_ref: null }, source_ref: "seed:dms/ai-devops", status: "read_only", created_at: "2026-06-27T00:00:00Z", updated_at: "2026-06-27T12:40:00Z" },
32
+ participants: [{ id: "participant-dm-local", conversation_id: "dm-ai-devops", kind: "human", display_name: "Local user", identity_ref: "local:user", agent_ref: null, worker_ref: null, membership_state: "active", joined_at: "2026-06-27T00:00:00Z" }, { id: "participant-dm-ai", conversation_id: "dm-ai-devops", kind: "ai_assistant", display_name: "AI DevOps", identity_ref: null, agent_ref: "aidevops", worker_ref: null, membership_state: "active", joined_at: "2026-06-27T00:00:00Z" }],
33
+ messages: [{ id: "message-dm-1", conversation_id: "dm-ai-devops", sender_participant_id: "participant-dm-ai", sender_kind: "ai_assistant", sequence: 1, status: "delivered", usage: null, created_at: "2026-06-27T12:40:00Z", edited_at: null }],
34
+ parts: [{ id: "part-dm-1", message_id: "message-dm-1", kind: "text", ordinal: 1, text: "Direct support threads share the same message parts, participants, reactions, and read state as channels.", payload_json: null, file_ref: null, source_ref: null }],
35
+ reactions: [],
36
+ read_states: [{ conversation_id: "dm-ai-devops", participant_id: "participant-dm-local", last_read_message_id: null, last_read_sequence: 0, updated_at: "2026-06-27T12:00:00Z" }],
37
+ },
38
+ ];
@@ -69,6 +69,7 @@ body {
69
69
  radial-gradient(ellipse at top left, color-mix(in srgb, var(--accent) 16%, transparent) 0%, transparent 36%),
70
70
  var(--bg-secondary);
71
71
  color: var(--text-primary);
72
+ font-family: var(--font-family-app);
72
73
  line-height: 1.6;
73
74
  margin: 0;
74
75
  font-size: var(--font-size-app);
@@ -1286,11 +1287,28 @@ code {
1286
1287
  gap: 16px;
1287
1288
  }
1288
1289
 
1290
+ .sr-only {
1291
+ height: 1px;
1292
+ margin: -1px;
1293
+ overflow: hidden;
1294
+ position: absolute;
1295
+ width: 1px;
1296
+ }
1297
+
1289
1298
  .chat-surface {
1290
1299
  display: grid;
1300
+ gap: 16px;
1291
1301
  min-height: 100%;
1292
1302
  }
1293
1303
 
1304
+ .ai-sessions-surface {
1305
+ grid-template-columns: minmax(248px, 320px) minmax(0, 1fr);
1306
+ }
1307
+
1308
+ .comms-surface {
1309
+ grid-template-columns: minmax(260px, 340px) minmax(0, 1fr);
1310
+ }
1311
+
1294
1312
  .chat-thread-panel {
1295
1313
  background: var(--surface-elevated);
1296
1314
  border: 1px solid var(--border);
@@ -1346,6 +1364,69 @@ code {
1346
1364
  margin: 0;
1347
1365
  }
1348
1366
 
1367
+ .chat-bubble small {
1368
+ color: var(--text-muted);
1369
+ }
1370
+
1371
+ .chat-bubble[data-sender-kind="system"],
1372
+ .chat-bubble[data-sender-kind="worker"] {
1373
+ border-style: dashed;
1374
+ }
1375
+
1376
+ .conversation-list-panel {
1377
+ grid-template-rows: auto auto auto minmax(0, 1fr);
1378
+ }
1379
+
1380
+ .conversation-search {
1381
+ align-items: center;
1382
+ background: var(--surface-muted);
1383
+ border: 1px solid var(--border);
1384
+ border-radius: 14px;
1385
+ display: flex;
1386
+ gap: 8px;
1387
+ margin: 14px;
1388
+ padding: 10px 12px;
1389
+ }
1390
+
1391
+ .conversation-search input {
1392
+ background: transparent;
1393
+ border: 0;
1394
+ color: var(--text-secondary);
1395
+ min-width: 0;
1396
+ width: 100%;
1397
+ }
1398
+
1399
+ .conversation-list {
1400
+ display: grid;
1401
+ gap: 8px;
1402
+ list-style: none;
1403
+ margin: 0;
1404
+ padding: 0 14px 14px;
1405
+ }
1406
+
1407
+ .participant-strip,
1408
+ .reaction-row {
1409
+ display: flex;
1410
+ flex-wrap: wrap;
1411
+ gap: 8px;
1412
+ padding: 12px 18px 0;
1413
+ }
1414
+
1415
+ .participant-strip span,
1416
+ .reaction-row span {
1417
+ background: var(--surface-muted);
1418
+ border: 1px solid var(--border);
1419
+ border-radius: 999px;
1420
+ color: var(--text-secondary);
1421
+ display: inline-flex;
1422
+ gap: 6px;
1423
+ padding: 6px 9px;
1424
+ }
1425
+
1426
+ .participant-strip small {
1427
+ color: var(--text-muted);
1428
+ }
1429
+
1349
1430
  .chat-composer {
1350
1431
  border-top: 1px solid var(--border);
1351
1432
  display: grid;
@@ -1354,6 +1435,10 @@ code {
1354
1435
  padding: 14px;
1355
1436
  }
1356
1437
 
1438
+ .chat-composer.ai-composer {
1439
+ grid-template-columns: auto minmax(0, 1fr) auto;
1440
+ }
1441
+
1357
1442
  .chat-composer textarea {
1358
1443
  background: var(--surface-muted);
1359
1444
  border: 1px solid var(--border);
@@ -1365,14 +1450,33 @@ code {
1365
1450
  }
1366
1451
 
1367
1452
  .chat-composer button {
1453
+ align-items: center;
1368
1454
  background: color-mix(in srgb, var(--accent) 16%, transparent);
1369
1455
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
1370
1456
  border-radius: 14px;
1371
1457
  color: var(--accent);
1458
+ display: inline-flex;
1372
1459
  font-weight: 900;
1460
+ gap: 8px;
1373
1461
  padding: 0 18px;
1374
1462
  }
1375
1463
 
1464
+ @media (max-width: 980px) {
1465
+ .ai-sessions-surface,
1466
+ .comms-surface {
1467
+ grid-template-columns: 1fr;
1468
+ }
1469
+
1470
+ .chat-composer {
1471
+ grid-template-columns: 1fr;
1472
+ }
1473
+
1474
+ .chat-composer button {
1475
+ justify-content: center;
1476
+ min-height: 44px;
1477
+ }
1478
+ }
1479
+
1376
1480
  .hero-panel,
1377
1481
  .panel,
1378
1482
  .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.29.7
15
+ # Version: 3.29.8
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)