aidevops 3.29.11 → 3.29.13
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-api/src/app.ts +15 -5
- package/packages/gui-shared/src/notifications.ts +6 -6
- package/packages/gui-web/src/AppNavigation.tsx +5 -0
- package/packages/gui-web/src/AppWorkspace.tsx +14 -5
- package/packages/gui-web/src/CommsConversationSurface.tsx +49 -12
- package/packages/gui-web/src/InventorySurfaces.tsx +50 -19
- package/packages/gui-web/src/app-model.ts +1 -1
- package/packages/gui-web/src/styles.css +115 -30
- 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.13
|
package/aidevops.sh
CHANGED
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import * as readline from "node:readline";
|
|
2
3
|
import { Hono } from "hono";
|
|
3
4
|
import { APP_ACTION_ROUTE_MANIFEST, APP_ACTION_STATUS_ROUTE_MANIFEST, BANNED_ROUTE_PATTERNS, createEnvelope, FILE_EXPLORER_ROUTE_MANIFEST, type GuiAppActionId, type GuiAppActionJobSummary, STATUS_ROUTE_MANIFEST, VAULT_STATUS_ROUTE_MANIFEST } from "../../gui-shared/src";
|
|
4
5
|
import { readFileExplorer } from "./file-adapter";
|
|
@@ -150,10 +151,16 @@ function startAppActionJob(appId: string, action: GuiAppActionId, command: strin
|
|
|
150
151
|
env: { ...process.env, AIDEVOPS_NON_INTERACTIVE: "true" },
|
|
151
152
|
stdio: ["ignore", "pipe", "pipe"],
|
|
152
153
|
});
|
|
153
|
-
child.stdout
|
|
154
|
-
|
|
154
|
+
if (child.stdout !== null) {
|
|
155
|
+
const stdoutLines = readline.createInterface({ input: child.stdout, terminal: false });
|
|
156
|
+
stdoutLines.on("line", (line) => appendJobLine(job, line));
|
|
157
|
+
}
|
|
158
|
+
if (child.stderr !== null) {
|
|
159
|
+
const stderrLines = readline.createInterface({ input: child.stderr, terminal: false });
|
|
160
|
+
stderrLines.on("line", (line) => appendJobLine(job, line));
|
|
161
|
+
}
|
|
155
162
|
child.on("error", (error) => {
|
|
156
|
-
|
|
163
|
+
appendJobLine(job, error.message);
|
|
157
164
|
job.status = "failed";
|
|
158
165
|
job.finished_at = new Date().toISOString();
|
|
159
166
|
job.exit_code = 127;
|
|
@@ -167,8 +174,11 @@ function startAppActionJob(appId: string, action: GuiAppActionId, command: strin
|
|
|
167
174
|
return job;
|
|
168
175
|
}
|
|
169
176
|
|
|
170
|
-
function
|
|
171
|
-
|
|
177
|
+
function appendJobLine(job: GuiAppActionJobSummary, line: string): void {
|
|
178
|
+
if (line.length === 0) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
job.output.push(redactOutputLine(line));
|
|
172
182
|
if (job.output.length > 400) {
|
|
173
183
|
job.output.splice(1, job.output.length - 400);
|
|
174
184
|
}
|
|
@@ -17,7 +17,7 @@ const toastSeverityRank: Record<GuiNotificationSeverity, number> = {
|
|
|
17
17
|
|
|
18
18
|
const warningLineMatchers: Array<(line: string) => boolean> = [
|
|
19
19
|
(line) => line.startsWith("Pulse stalled"),
|
|
20
|
-
(line) => /contribution\(s\)
|
|
20
|
+
(line) => /contribution(\(s\)|s)?\s+needs?/i.test(line),
|
|
21
21
|
(line) => line.startsWith("[OPENCODE MAINTENANCE]"),
|
|
22
22
|
(line) => line.startsWith("[WARNING]"),
|
|
23
23
|
(line) => line.startsWith("[WARN]"),
|
|
@@ -28,7 +28,7 @@ const toastTitleRules: Array<{ matches: (line: string) => boolean; title: string
|
|
|
28
28
|
{ matches: (line) => line.startsWith("[ERROR]"), title: "Startup error" },
|
|
29
29
|
{ matches: (line) => line.startsWith("[OPENCODE MAINTENANCE]"), title: "OpenCode maintenance" },
|
|
30
30
|
{ matches: (line) => line.startsWith("Pulse stalled"), title: "Pulse stalled" },
|
|
31
|
-
{ matches: (line) => /contribution\(s\)
|
|
31
|
+
{ matches: (line) => /contribution(\(s\)|s)?\s+needs?/i.test(line), title: "External contribution review" },
|
|
32
32
|
{ matches: (line) => line.startsWith("[WARNING]") || line.startsWith("[WARN]"), title: "Startup warning" },
|
|
33
33
|
{ matches: (line) => line.startsWith("Security: all protections active"), title: "Security protections active" },
|
|
34
34
|
];
|
|
@@ -93,10 +93,10 @@ function buildToastNotifications(greetingOutput: string): GuiNotificationSummary
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
function buildGuiStateNotifications(input: BuildNotificationInput): GuiNotificationSummary[] {
|
|
96
|
-
const setupNeedsUpdate = input.setupTargets.filter((target) => target.needs_update);
|
|
97
|
-
const appNeedsUpdate = input.aiApps.filter((app) => app.needs_update);
|
|
98
|
-
const authErrors = input.oauthPool
|
|
99
|
-
const rateLimited = input.oauthPool
|
|
96
|
+
const setupNeedsUpdate = (input.setupTargets ?? []).filter((target) => target.needs_update);
|
|
97
|
+
const appNeedsUpdate = (input.aiApps ?? []).filter((app) => app.needs_update);
|
|
98
|
+
const authErrors = (input.oauthPool?.providers ?? []).filter((provider) => provider.auth_errors > 0);
|
|
99
|
+
const rateLimited = (input.oauthPool?.providers ?? []).filter((provider) => provider.rate_limited > 0);
|
|
100
100
|
const notifications: GuiNotificationSummary[] = [];
|
|
101
101
|
|
|
102
102
|
if (input.restartRequired) {
|
|
@@ -222,6 +222,11 @@ function ConversationSidebar({ conversationMode, selectedLocalRepoIndex, selecte
|
|
|
222
222
|
? []
|
|
223
223
|
: status.opencode_sessions.sessions.filter((session) => session.repo_path_ref === selectedRepo.path_ref);
|
|
224
224
|
const activeSessionId = sessions.some((session) => session.id_ref === selectedSessionId) ? selectedSessionId : sessions[0]?.id_ref;
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (activeSessionId !== selectedSessionId) {
|
|
227
|
+
setSelectedSessionId(activeSessionId);
|
|
228
|
+
}
|
|
229
|
+
}, [activeSessionId, selectedSessionId, setSelectedSessionId]);
|
|
225
230
|
const canSelectPreviousRepo = selectedLocalRepoIndex > 0;
|
|
226
231
|
const canSelectNextRepo = selectedLocalRepoIndex < repos.length - 1;
|
|
227
232
|
|
|
@@ -189,21 +189,30 @@ function NotificationsMenu({ notifications, openSurface }: { notifications: GuiN
|
|
|
189
189
|
<span>{active.length} active</span>
|
|
190
190
|
</div>
|
|
191
191
|
{preview.length === 0 ? <p>No current aidevops notifications.</p> : null}
|
|
192
|
-
{preview.map((notification) =>
|
|
192
|
+
{preview.map((notification) => (
|
|
193
|
+
<NotificationPreview
|
|
194
|
+
key={notification.id}
|
|
195
|
+
notification={notification}
|
|
196
|
+
onClick={() => {
|
|
197
|
+
const action = notification.actions.find((candidate) => candidate.enabled && candidate.kind === "surface" && isSurfaceId(candidate.surface_id));
|
|
198
|
+
openSurface(action?.kind === "surface" && isSurfaceId(action.surface_id) ? action.surface_id : "notifications");
|
|
199
|
+
}}
|
|
200
|
+
/>
|
|
201
|
+
))}
|
|
193
202
|
<button onClick={() => openSurface("notifications")} role="menuitem" type="button">Open notifications</button>
|
|
194
203
|
</div>
|
|
195
204
|
);
|
|
196
205
|
}
|
|
197
206
|
|
|
198
|
-
function NotificationPreview({ notification }: { notification: GuiNotificationSummary }): ReactElement {
|
|
207
|
+
function NotificationPreview({ notification, onClick }: { notification: GuiNotificationSummary; onClick: () => void }): ReactElement {
|
|
199
208
|
return (
|
|
200
|
-
<
|
|
209
|
+
<button className={`notification-preview ${notification.severity}`} onClick={onClick} role="menuitem" type="button">
|
|
201
210
|
<span aria-hidden="true"><NotificationIcon notification={notification} /></span>
|
|
202
211
|
<div>
|
|
203
212
|
<strong>{notification.title}</strong>
|
|
204
213
|
<small>{notification.category} · {notification.status}</small>
|
|
205
214
|
</div>
|
|
206
|
-
</
|
|
215
|
+
</button>
|
|
207
216
|
);
|
|
208
217
|
}
|
|
209
218
|
|
|
@@ -528,7 +537,7 @@ function NotificationCard({ notification, openSurface }: { notification: GuiNoti
|
|
|
528
537
|
return <button className="secondary-action" disabled={!action.enabled} key={action.id} onClick={() => openSurface(surfaceId)} type="button">{action.label}</button>;
|
|
529
538
|
}
|
|
530
539
|
|
|
531
|
-
return <button className="secondary-action" disabled key={action.id} title={action.command_preview} type="button">{action.label}</button>;
|
|
540
|
+
return <button className="secondary-action" disabled={!action.enabled} key={action.id} title={action.command_preview} type="button">{action.label}</button>;
|
|
532
541
|
})}
|
|
533
542
|
</div>
|
|
534
543
|
</div>
|
|
@@ -3,12 +3,29 @@ import { sortConversationMessageParts, sortConversationMessages, type GuiConvers
|
|
|
3
3
|
import { text } from "./app-model";
|
|
4
4
|
import { conversationThreads } from "./conversation-fixtures";
|
|
5
5
|
|
|
6
|
-
export function CommsConversationSurface({ mode }: { mode: "channels" | "directMessages" | "people" }) {
|
|
7
|
-
const
|
|
8
|
-
const
|
|
6
|
+
export function CommsConversationSurface({ mode, threads = conversationThreads }: { mode: "channels" | "directMessages" | "people"; threads?: GuiConversationThread[] }) {
|
|
7
|
+
const availableThreads = threads ?? [];
|
|
8
|
+
const conversations = mode === "directMessages" ? availableThreads.filter((thread) => thread.conversation.type === "dm" || thread.conversation.type === "group_dm") : availableThreads.filter((thread) => thread.conversation.type === "channel");
|
|
9
|
+
const selectedThread = conversations[0] ?? availableThreads[0];
|
|
9
10
|
const partsByMessage = new Map<string, GuiConversationMessagePart[]>();
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
if (!selectedThread) {
|
|
13
|
+
return (
|
|
14
|
+
<section className="chat-surface comms-surface" aria-label={mode === "directMessages" ? text.directMessages : text.channels} data-tour="comms-conversations">
|
|
15
|
+
<aside className="chat-thread-panel conversation-list-panel" aria-label="Conversation list">
|
|
16
|
+
<header className="chat-thread-header">
|
|
17
|
+
<div>
|
|
18
|
+
<p className="eyebrow">Unified conversation model</p>
|
|
19
|
+
<h2>{mode === "directMessages" ? <FiAtSign aria-hidden="true" /> : <FiHash aria-hidden="true" />} {mode === "directMessages" ? text.directMessages : text.channels}</h2>
|
|
20
|
+
</div>
|
|
21
|
+
</header>
|
|
22
|
+
<div className="notice compact-notice">No conversations yet</div>
|
|
23
|
+
</aside>
|
|
24
|
+
</section>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const part of sortConversationMessageParts(conversationParts(selectedThread))) {
|
|
12
29
|
partsByMessage.set(part.message_id, [...(partsByMessage.get(part.message_id) ?? []), part]);
|
|
13
30
|
}
|
|
14
31
|
|
|
@@ -37,10 +54,10 @@ export function CommsConversationSurface({ mode }: { mode: "channels" | "directM
|
|
|
37
54
|
<span className="count-pill">{participantSummary(selectedThread)}</span>
|
|
38
55
|
</header>
|
|
39
56
|
<div className="participant-strip">
|
|
40
|
-
{selectedThread.
|
|
57
|
+
{conversationParticipants(selectedThread).map((participant) => <span key={participant.id}>{participant.display_name}<small>{participant.kind}</small></span>)}
|
|
41
58
|
</div>
|
|
42
59
|
<div className="chat-message-list" data-tour="comms-message-timeline">
|
|
43
|
-
{sortConversationMessages(selectedThread
|
|
60
|
+
{sortConversationMessages(conversationMessages(selectedThread)).map((message) => <ConversationMessageRow key={message.id} message={message} parts={partsByMessage.get(message.id) ?? []} thread={selectedThread} />)}
|
|
44
61
|
</div>
|
|
45
62
|
<form className="chat-composer" aria-label="Conversation composer">
|
|
46
63
|
<textarea disabled placeholder={text.chatInputPlaceholder} />
|
|
@@ -52,7 +69,7 @@ export function CommsConversationSurface({ mode }: { mode: "channels" | "directM
|
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
function ConversationListItem({ thread }: { thread: GuiConversationThread }) {
|
|
55
|
-
const latestMessage = sortConversationMessages(thread
|
|
72
|
+
const latestMessage = sortConversationMessages(conversationMessages(thread)).at(-1);
|
|
56
73
|
const unread = unreadCount(thread);
|
|
57
74
|
|
|
58
75
|
return (
|
|
@@ -67,8 +84,8 @@ function ConversationListItem({ thread }: { thread: GuiConversationThread }) {
|
|
|
67
84
|
}
|
|
68
85
|
|
|
69
86
|
function ConversationMessageRow({ message, parts, thread }: { message: GuiConversationMessage; parts: GuiConversationMessagePart[]; thread: GuiConversationThread }) {
|
|
70
|
-
const sender = thread.
|
|
71
|
-
const reactions = thread.
|
|
87
|
+
const sender = conversationParticipants(thread).find((participant) => participant.id === message.sender_participant_id);
|
|
88
|
+
const reactions = conversationReactions(thread).filter((reaction) => reaction.message_id === message.id);
|
|
72
89
|
const speaker = message.sender_kind === "human" ? "user" : "assistant";
|
|
73
90
|
|
|
74
91
|
return (
|
|
@@ -82,12 +99,32 @@ function ConversationMessageRow({ message, parts, thread }: { message: GuiConver
|
|
|
82
99
|
}
|
|
83
100
|
|
|
84
101
|
function participantSummary(thread: GuiConversationThread): string {
|
|
85
|
-
const active = thread.
|
|
102
|
+
const active = conversationParticipants(thread).filter((participant) => participant.membership_state === "active").length;
|
|
86
103
|
return `${active} members`;
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
function unreadCount(thread: GuiConversationThread): number {
|
|
90
|
-
const lastSequence = Math.max(0, ...thread.
|
|
91
|
-
const localRead = thread
|
|
107
|
+
const lastSequence = Math.max(0, ...conversationMessages(thread).map((message) => message.sequence));
|
|
108
|
+
const localRead = conversationReadStates(thread)[0]?.last_read_sequence ?? 0;
|
|
92
109
|
return Math.max(0, lastSequence - localRead);
|
|
93
110
|
}
|
|
111
|
+
|
|
112
|
+
function conversationMessages(thread: GuiConversationThread): GuiConversationMessage[] {
|
|
113
|
+
return thread.messages ?? [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function conversationParts(thread: GuiConversationThread): GuiConversationMessagePart[] {
|
|
117
|
+
return thread.parts ?? [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function conversationParticipants(thread: GuiConversationThread): GuiConversationThread["participants"] {
|
|
121
|
+
return thread.participants ?? [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function conversationReactions(thread: GuiConversationThread): GuiConversationThread["reactions"] {
|
|
125
|
+
return thread.reactions ?? [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function conversationReadStates(thread: GuiConversationThread): GuiConversationThread["read_states"] {
|
|
129
|
+
return thread.read_states ?? [];
|
|
130
|
+
}
|
|
@@ -19,6 +19,7 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
|
|
|
19
19
|
const [appCollection, setAppCollection] = useState<AppCollectionId>("aidevops");
|
|
20
20
|
const [managedCategory, setManagedCategory] = useState<ManagedCategoryId>("core");
|
|
21
21
|
const [recommendedPlatform, setRecommendedPlatform] = useState<RecommendedPlatformFilterId>("all");
|
|
22
|
+
const [recommendedOs, setRecommendedOs] = useState<RecommendedOsId>("all");
|
|
22
23
|
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
|
|
23
24
|
const [expandedAppId, setExpandedAppId] = useState<string | null>(() => sortedManagedApps(status.managed_apps).find((app) => managedCategoryForApp(app) === "core")?.id ?? null);
|
|
24
25
|
const [jobs, setJobs] = useState<Record<string, GuiAppActionJobSummary>>({});
|
|
@@ -27,7 +28,9 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
|
|
|
27
28
|
const selectedJob = selectedJobId === null ? null : jobs[selectedJobId] ?? null;
|
|
28
29
|
const managedApps = sortedManagedApps(status.managed_apps).map((app) => applyManagedPolicyToggles(app, policyToggles[app.id]));
|
|
29
30
|
const visibleManagedApps = managedApps.filter((app) => managedCategoryForApp(app) === managedCategory);
|
|
30
|
-
const visibleRecommendedApps = recommendedApps.filter((app) => recommendedPlatform
|
|
31
|
+
const visibleRecommendedApps = recommendedApps.filter((app) => recommendedAppMatchesFilters(app, recommendedPlatform, recommendedOs));
|
|
32
|
+
const selectRecommendedPlatform = (value: RecommendedPlatformFilterId) => setRecommendedPlatform((current) => nextRecommendedFilterValue(current, value, "all"));
|
|
33
|
+
const selectRecommendedOs = (value: RecommendedOsId) => setRecommendedOs((current) => nextRecommendedFilterValue(current, value, "all"));
|
|
31
34
|
|
|
32
35
|
useEffect(() => {
|
|
33
36
|
setExpandedAppId(visibleManagedApps[0]?.id ?? null);
|
|
@@ -84,7 +87,7 @@ export function AppsSurface({ status }: { status: GuiStatusData }): ReactElement
|
|
|
84
87
|
))}
|
|
85
88
|
</div>
|
|
86
89
|
<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>
|
|
87
|
-
</> : <RecommendedAppsSurface apps={visibleRecommendedApps} platform={recommendedPlatform} setPlatform={
|
|
90
|
+
</> : <RecommendedAppsSurface apps={visibleRecommendedApps} os={recommendedOs} platform={recommendedPlatform} setOs={selectRecommendedOs} setPlatform={selectRecommendedPlatform} />}
|
|
88
91
|
</section>
|
|
89
92
|
);
|
|
90
93
|
}
|
|
@@ -119,11 +122,9 @@ function ManagedAppPanel({ app, expanded, job, onJob, onPolicyToggle, onToggle,
|
|
|
119
122
|
<div className="managed-app-details">
|
|
120
123
|
<ToggleSwitch checked={app.aidevops_install} disabled={lockedPolicy} label="setup installs" onChange={(value) => onPolicyToggle("setup", value)} />
|
|
121
124
|
<ToggleSwitch checked={app.aidevops_update} disabled={lockedPolicy} label="update maintains" onChange={(value) => onPolicyToggle("update", value)} />
|
|
122
|
-
<AppMeta label="Installed" value={app.installed_version} />
|
|
123
|
-
<AppMeta label="Latest" value={app.latest_version} />
|
|
124
|
-
<AppMeta label={text.path} value={app.install_path_ref} />
|
|
125
125
|
{job ? <AppLogLink job={job} /> : null}
|
|
126
126
|
</div>
|
|
127
|
+
<AppMeta className="managed-app-path" label={text.path} value={app.install_path_ref} />
|
|
127
128
|
{job ? <AppActionTerminal job={job} /> : null}
|
|
128
129
|
{policyJobs.map((policyJob) => <AppActionTerminal job={policyJob} key={policyJob.id} />)}
|
|
129
130
|
{job === null && policyJobs.length === 0 ? <p className="empty-state compact-notice">No recent command output for this app. Run an action or change a policy toggle to open this app's terminal log.</p> : null}
|
|
@@ -145,11 +146,11 @@ function OriginLink({ href, label }: { href: string; label: string }): ReactElem
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
function ToggleSwitch({ checked, disabled = false, label, onChange }: { checked: boolean; disabled?: boolean; label: string; onChange: (checked: boolean) => void }): ReactElement {
|
|
148
|
-
return <button aria-pressed={checked} className={checked ? "managed-toggle checked" : "managed-toggle"} data-tooltip={disabled ? "Essential aidevops component; policy is locked on" : undefined} disabled={disabled} onClick={() => onChange(!checked)} title={disabled ? "Essential aidevops component; policy is locked on" : undefined} type="button"><span aria-hidden="true" className={checked ? "switch-track checked" : "switch-track"}><span /></span
|
|
149
|
+
return <button aria-pressed={checked} className={checked ? "managed-toggle checked" : "managed-toggle"} data-tooltip={disabled ? "Essential aidevops component; policy is locked on" : undefined} disabled={disabled} onClick={() => onChange(!checked)} title={disabled ? "Essential aidevops component; policy is locked on" : undefined} type="button"><span className="managed-toggle-label">{label}</span><span aria-hidden="true" className={checked ? "switch-track checked" : "switch-track"}><span /></span></button>;
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
function AppMeta({ label, value }: { label: string; value: string }): ReactElement {
|
|
152
|
-
return <span className="app-meta"><small>{label}</small><strong>{value}</strong></span>;
|
|
152
|
+
function AppMeta({ className = "", label, value }: { className?: string; label: string; value: string }): ReactElement {
|
|
153
|
+
return <span className={className.length > 0 ? `app-meta ${className}` : "app-meta"}><small>{label}</small><strong>{value}</strong></span>;
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
function AppLogLink({ job }: { job: GuiAppActionJobSummary }): ReactElement {
|
|
@@ -224,7 +225,14 @@ function jobForApp(appId: string, jobs: Record<string, GuiAppActionJobSummary>,
|
|
|
224
225
|
return selectedJob;
|
|
225
226
|
}
|
|
226
227
|
|
|
227
|
-
|
|
228
|
+
let latestJob: GuiAppActionJobSummary | null = null;
|
|
229
|
+
for (const job of Object.values(jobs)) {
|
|
230
|
+
if (job.app_id === appId && (latestJob === null || job.started_at > latestJob.started_at)) {
|
|
231
|
+
latestJob = job;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return latestJob;
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
function sortedManagedApps(apps: GuiManagedAppSummary[]): GuiManagedAppSummary[] {
|
|
@@ -283,7 +291,7 @@ interface RecommendedApp {
|
|
|
283
291
|
}
|
|
284
292
|
|
|
285
293
|
const appCollectionTabs: TabOption<AppCollectionId>[] = [
|
|
286
|
-
{ id: "aidevops", label: "
|
|
294
|
+
{ id: "aidevops", label: "AIDevOps" },
|
|
287
295
|
{ id: "recommended", label: "Recommended" },
|
|
288
296
|
];
|
|
289
297
|
|
|
@@ -303,6 +311,15 @@ const recommendedPlatformTabs: TabOption<RecommendedPlatformFilterId>[] = [
|
|
|
303
311
|
{ id: "api", label: "API" },
|
|
304
312
|
];
|
|
305
313
|
|
|
314
|
+
const recommendedOsTabs: TabOption<RecommendedOsId>[] = [
|
|
315
|
+
{ id: "all", label: "All OS" },
|
|
316
|
+
{ id: "macos", label: "macOS" },
|
|
317
|
+
{ id: "linux", label: "Linux" },
|
|
318
|
+
{ id: "windows", label: "Windows" },
|
|
319
|
+
{ id: "ios", label: "iOS" },
|
|
320
|
+
{ id: "android", label: "Android" },
|
|
321
|
+
];
|
|
322
|
+
|
|
306
323
|
const recommendedApps = ([
|
|
307
324
|
{ name: "Affinity Studio", description: "Creative design suite.", websiteUrl: "https://www.affinity.studio/", alternativeToUrl: "https://alternativeto.net/software/affinity-1/", os: ["macos", "windows", "ios"], platforms: [] },
|
|
308
325
|
{ name: "Bitwarden", description: "Open source password manager.", websiteUrl: "https://bitwarden.com", alternativeToUrl: "https://alternativeto.net/software/bitwarden--free-password-manager/", repoUrl: "https://github.com/bitwarden", os: ["macos", "linux", "windows", "ios", "android"], platforms: ["webapp", "saas", "cli", "api"] },
|
|
@@ -359,16 +376,19 @@ function TabNav<T extends string>({ label, onChange, tabs, value }: { label: str
|
|
|
359
376
|
return <div aria-label={label} className="pill-tabs app-subnav" role="tablist">{tabs.map((tab) => <button aria-selected={value === tab.id} className={value === tab.id ? "active" : ""} key={tab.id} onClick={() => onChange(tab.id)} role="tab" type="button">{tab.label}</button>)}</div>;
|
|
360
377
|
}
|
|
361
378
|
|
|
362
|
-
function RecommendedAppsSurface({ apps, platform, setPlatform }: { apps: RecommendedApp[]; platform: RecommendedPlatformFilterId; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
|
|
379
|
+
function RecommendedAppsSurface({ apps, os, platform, setOs, setPlatform }: { apps: RecommendedApp[]; os: RecommendedOsId; platform: RecommendedPlatformFilterId; setOs: (os: RecommendedOsId) => void; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
|
|
363
380
|
return <>
|
|
364
|
-
<
|
|
381
|
+
<div className="recommended-filter-tabs">
|
|
382
|
+
<TabNav label="Recommended app operating system filters" tabs={recommendedOsTabs} value={os} onChange={setOs} />
|
|
383
|
+
<TabNav label="Recommended app platform filters" tabs={recommendedPlatformTabs} value={platform} onChange={setPlatform} />
|
|
384
|
+
</div>
|
|
365
385
|
<div className="recommended-app-grid">
|
|
366
|
-
{apps.map((app) => <RecommendedAppCard app={app} key={app.name} setPlatform={setPlatform} />)}
|
|
386
|
+
{apps.map((app) => <RecommendedAppCard app={app} key={app.name} setOs={setOs} setPlatform={setPlatform} />)}
|
|
367
387
|
</div>
|
|
368
388
|
</>;
|
|
369
389
|
}
|
|
370
390
|
|
|
371
|
-
function RecommendedAppCard({ app, setPlatform }: { app: RecommendedApp; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
|
|
391
|
+
function RecommendedAppCard({ app, setOs, setPlatform }: { app: RecommendedApp; setOs: (os: RecommendedOsId) => void; setPlatform: (platform: RecommendedPlatformFilterId) => void }): ReactElement {
|
|
372
392
|
return <article className="recommended-app-card">
|
|
373
393
|
<div>
|
|
374
394
|
<strong>{app.name}</strong>
|
|
@@ -376,7 +396,7 @@ function RecommendedAppCard({ app, setPlatform }: { app: RecommendedApp; setPlat
|
|
|
376
396
|
</div>
|
|
377
397
|
<div className="app-icon-groups">
|
|
378
398
|
<PlatformIconList platforms={app.platforms} setPlatform={setPlatform} />
|
|
379
|
-
<OsIconList os={app.os} />
|
|
399
|
+
<OsIconList os={app.os} setOs={setOs} />
|
|
380
400
|
</div>
|
|
381
401
|
<div className="managed-app-links plain-links">
|
|
382
402
|
<OriginLink href={app.websiteUrl} label={text.website} />
|
|
@@ -410,15 +430,15 @@ function PlatformIcon({ id, setPlatform }: { id: RecommendedPlatformId; setPlatf
|
|
|
410
430
|
return <button aria-label={`Filter by ${label}`} data-tooltip={`Filter by ${label}`} onClick={() => setPlatform(id)} title={`Filter by ${label}`} type="button"><Icon aria-hidden="true" focusable="false" /></button>;
|
|
411
431
|
}
|
|
412
432
|
|
|
413
|
-
function OsIconList({ os }: { os: RecommendedOsId[] }): ReactElement | null {
|
|
433
|
+
function OsIconList({ os, setOs }: { os: RecommendedOsId[]; setOs: (os: RecommendedOsId) => void }): ReactElement | null {
|
|
414
434
|
if (os.length === 0) {
|
|
415
435
|
return null;
|
|
416
436
|
}
|
|
417
437
|
|
|
418
|
-
return <span className="os-icon-list">{os.map((item) => <OsIcon id={item} key={item} />)}</span>;
|
|
438
|
+
return <span className="os-icon-list">{os.map((item) => <OsIcon id={item} key={item} setOs={setOs} />)}</span>;
|
|
419
439
|
}
|
|
420
440
|
|
|
421
|
-
function OsIcon({ id }: { id: RecommendedOsId }): ReactElement {
|
|
441
|
+
function OsIcon({ id, setOs }: { id: RecommendedOsId; setOs: (os: RecommendedOsId) => void }): ReactElement {
|
|
422
442
|
const iconMap: Record<RecommendedOsId, { Icon: IconType; label: string }> = {
|
|
423
443
|
all: { Icon: FiGlobe, label: "Web app" },
|
|
424
444
|
macos: { Icon: FaApple, label: "macOS" },
|
|
@@ -429,7 +449,18 @@ function OsIcon({ id }: { id: RecommendedOsId }): ReactElement {
|
|
|
429
449
|
};
|
|
430
450
|
const { Icon, label } = iconMap[id];
|
|
431
451
|
|
|
432
|
-
return <
|
|
452
|
+
return <button aria-label={`Filter by ${label}`} data-tooltip={`Filter by ${label}`} onClick={() => setOs(id)} title={`Filter by ${label}`} type="button"><Icon aria-hidden="true" focusable="false" /></button>;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function recommendedAppMatchesFilters(app: RecommendedApp, platform: RecommendedPlatformFilterId, os: RecommendedOsId): boolean {
|
|
456
|
+
const platformMatches = platform === "all" || app.platforms.includes(platform);
|
|
457
|
+
const osMatches = os === "all" || app.os.includes(os);
|
|
458
|
+
|
|
459
|
+
return platformMatches && osMatches;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function nextRecommendedFilterValue<T extends string>(current: T, next: T, defaultValue: T): T {
|
|
463
|
+
return current === next && next !== defaultValue ? defaultValue : next;
|
|
433
464
|
}
|
|
434
465
|
|
|
435
466
|
function applyManagedPolicyToggles(app: GuiManagedAppSummary, policy: Partial<Record<ManagedPolicyId, boolean>> | undefined): GuiManagedAppSummary {
|
|
@@ -147,7 +147,7 @@ export const text = {
|
|
|
147
147
|
aidevops: "aidevops",
|
|
148
148
|
appShell: "App shell",
|
|
149
149
|
apps: "Apps",
|
|
150
|
-
appsIntro: "
|
|
150
|
+
appsIntro: "The app toolkit we use and recommend to enable all the things we can do with AI, development, and operations to create and share our work. Opinionated preferences based on open-source-first, lowest-costs, highest-quality UI & UX, teamwork, data security, privacy, and AI-usage capabilities.",
|
|
151
151
|
appStatusPending: "adapter pending",
|
|
152
152
|
brands: "Brands",
|
|
153
153
|
brandsIntro: "Draft brand inventory for names and website references.",
|
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
--font-family-app: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
14
14
|
--font-size-app: 16px;
|
|
15
15
|
--glass-panel-shadow: inset 0 1px 0 rgb(255 255 255 / 58%), inset 0 -1px 0 rgb(0 0 0 / 3%), var(--shadow-soft);
|
|
16
|
+
--notification-error-bg: rgb(207 34 46 / 10%);
|
|
17
|
+
--notification-error-fg: #cf222e;
|
|
18
|
+
--notification-info-bg: rgb(9 105 218 / 10%);
|
|
19
|
+
--notification-info-fg: #0969da;
|
|
20
|
+
--notification-success-bg: rgb(26 127 55 / 10%);
|
|
21
|
+
--notification-success-fg: #1a7f37;
|
|
22
|
+
--notification-warning-bg: rgb(191 135 0 / 12%);
|
|
23
|
+
--notification-warning-fg: #9a6700;
|
|
16
24
|
--shell-gutter: 8px;
|
|
17
25
|
--shadow-soft: 0 18px 56px rgb(15 23 42 / 9%);
|
|
18
26
|
--surface-elevated: #ffffff;
|
|
@@ -43,6 +51,14 @@
|
|
|
43
51
|
--border-hover: color-mix(in srgb, rgb(255 255 255 / 22%) 74%, rgb(255 255 255 / 40%));
|
|
44
52
|
--code-bg: hsl(var(--accent-hue) 18% 6%);
|
|
45
53
|
--glass-panel-shadow: inset 0 1px 0 rgb(255 255 255 / 10%), inset 0 -1px 0 rgb(0 0 0 / 24%), var(--shadow-soft);
|
|
54
|
+
--notification-error-bg: rgb(255 123 114 / 13%);
|
|
55
|
+
--notification-error-fg: #ff7b72;
|
|
56
|
+
--notification-info-bg: rgb(88 166 255 / 13%);
|
|
57
|
+
--notification-info-fg: #58a6ff;
|
|
58
|
+
--notification-success-bg: rgb(86 211 100 / 13%);
|
|
59
|
+
--notification-success-fg: #56d364;
|
|
60
|
+
--notification-warning-bg: rgb(210 153 34 / 15%);
|
|
61
|
+
--notification-warning-fg: #d29922;
|
|
46
62
|
--shadow-soft: 0 24px 80px rgb(0 0 0 / 40%);
|
|
47
63
|
--surface-elevated: #151515;
|
|
48
64
|
--surface-muted: #202020;
|
|
@@ -927,9 +943,8 @@ code {
|
|
|
927
943
|
.header-actions > .header-action-menu {
|
|
928
944
|
align-items: center;
|
|
929
945
|
display: inline-flex;
|
|
930
|
-
flex: 0 0
|
|
946
|
+
flex: 0 0 auto;
|
|
931
947
|
justify-content: center;
|
|
932
|
-
width: 38px;
|
|
933
948
|
}
|
|
934
949
|
|
|
935
950
|
.workspace-search {
|
|
@@ -985,6 +1000,7 @@ code {
|
|
|
985
1000
|
}
|
|
986
1001
|
|
|
987
1002
|
.header-action-menu {
|
|
1003
|
+
gap: 10px;
|
|
988
1004
|
position: relative;
|
|
989
1005
|
}
|
|
990
1006
|
|
|
@@ -1082,11 +1098,22 @@ code {
|
|
|
1082
1098
|
}
|
|
1083
1099
|
|
|
1084
1100
|
.notification-preview {
|
|
1101
|
+
background: var(--notification-bg, transparent);
|
|
1085
1102
|
border: 1px solid var(--border);
|
|
1086
1103
|
border-left: 4px solid var(--notification-color, var(--accent));
|
|
1087
1104
|
border-radius: 12px;
|
|
1105
|
+
color: var(--text-secondary);
|
|
1106
|
+
cursor: pointer;
|
|
1088
1107
|
gap: 10px;
|
|
1089
1108
|
padding: 10px;
|
|
1109
|
+
text-align: left;
|
|
1110
|
+
width: 100%;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
.notification-preview:hover,
|
|
1114
|
+
.notification-preview:focus-visible {
|
|
1115
|
+
border-color: var(--notification-color, var(--accent));
|
|
1116
|
+
outline: none;
|
|
1090
1117
|
}
|
|
1091
1118
|
|
|
1092
1119
|
.notification-preview > span {
|
|
@@ -1106,29 +1133,29 @@ code {
|
|
|
1106
1133
|
|
|
1107
1134
|
.notification-preview.error,
|
|
1108
1135
|
.github-notification-card.error {
|
|
1109
|
-
--notification-color:
|
|
1110
|
-
--notification-bg:
|
|
1136
|
+
--notification-color: var(--notification-error-fg);
|
|
1137
|
+
--notification-bg: var(--notification-error-bg);
|
|
1111
1138
|
}
|
|
1112
1139
|
|
|
1113
1140
|
.notification-preview.warning,
|
|
1114
1141
|
.github-notification-card.warning {
|
|
1115
|
-
--notification-color:
|
|
1116
|
-
--notification-bg:
|
|
1142
|
+
--notification-color: var(--notification-warning-fg);
|
|
1143
|
+
--notification-bg: var(--notification-warning-bg);
|
|
1117
1144
|
}
|
|
1118
1145
|
|
|
1119
1146
|
.notification-preview.info,
|
|
1120
1147
|
.github-notification-card.info {
|
|
1121
|
-
--notification-color:
|
|
1122
|
-
--notification-bg:
|
|
1148
|
+
--notification-color: var(--notification-info-fg);
|
|
1149
|
+
--notification-bg: var(--notification-info-bg);
|
|
1123
1150
|
}
|
|
1124
1151
|
|
|
1125
1152
|
.notification-preview.success,
|
|
1126
1153
|
.github-notification-card.success {
|
|
1127
|
-
--notification-color:
|
|
1128
|
-
--notification-bg:
|
|
1154
|
+
--notification-color: var(--notification-success-fg);
|
|
1155
|
+
--notification-bg: var(--notification-success-bg);
|
|
1129
1156
|
}
|
|
1130
1157
|
|
|
1131
|
-
.popover-menu button,
|
|
1158
|
+
.popover-menu button:not(.notification-preview),
|
|
1132
1159
|
.popover-menu a {
|
|
1133
1160
|
align-items: center;
|
|
1134
1161
|
background: transparent;
|
|
@@ -1142,9 +1169,9 @@ code {
|
|
|
1142
1169
|
text-decoration: none;
|
|
1143
1170
|
}
|
|
1144
1171
|
|
|
1145
|
-
.popover-menu button:hover:not(:disabled),
|
|
1172
|
+
.popover-menu button:not(.notification-preview):hover:not(:disabled),
|
|
1146
1173
|
.popover-menu a:hover,
|
|
1147
|
-
.popover-menu button:focus-visible,
|
|
1174
|
+
.popover-menu button:not(.notification-preview):focus-visible,
|
|
1148
1175
|
.popover-menu a:focus-visible {
|
|
1149
1176
|
background: var(--surface-muted);
|
|
1150
1177
|
color: var(--accent);
|
|
@@ -1757,15 +1784,23 @@ code {
|
|
|
1757
1784
|
}
|
|
1758
1785
|
|
|
1759
1786
|
.section-heading {
|
|
1760
|
-
margin-bottom:
|
|
1787
|
+
margin-bottom: 16px;
|
|
1761
1788
|
max-width: 860px;
|
|
1762
1789
|
}
|
|
1763
1790
|
|
|
1791
|
+
.section-heading .eyebrow {
|
|
1792
|
+
margin: 0 0 9px;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1764
1795
|
.section-heading h2 {
|
|
1765
1796
|
font-size: 1.7rem;
|
|
1766
1797
|
letter-spacing: -0.05em;
|
|
1767
1798
|
line-height: 1.04;
|
|
1768
|
-
margin:
|
|
1799
|
+
margin: 0 0 16px;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
.section-heading p {
|
|
1803
|
+
margin: 0;
|
|
1769
1804
|
}
|
|
1770
1805
|
|
|
1771
1806
|
.section-heading p,
|
|
@@ -1976,13 +2011,25 @@ code {
|
|
|
1976
2011
|
}
|
|
1977
2012
|
|
|
1978
2013
|
.apps-surface {
|
|
1979
|
-
display: grid;
|
|
1980
2014
|
align-content: start;
|
|
1981
|
-
|
|
2015
|
+
display: grid;
|
|
2016
|
+
gap: 0;
|
|
1982
2017
|
}
|
|
1983
2018
|
|
|
1984
2019
|
.apps-surface .section-heading {
|
|
1985
|
-
margin-bottom:
|
|
2020
|
+
margin-bottom: 18px;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
.apps-surface > .app-subnav {
|
|
2024
|
+
margin-bottom: 18px;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
.apps-surface > .app-subnav + .app-subnav {
|
|
2028
|
+
margin-bottom: 16px;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
.apps-surface .compact-notice {
|
|
2032
|
+
margin-bottom: 0;
|
|
1986
2033
|
}
|
|
1987
2034
|
|
|
1988
2035
|
.data-table,
|
|
@@ -1994,8 +2041,8 @@ code {
|
|
|
1994
2041
|
}
|
|
1995
2042
|
|
|
1996
2043
|
.managed-app-list {
|
|
1997
|
-
gap:
|
|
1998
|
-
margin-bottom:
|
|
2044
|
+
gap: 16px;
|
|
2045
|
+
margin-bottom: 16px;
|
|
1999
2046
|
overflow: visible;
|
|
2000
2047
|
}
|
|
2001
2048
|
|
|
@@ -2006,7 +2053,7 @@ code {
|
|
|
2006
2053
|
border: 1px solid var(--border);
|
|
2007
2054
|
border-radius: 18px;
|
|
2008
2055
|
box-shadow: inset 0 1px 0 rgb(255 255 255 / 7%);
|
|
2009
|
-
overflow:
|
|
2056
|
+
overflow: visible;
|
|
2010
2057
|
transition: background 140ms ease, border-color 140ms ease;
|
|
2011
2058
|
}
|
|
2012
2059
|
|
|
@@ -2044,14 +2091,23 @@ code {
|
|
|
2044
2091
|
.managed-app-title-block,
|
|
2045
2092
|
.app-meta {
|
|
2046
2093
|
display: grid;
|
|
2047
|
-
gap: 7px;
|
|
2048
2094
|
min-width: 0;
|
|
2049
2095
|
}
|
|
2050
2096
|
|
|
2097
|
+
.managed-app-title-block {
|
|
2098
|
+
gap: 0;
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
.managed-app-title-block .eyebrow {
|
|
2102
|
+
line-height: 1;
|
|
2103
|
+
margin: 0 0 8px;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2051
2106
|
.managed-app-title-block strong {
|
|
2052
2107
|
font-size: clamp(1.12rem, 2vw, 1.4rem);
|
|
2053
2108
|
letter-spacing: -0.04em;
|
|
2054
2109
|
line-height: 1.15;
|
|
2110
|
+
margin-bottom: 16px;
|
|
2055
2111
|
}
|
|
2056
2112
|
|
|
2057
2113
|
.managed-app-title-block > span:last-child {
|
|
@@ -2070,14 +2126,16 @@ code {
|
|
|
2070
2126
|
|
|
2071
2127
|
.managed-summary-chip {
|
|
2072
2128
|
align-items: center;
|
|
2129
|
+
align-content: center;
|
|
2073
2130
|
background: color-mix(in srgb, var(--surface-elevated) 92%, transparent);
|
|
2074
2131
|
border: 1px solid var(--border);
|
|
2075
2132
|
border-radius: 999px;
|
|
2076
2133
|
display: inline-grid;
|
|
2077
2134
|
gap: 1px;
|
|
2078
2135
|
justify-items: center;
|
|
2136
|
+
min-height: 58px;
|
|
2079
2137
|
min-width: 118px;
|
|
2080
|
-
padding: 7px
|
|
2138
|
+
padding: 7px 12px;
|
|
2081
2139
|
text-align: center;
|
|
2082
2140
|
}
|
|
2083
2141
|
|
|
@@ -2093,6 +2151,7 @@ code {
|
|
|
2093
2151
|
color: var(--text-primary);
|
|
2094
2152
|
font-size: 0.84rem;
|
|
2095
2153
|
font-weight: 800;
|
|
2154
|
+
line-height: 1.15;
|
|
2096
2155
|
max-width: 18ch;
|
|
2097
2156
|
overflow: hidden;
|
|
2098
2157
|
text-overflow: ellipsis;
|
|
@@ -2112,7 +2171,7 @@ code {
|
|
|
2112
2171
|
.managed-app-body {
|
|
2113
2172
|
border-top: 1px solid var(--border);
|
|
2114
2173
|
display: grid;
|
|
2115
|
-
gap:
|
|
2174
|
+
gap: 16px;
|
|
2116
2175
|
padding: 0 16px 16px;
|
|
2117
2176
|
position: relative;
|
|
2118
2177
|
}
|
|
@@ -2120,9 +2179,9 @@ code {
|
|
|
2120
2179
|
.managed-app-toolbar {
|
|
2121
2180
|
align-items: start;
|
|
2122
2181
|
display: grid;
|
|
2123
|
-
gap:
|
|
2182
|
+
gap: 16px;
|
|
2124
2183
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
2125
|
-
padding-top:
|
|
2184
|
+
padding-top: 16px;
|
|
2126
2185
|
}
|
|
2127
2186
|
|
|
2128
2187
|
.managed-app-links,
|
|
@@ -2168,11 +2227,16 @@ code {
|
|
|
2168
2227
|
grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
|
|
2169
2228
|
}
|
|
2170
2229
|
|
|
2230
|
+
.managed-app-path {
|
|
2231
|
+
min-height: auto;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2171
2234
|
.managed-toggle,
|
|
2172
2235
|
.app-meta {
|
|
2173
2236
|
background: var(--surface-elevated);
|
|
2174
2237
|
border: 1px solid var(--border);
|
|
2175
2238
|
border-radius: 12px;
|
|
2239
|
+
gap: 7px;
|
|
2176
2240
|
min-width: 0;
|
|
2177
2241
|
min-height: 64px;
|
|
2178
2242
|
padding: 10px 12px;
|
|
@@ -2184,10 +2248,15 @@ code {
|
|
|
2184
2248
|
display: flex;
|
|
2185
2249
|
font-size: 0.78rem;
|
|
2186
2250
|
font-weight: 800;
|
|
2187
|
-
gap:
|
|
2251
|
+
gap: 12px;
|
|
2252
|
+
justify-content: space-between;
|
|
2188
2253
|
text-transform: uppercase;
|
|
2189
2254
|
}
|
|
2190
2255
|
|
|
2256
|
+
.managed-toggle-label {
|
|
2257
|
+
min-width: 0;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2191
2260
|
button.managed-toggle {
|
|
2192
2261
|
cursor: pointer;
|
|
2193
2262
|
text-align: left;
|
|
@@ -2209,6 +2278,7 @@ button.managed-toggle:focus-visible {
|
|
|
2209
2278
|
border: 1px solid color-mix(in srgb, #ef4444 44%, var(--border));
|
|
2210
2279
|
border-radius: 999px;
|
|
2211
2280
|
display: inline-flex;
|
|
2281
|
+
flex: 0 0 auto;
|
|
2212
2282
|
padding: 2px;
|
|
2213
2283
|
width: 34px;
|
|
2214
2284
|
}
|
|
@@ -2266,19 +2336,27 @@ button.managed-toggle:focus-visible {
|
|
|
2266
2336
|
color: var(--accent);
|
|
2267
2337
|
display: inline-flex;
|
|
2268
2338
|
gap: 7px;
|
|
2269
|
-
min-height: 42px;
|
|
2270
2339
|
justify-content: center;
|
|
2340
|
+
line-height: 1;
|
|
2341
|
+
min-height: 42px;
|
|
2271
2342
|
padding: 0 12px;
|
|
2272
2343
|
position: relative;
|
|
2273
2344
|
transition: background 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms ease;
|
|
2274
2345
|
}
|
|
2275
2346
|
|
|
2276
2347
|
.app-action-button span {
|
|
2348
|
+
align-items: center;
|
|
2349
|
+
display: inline-flex;
|
|
2277
2350
|
font-size: 0.7rem;
|
|
2278
2351
|
font-weight: 900;
|
|
2352
|
+
line-height: 1;
|
|
2279
2353
|
text-transform: uppercase;
|
|
2280
2354
|
}
|
|
2281
2355
|
|
|
2356
|
+
.app-action-button svg {
|
|
2357
|
+
flex: 0 0 auto;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2282
2360
|
.app-action-button:hover,
|
|
2283
2361
|
.app-action-button:focus-visible {
|
|
2284
2362
|
background: var(--accent);
|
|
@@ -2394,7 +2472,7 @@ button.managed-toggle:focus-visible {
|
|
|
2394
2472
|
content: attr(data-tooltip);
|
|
2395
2473
|
font-size: 0.72rem;
|
|
2396
2474
|
font-weight: 700;
|
|
2397
|
-
left:
|
|
2475
|
+
left: 50%;
|
|
2398
2476
|
line-height: 1.35;
|
|
2399
2477
|
max-width: min(320px, 72vw);
|
|
2400
2478
|
opacity: 0;
|
|
@@ -2405,7 +2483,7 @@ button.managed-toggle:focus-visible {
|
|
|
2405
2483
|
transform: translate(-50%, 4px);
|
|
2406
2484
|
transition: opacity 140ms ease, transform 140ms ease;
|
|
2407
2485
|
width: max-content;
|
|
2408
|
-
z-index:
|
|
2486
|
+
z-index: 1000;
|
|
2409
2487
|
}
|
|
2410
2488
|
|
|
2411
2489
|
[data-tooltip]:hover::after,
|
|
@@ -2496,6 +2574,13 @@ button.managed-toggle:focus-visible {
|
|
|
2496
2574
|
justify-self: start;
|
|
2497
2575
|
}
|
|
2498
2576
|
|
|
2577
|
+
.recommended-filter-tabs {
|
|
2578
|
+
display: flex;
|
|
2579
|
+
flex-wrap: wrap;
|
|
2580
|
+
gap: 16px;
|
|
2581
|
+
margin-bottom: 16px;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2499
2584
|
.recommended-app-grid {
|
|
2500
2585
|
display: grid;
|
|
2501
2586
|
gap: 12px;
|
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.13
|
|
16
16
|
#
|
|
17
17
|
# Quick Install:
|
|
18
18
|
# npm install -g aidevops && aidevops update (recommended)
|