aidevops 3.29.6 → 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 +1 -1
- package/VERSION +1 -1
- package/aidevops.sh +1 -1
- package/package.json +1 -1
- package/packages/gui-web/src/AppWorkspace.tsx +93 -21
- package/packages/gui-web/src/CommsConversationSurface.tsx +93 -0
- package/packages/gui-web/src/InventorySurfaces.tsx +74 -21
- package/packages/gui-web/src/conversation-fixtures.ts +38 -0
- package/packages/gui-web/src/styles.css +316 -27
- package/setup.sh +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ The result: an AI operations platform that manages projects across every busines
|
|
|
58
58
|
[](https://github.com/marcusquinn)
|
|
59
59
|
|
|
60
60
|
<!-- Release & Version Info -->
|
|
61
|
-
[](https://github.com/marcusquinn/aidevops/releases)
|
|
62
62
|
[](https://www.npmjs.com/package/aidevops)
|
|
63
63
|
[](https://github.com/marcusquinn/homebrew-tap)
|
|
64
64
|
[](https://github.com/marcusquinn/aidevops)
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.29.
|
|
1
|
+
3.29.8
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -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
|
-
<
|
|
223
|
-
|
|
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">{
|
|
227
|
-
<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="
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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="
|
|
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: <
|
|
308
|
-
channels: <
|
|
309
|
-
directMessages: <
|
|
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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GuiAppActionId, GuiAppActionJobSummary, GuiManagedAppSummary, GuiResponseEnvelope, GuiStatusData } from "@aidevops/gui-shared";
|
|
2
2
|
import { type Dispatch, type ReactElement, type SetStateAction, useEffect, useState } from "react";
|
|
3
|
-
import { FiDownload, FiExternalLink, FiRefreshCw, FiRepeat, FiTrash2 } from "react-icons/fi";
|
|
3
|
+
import { FiChevronDown, FiDownload, FiExternalLink, FiRefreshCw, FiRepeat, FiTrash2 } from "react-icons/fi";
|
|
4
4
|
import type { InventoryColumn } from "./app-model";
|
|
5
5
|
import { installationRows, text } from "./app-model";
|
|
6
6
|
|
|
@@ -13,9 +13,17 @@ let draftRowCounter = 0;
|
|
|
13
13
|
|
|
14
14
|
export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement {
|
|
15
15
|
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
|
|
16
|
+
const [expandedAppIds, setExpandedAppIds] = useState<Set<string>>(() => new Set(status.managed_apps.length > 0 ? [status.managed_apps[0].id] : []));
|
|
16
17
|
const [jobs, setJobs] = useState<Record<string, GuiAppActionJobSummary>>({});
|
|
17
18
|
const selectedJob = selectedJobId === null ? null : jobs[selectedJobId] ?? null;
|
|
18
19
|
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (status.managed_apps.length === 0) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
setExpandedAppIds((current) => current.size === 0 ? new Set([status.managed_apps[0].id]) : current);
|
|
25
|
+
}, [status.managed_apps]);
|
|
26
|
+
|
|
19
27
|
useEffect(() => {
|
|
20
28
|
if (selectedJob === null || selectedJob.status !== "running") {
|
|
21
29
|
return undefined;
|
|
@@ -37,28 +45,51 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
|
|
|
37
45
|
</div>
|
|
38
46
|
<div className="managed-app-list">
|
|
39
47
|
{status.managed_apps.map((app) => (
|
|
40
|
-
<
|
|
48
|
+
<ManagedAppPanel
|
|
49
|
+
app={app}
|
|
50
|
+
expanded={expandedAppIds.has(app.id)}
|
|
51
|
+
job={jobForApp(app.id, jobs, selectedJob)}
|
|
52
|
+
key={app.id}
|
|
53
|
+
onJob={(job) => {
|
|
54
|
+
setJobs((current) => ({ ...current, [job.id]: job }));
|
|
55
|
+
setSelectedJobId(job.id);
|
|
56
|
+
setExpandedAppIds((current) => toggledAppIds(current, app.id, true));
|
|
57
|
+
}}
|
|
58
|
+
onToggle={() => setExpandedAppIds((current) => toggledAppIds(current, app.id))}
|
|
59
|
+
/>
|
|
41
60
|
))}
|
|
42
61
|
</div>
|
|
43
|
-
|
|
62
|
+
<p className="empty-state compact-notice">Install/update actions run as allowlisted background jobs and stream inside each app panel. xterm.js with a node-pty bridge is the right next step for full TUI apps such as OpenCode; this view starts with non-interactive command logs.</p>
|
|
44
63
|
</section>
|
|
45
64
|
);
|
|
46
65
|
}
|
|
47
66
|
|
|
48
|
-
function
|
|
67
|
+
function ManagedAppPanel({ app, expanded, job, onJob, onToggle }: { app: GuiManagedAppSummary; expanded: boolean; job: GuiAppActionJobSummary | null; onJob: (job: GuiAppActionJobSummary) => void; onToggle: () => void }): ReactElement {
|
|
49
68
|
return (
|
|
50
|
-
<article className="managed-app-
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
</
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
<
|
|
69
|
+
<article className={expanded ? "managed-app-card expanded" : "managed-app-card"}>
|
|
70
|
+
<button aria-expanded={expanded} className="managed-app-summary" data-tooltip={`${expanded ? "Collapse" : "Expand"} ${app.name} controls`} onClick={onToggle} type="button">
|
|
71
|
+
<span className="managed-app-title-block">
|
|
72
|
+
<span className="eyebrow">{app.category}</span>
|
|
73
|
+
<strong>{app.name}</strong>
|
|
74
|
+
<span>{app.description}</span>
|
|
75
|
+
</span>
|
|
76
|
+
<span className="managed-app-summary-meta">
|
|
77
|
+
<SummaryChip label="Status" value={app.status} />
|
|
78
|
+
<SummaryChip label="Installed" value={app.installed_version} />
|
|
79
|
+
<SummaryChip label="Latest" value={app.latest_version} />
|
|
80
|
+
<FiChevronDown aria-hidden="true" className="managed-app-chevron" />
|
|
81
|
+
</span>
|
|
82
|
+
</button>
|
|
83
|
+
{expanded ? <div className="managed-app-body">
|
|
84
|
+
<div className="managed-app-toolbar">
|
|
85
|
+
<div className="managed-app-links">
|
|
86
|
+
<OriginLink href={app.origin_website_url} label={text.website} />
|
|
87
|
+
<OriginLink href={app.origin_repo_url} label="Repo" />
|
|
88
|
+
</div>
|
|
89
|
+
<div className="managed-app-actions">
|
|
90
|
+
{app.actions.map((action) => <AppActionButton action={action.id} app={app} disabled={!action.enabled} key={action.id} onJob={onJob} commandPreview={action.command_preview} />)}
|
|
91
|
+
</div>
|
|
60
92
|
</div>
|
|
61
|
-
</div>
|
|
62
93
|
<div className="managed-app-details">
|
|
63
94
|
<ToggleSwitch checked={app.aidevops_install} label="setup installs" />
|
|
64
95
|
<ToggleSwitch checked={app.aidevops_update} label="update maintains" />
|
|
@@ -67,19 +98,22 @@ function ManagedAppRow({ app, onJob }: { app: GuiManagedAppSummary; onJob: (job:
|
|
|
67
98
|
<AppMeta label={text.path} value={app.install_path_ref} />
|
|
68
99
|
<AppMeta label="Status" value={app.status} />
|
|
69
100
|
</div>
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
</div>
|
|
101
|
+
{job ? <AppActionTerminal job={job} /> : <p className="empty-state compact-notice">No recent command output for this app. Run an action to open this app's terminal log.</p>}
|
|
102
|
+
</div> : null}
|
|
73
103
|
</article>
|
|
74
104
|
);
|
|
75
105
|
}
|
|
76
106
|
|
|
107
|
+
function SummaryChip({ label, value }: { label: string; value: string }): ReactElement {
|
|
108
|
+
return <span className="managed-summary-chip"><small>{label}</small><span>{value}</span></span>;
|
|
109
|
+
}
|
|
110
|
+
|
|
77
111
|
function OriginLink({ href, label }: { href: string; label: string }): ReactElement {
|
|
78
112
|
if (href.length === 0) {
|
|
79
113
|
return <span className="origin-missing">{label}: source pending</span>;
|
|
80
114
|
}
|
|
81
115
|
|
|
82
|
-
return <a href={href} rel="noreferrer" target="_blank">{label} <FiExternalLink aria-hidden="true" /></a>;
|
|
116
|
+
return <a data-tooltip={`Open ${label.toLowerCase()} destination`} href={href} rel="noreferrer" target="_blank">{label} <FiExternalLink aria-hidden="true" /></a>;
|
|
83
117
|
}
|
|
84
118
|
|
|
85
119
|
function ToggleSwitch({ checked, label }: { checked: boolean; label: string }): ReactElement {
|
|
@@ -90,7 +124,7 @@ function AppMeta({ label, value }: { label: string; value: string }): ReactEleme
|
|
|
90
124
|
return <span className="app-meta"><small>{label}</small><strong>{value}</strong></span>;
|
|
91
125
|
}
|
|
92
126
|
|
|
93
|
-
function AppActionButton({ action, app, disabled, onJob
|
|
127
|
+
function AppActionButton({ action, app, commandPreview, disabled, onJob }: { action: GuiAppActionId; app: GuiManagedAppSummary; commandPreview: string; disabled: boolean; onJob: (job: GuiAppActionJobSummary) => void }): ReactElement {
|
|
94
128
|
const icon = action === "install" ? <FiDownload /> : action === "update" ? <FiRefreshCw /> : action === "reinstall" ? <FiRepeat /> : <FiTrash2 />;
|
|
95
129
|
|
|
96
130
|
async function runAction(): Promise<void> {
|
|
@@ -106,7 +140,7 @@ function AppActionButton({ action, app, disabled, onJob, title }: { action: GuiA
|
|
|
106
140
|
onJob(envelope.data);
|
|
107
141
|
}
|
|
108
142
|
|
|
109
|
-
return <button aria-label={`${action} ${app.name}`} className="app-action-button" disabled={disabled} onClick={() => void runAction()}
|
|
143
|
+
return <button aria-label={`${action} ${app.name}`} className={action === "remove" ? "app-action-button remove" : "app-action-button"} data-tooltip={commandPreview} disabled={disabled} onClick={() => void runAction()} type="button">{icon}<span>{action}</span></button>;
|
|
110
144
|
}
|
|
111
145
|
|
|
112
146
|
function AppActionTerminal({ job }: { job: GuiAppActionJobSummary }): ReactElement {
|
|
@@ -127,6 +161,25 @@ async function refreshJob(jobId: string, setJobs: Dispatch<SetStateAction<Record
|
|
|
127
161
|
setJobs((current) => ({ ...current, [envelope.data.id]: envelope.data }));
|
|
128
162
|
}
|
|
129
163
|
|
|
164
|
+
function jobForApp(appId: string, jobs: Record<string, GuiAppActionJobSummary>, selectedJob: GuiAppActionJobSummary | null): GuiAppActionJobSummary | null {
|
|
165
|
+
if (selectedJob?.app_id === appId) {
|
|
166
|
+
return selectedJob;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return Object.values(jobs).reverse().find((job) => job.app_id === appId) ?? null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function toggledAppIds(current: Set<string>, appId: string, forceOpen = false): Set<string> {
|
|
173
|
+
const next = new Set(current);
|
|
174
|
+
if (forceOpen || !next.has(appId)) {
|
|
175
|
+
next.add(appId);
|
|
176
|
+
} else {
|
|
177
|
+
next.delete(appId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return next;
|
|
181
|
+
}
|
|
182
|
+
|
|
130
183
|
export function InstallationSurface(): ReactElement {
|
|
131
184
|
return (
|
|
132
185
|
<section className="panel" aria-label={text.installation}>
|
|
@@ -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,
|
|
@@ -1869,38 +1973,122 @@ code {
|
|
|
1869
1973
|
}
|
|
1870
1974
|
|
|
1871
1975
|
.managed-app-list {
|
|
1872
|
-
|
|
1873
|
-
border: 1px solid var(--border);
|
|
1874
|
-
border-radius: 16px;
|
|
1976
|
+
gap: 12px;
|
|
1875
1977
|
margin-bottom: 12px;
|
|
1978
|
+
overflow: visible;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
.managed-app-card {
|
|
1982
|
+
background:
|
|
1983
|
+
linear-gradient(135deg, color-mix(in srgb, var(--accent) 7%, transparent), transparent 34%),
|
|
1984
|
+
var(--surface-muted);
|
|
1985
|
+
border: 1px solid var(--border);
|
|
1986
|
+
border-radius: 18px;
|
|
1987
|
+
box-shadow: inset 0 1px 0 rgb(255 255 255 / 7%);
|
|
1988
|
+
overflow: visible;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
.managed-app-card.expanded {
|
|
1992
|
+
border-color: color-mix(in srgb, var(--accent) 32%, var(--border));
|
|
1876
1993
|
}
|
|
1877
1994
|
|
|
1878
|
-
.managed-app-
|
|
1995
|
+
.managed-app-summary {
|
|
1996
|
+
align-items: center;
|
|
1997
|
+
background: transparent;
|
|
1998
|
+
border: 0;
|
|
1999
|
+
color: var(--text-primary);
|
|
1879
2000
|
display: grid;
|
|
1880
|
-
gap:
|
|
1881
|
-
grid-template-columns: minmax(
|
|
1882
|
-
padding:
|
|
2001
|
+
gap: 16px;
|
|
2002
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
2003
|
+
padding: 16px;
|
|
2004
|
+
text-align: left;
|
|
2005
|
+
width: 100%;
|
|
1883
2006
|
}
|
|
1884
2007
|
|
|
1885
|
-
.managed-app-
|
|
1886
|
-
|
|
2008
|
+
.managed-app-summary:hover,
|
|
2009
|
+
.managed-app-summary:focus-visible {
|
|
2010
|
+
background: color-mix(in srgb, var(--accent) 7%, transparent);
|
|
2011
|
+
outline: none;
|
|
1887
2012
|
}
|
|
1888
2013
|
|
|
1889
|
-
.managed-app-
|
|
1890
|
-
.managed-app-main > div,
|
|
2014
|
+
.managed-app-title-block,
|
|
1891
2015
|
.app-meta {
|
|
1892
2016
|
display: grid;
|
|
1893
|
-
gap:
|
|
2017
|
+
gap: 7px;
|
|
1894
2018
|
min-width: 0;
|
|
1895
2019
|
}
|
|
1896
2020
|
|
|
1897
|
-
.managed-app-
|
|
1898
|
-
|
|
1899
|
-
|
|
2021
|
+
.managed-app-title-block strong {
|
|
2022
|
+
font-size: clamp(1.12rem, 2vw, 1.4rem);
|
|
2023
|
+
letter-spacing: -0.04em;
|
|
2024
|
+
line-height: 1.15;
|
|
1900
2025
|
}
|
|
1901
2026
|
|
|
1902
|
-
.managed-app-
|
|
2027
|
+
.managed-app-title-block > span:last-child {
|
|
1903
2028
|
color: var(--text-secondary);
|
|
2029
|
+
max-width: 72ch;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
.managed-app-summary-meta {
|
|
2033
|
+
align-items: center;
|
|
2034
|
+
display: flex;
|
|
2035
|
+
flex-wrap: wrap;
|
|
2036
|
+
gap: 8px;
|
|
2037
|
+
justify-content: end;
|
|
2038
|
+
min-width: min(420px, 38vw);
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
.managed-summary-chip {
|
|
2042
|
+
background: color-mix(in srgb, var(--surface-elevated) 92%, transparent);
|
|
2043
|
+
border: 1px solid var(--border);
|
|
2044
|
+
border-radius: 999px;
|
|
2045
|
+
display: inline-grid;
|
|
2046
|
+
gap: 1px;
|
|
2047
|
+
min-width: 118px;
|
|
2048
|
+
padding: 7px 11px;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
.managed-summary-chip small {
|
|
2052
|
+
color: var(--text-muted);
|
|
2053
|
+
font-size: 0.66rem;
|
|
2054
|
+
font-weight: 900;
|
|
2055
|
+
line-height: 1;
|
|
2056
|
+
text-transform: uppercase;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
.managed-summary-chip span {
|
|
2060
|
+
color: var(--text-primary);
|
|
2061
|
+
font-size: 0.84rem;
|
|
2062
|
+
font-weight: 800;
|
|
2063
|
+
max-width: 18ch;
|
|
2064
|
+
overflow: hidden;
|
|
2065
|
+
text-overflow: ellipsis;
|
|
2066
|
+
white-space: nowrap;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
.managed-app-chevron {
|
|
2070
|
+
color: var(--accent);
|
|
2071
|
+
flex: 0 0 auto;
|
|
2072
|
+
transition: transform 180ms ease;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
.managed-app-card.expanded .managed-app-chevron {
|
|
2076
|
+
transform: rotate(180deg);
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
.managed-app-body {
|
|
2080
|
+
border-top: 1px solid var(--border);
|
|
2081
|
+
display: grid;
|
|
2082
|
+
gap: 14px;
|
|
2083
|
+
padding: 0 16px 16px;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
.managed-app-toolbar {
|
|
2087
|
+
align-items: start;
|
|
2088
|
+
display: grid;
|
|
2089
|
+
gap: 12px;
|
|
2090
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
2091
|
+
padding-top: 14px;
|
|
1904
2092
|
}
|
|
1905
2093
|
|
|
1906
2094
|
.managed-app-links,
|
|
@@ -1911,6 +2099,10 @@ code {
|
|
|
1911
2099
|
gap: 8px;
|
|
1912
2100
|
}
|
|
1913
2101
|
|
|
2102
|
+
.managed-app-actions {
|
|
2103
|
+
justify-content: end;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
1914
2106
|
.managed-app-links a,
|
|
1915
2107
|
.origin-missing {
|
|
1916
2108
|
align-items: center;
|
|
@@ -1921,18 +2113,28 @@ code {
|
|
|
1921
2113
|
font-size: 0.78rem;
|
|
1922
2114
|
font-weight: 800;
|
|
1923
2115
|
gap: 4px;
|
|
2116
|
+
max-width: 100%;
|
|
1924
2117
|
padding: 6px 9px;
|
|
2118
|
+
position: relative;
|
|
1925
2119
|
text-decoration: none;
|
|
1926
2120
|
}
|
|
1927
2121
|
|
|
2122
|
+
.managed-app-links a:hover,
|
|
2123
|
+
.managed-app-links a:focus-visible {
|
|
2124
|
+
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
|
2125
|
+
border-color: color-mix(in srgb, var(--accent) 48%, var(--border));
|
|
2126
|
+
color: var(--accent);
|
|
2127
|
+
outline: none;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
1928
2130
|
.origin-missing {
|
|
1929
2131
|
color: var(--text-muted);
|
|
1930
2132
|
}
|
|
1931
2133
|
|
|
1932
2134
|
.managed-app-details {
|
|
1933
2135
|
display: grid;
|
|
1934
|
-
gap:
|
|
1935
|
-
grid-template-columns: repeat(
|
|
2136
|
+
gap: 10px;
|
|
2137
|
+
grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
|
|
1936
2138
|
}
|
|
1937
2139
|
|
|
1938
2140
|
.managed-toggle,
|
|
@@ -1941,7 +2143,8 @@ code {
|
|
|
1941
2143
|
border: 1px solid var(--border);
|
|
1942
2144
|
border-radius: 12px;
|
|
1943
2145
|
min-width: 0;
|
|
1944
|
-
|
|
2146
|
+
min-height: 64px;
|
|
2147
|
+
padding: 10px 12px;
|
|
1945
2148
|
}
|
|
1946
2149
|
|
|
1947
2150
|
.managed-toggle {
|
|
@@ -1987,30 +2190,68 @@ code {
|
|
|
1987
2190
|
.app-meta strong {
|
|
1988
2191
|
color: var(--text-primary);
|
|
1989
2192
|
font-size: 0.84rem;
|
|
2193
|
+
line-height: 1.35;
|
|
1990
2194
|
overflow-wrap: anywhere;
|
|
2195
|
+
word-break: break-word;
|
|
1991
2196
|
}
|
|
1992
2197
|
|
|
1993
2198
|
.app-action-button {
|
|
1994
2199
|
align-items: center;
|
|
1995
|
-
|
|
2200
|
+
background: color-mix(in srgb, var(--accent) 16%, var(--surface-elevated));
|
|
2201
|
+
border: 1px solid color-mix(in srgb, var(--accent) 48%, var(--border));
|
|
2202
|
+
border-radius: 999px;
|
|
2203
|
+
color: var(--accent);
|
|
1996
2204
|
display: inline-flex;
|
|
1997
|
-
|
|
2205
|
+
gap: 7px;
|
|
2206
|
+
min-height: 42px;
|
|
1998
2207
|
justify-content: center;
|
|
1999
|
-
padding: 0;
|
|
2000
|
-
|
|
2208
|
+
padding: 0 12px;
|
|
2209
|
+
position: relative;
|
|
2210
|
+
transition: background 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms ease;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
.app-action-button span {
|
|
2214
|
+
font-size: 0.7rem;
|
|
2215
|
+
font-weight: 900;
|
|
2216
|
+
text-transform: uppercase;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
.app-action-button:hover,
|
|
2220
|
+
.app-action-button:focus-visible {
|
|
2221
|
+
background: var(--accent);
|
|
2222
|
+
border-color: var(--accent);
|
|
2223
|
+
color: var(--bg-primary);
|
|
2224
|
+
outline: none;
|
|
2225
|
+
transform: translateY(-1px);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
.app-action-button.remove {
|
|
2229
|
+
background: color-mix(in srgb, #ef4444 12%, var(--surface-elevated));
|
|
2230
|
+
border-color: color-mix(in srgb, #ef4444 45%, var(--border));
|
|
2231
|
+
color: #ff8a8a;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
.app-action-button.remove:hover,
|
|
2235
|
+
.app-action-button.remove:focus-visible {
|
|
2236
|
+
background: #ef4444;
|
|
2237
|
+
border-color: #ef4444;
|
|
2238
|
+
color: white;
|
|
2001
2239
|
}
|
|
2002
2240
|
|
|
2003
2241
|
.app-action-button:disabled {
|
|
2004
2242
|
color: var(--text-muted);
|
|
2005
2243
|
cursor: not-allowed;
|
|
2006
2244
|
opacity: 0.45;
|
|
2245
|
+
transform: none;
|
|
2007
2246
|
}
|
|
2008
2247
|
|
|
2009
2248
|
.app-terminal {
|
|
2010
|
-
background:
|
|
2249
|
+
background:
|
|
2250
|
+
linear-gradient(180deg, rgb(255 255 255 / 4%), transparent),
|
|
2251
|
+
#050806;
|
|
2011
2252
|
border: 1px solid var(--border);
|
|
2012
2253
|
border-radius: 16px;
|
|
2013
|
-
color: var(--
|
|
2254
|
+
color: color-mix(in srgb, var(--accent) 78%, white);
|
|
2014
2255
|
overflow: hidden;
|
|
2015
2256
|
}
|
|
2016
2257
|
|
|
@@ -2018,16 +2259,57 @@ code {
|
|
|
2018
2259
|
align-items: center;
|
|
2019
2260
|
border-bottom: 1px solid var(--border);
|
|
2020
2261
|
display: flex;
|
|
2262
|
+
gap: 12px;
|
|
2021
2263
|
justify-content: space-between;
|
|
2022
2264
|
padding: 10px 12px;
|
|
2023
2265
|
}
|
|
2024
2266
|
|
|
2267
|
+
.app-terminal header span {
|
|
2268
|
+
color: var(--text-muted);
|
|
2269
|
+
text-align: right;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2025
2272
|
.app-terminal pre {
|
|
2026
2273
|
margin: 0;
|
|
2027
|
-
max-height:
|
|
2274
|
+
max-height: 240px;
|
|
2028
2275
|
overflow: auto;
|
|
2029
2276
|
padding: 12px;
|
|
2030
2277
|
white-space: pre-wrap;
|
|
2278
|
+
word-break: break-word;
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
[data-tooltip] {
|
|
2282
|
+
position: relative;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
[data-tooltip]::after {
|
|
2286
|
+
background: color-mix(in srgb, var(--surface-elevated) 94%, black);
|
|
2287
|
+
border: 1px solid color-mix(in srgb, var(--accent) 36%, var(--border));
|
|
2288
|
+
border-radius: 10px;
|
|
2289
|
+
bottom: calc(100% + 8px);
|
|
2290
|
+
box-shadow: 0 18px 46px rgb(0 0 0 / 38%);
|
|
2291
|
+
color: var(--text-primary);
|
|
2292
|
+
content: attr(data-tooltip);
|
|
2293
|
+
font-size: 0.72rem;
|
|
2294
|
+
font-weight: 700;
|
|
2295
|
+
left: 50%;
|
|
2296
|
+
line-height: 1.35;
|
|
2297
|
+
max-width: min(320px, 72vw);
|
|
2298
|
+
opacity: 0;
|
|
2299
|
+
padding: 8px 10px;
|
|
2300
|
+
pointer-events: none;
|
|
2301
|
+
position: absolute;
|
|
2302
|
+
text-align: left;
|
|
2303
|
+
transform: translate(-50%, 4px);
|
|
2304
|
+
transition: opacity 140ms ease, transform 140ms ease;
|
|
2305
|
+
width: max-content;
|
|
2306
|
+
z-index: 40;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
[data-tooltip]:hover::after,
|
|
2310
|
+
[data-tooltip]:focus-visible::after {
|
|
2311
|
+
opacity: 1;
|
|
2312
|
+
transform: translate(-50%, 0);
|
|
2031
2313
|
}
|
|
2032
2314
|
|
|
2033
2315
|
.data-row,
|
|
@@ -2715,9 +2997,16 @@ code {
|
|
|
2715
2997
|
grid-template-columns: 1fr;
|
|
2716
2998
|
}
|
|
2717
2999
|
|
|
2718
|
-
.managed-app-
|
|
3000
|
+
.managed-app-summary,
|
|
3001
|
+
.managed-app-toolbar {
|
|
2719
3002
|
grid-template-columns: 1fr;
|
|
2720
3003
|
}
|
|
3004
|
+
|
|
3005
|
+
.managed-app-summary-meta,
|
|
3006
|
+
.managed-app-actions {
|
|
3007
|
+
justify-content: start;
|
|
3008
|
+
min-width: 0;
|
|
3009
|
+
}
|
|
2721
3010
|
}
|
|
2722
3011
|
|
|
2723
3012
|
@media (max-width: 980px) {
|
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.
|
|
15
|
+
# Version: 3.29.8
|
|
16
16
|
#
|
|
17
17
|
# Quick Install:
|
|
18
18
|
# npm install -g aidevops && aidevops update (recommended)
|