@agentprojectcontext/apx 1.56.1 → 1.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,8 +18,8 @@
18
18
  <link rel="apple-touch-icon" href="/favicon/dark/apple-touch-icon.png" media="(prefers-color-scheme: dark)" />
19
19
  <link rel="manifest" href="/favicon/white/site.webmanifest" media="(prefers-color-scheme: light)" />
20
20
  <link rel="manifest" href="/favicon/dark/site.webmanifest" media="(prefers-color-scheme: dark)" />
21
- <script type="module" crossorigin src="/assets/index-B34JqOED.js"></script>
22
- <link rel="stylesheet" crossorigin href="/assets/index-CAUezTBY.css">
21
+ <script type="module" crossorigin src="/assets/index-DJ-ocXOR.js"></script>
22
+ <link rel="stylesheet" crossorigin href="/assets/index-CEI8DfVg.css">
23
23
  </head>
24
24
  <body class="bg-background text-foreground antialiased">
25
25
  <div id="root"></div>
@@ -2162,9 +2162,9 @@
2162
2162
  }
2163
2163
  },
2164
2164
  "node_modules/baseline-browser-mapping": {
2165
- "version": "2.10.41",
2166
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.41.tgz",
2167
- "integrity": "sha512-WwS7MHhqGHHlaVsqRZnhvCEMS0owDX+SxRlve7JkuH7My1Ara3ZriTmCQupPfYjxMZ8I/tgxtJYr2t7taHaH4A==",
2165
+ "version": "2.10.42",
2166
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz",
2167
+ "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==",
2168
2168
  "dev": true,
2169
2169
  "license": "Apache-2.0",
2170
2170
  "bin": {
@@ -2580,9 +2580,9 @@
2580
2580
  "license": "MIT"
2581
2581
  },
2582
2582
  "node_modules/electron-to-chromium": {
2583
- "version": "1.5.385",
2584
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.385.tgz",
2585
- "integrity": "sha512-78sa/M08MNAYHQfjoWMvOlKQqZ0ElhSm/L5HNUc96VZ3b+KvDVnngFm8sYQy0XrhTRgAhggHr5abA7yTvRdo4Q==",
2583
+ "version": "1.5.387",
2584
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.387.tgz",
2585
+ "integrity": "sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==",
2586
2586
  "dev": true,
2587
2587
  "license": "ISC"
2588
2588
  },
@@ -84,7 +84,7 @@ function Shell() {
84
84
  onOpenRoby={() => setRobyOpen(true)}
85
85
  onOpenAddProject={openAdd}
86
86
  />
87
- <main className="m-2 ml-0 flex min-w-0 flex-1 flex-col overflow-hidden rounded-xl border border-border bg-card shadow-sm">
87
+ <main className="m-2 flex min-w-0 flex-1 flex-col overflow-hidden rounded-xl border border-border bg-card shadow-sm">
88
88
  <TopBar onToggleTheme={toggle} isDark={theme === "dark"} pathname={location.pathname} />
89
89
  <div className="flex-1 overflow-y-auto">
90
90
  <Routes>
@@ -12,7 +12,6 @@ import {
12
12
  Monitor,
13
13
  Plus,
14
14
  Send,
15
- Sparkles,
16
15
  Timer,
17
16
  User,
18
17
  } from "lucide-react";
@@ -23,14 +22,15 @@ import { t } from "../../i18n";
23
22
  import type { AgentEntry, ConversationListEntry, ThreadListEntry } from "../../types/daemon";
24
23
 
25
24
  // Channel taxonomy — same channels the daemon writes ("web", "voice",
26
- // "desktop", "telegram", …) folded into 8 sidebar groups. Each group has an
27
- // icon + a fixed display order so the sidebar is stable across reloads.
25
+ // "desktop", "telegram", …) folded into sidebar groups. Each group has an
26
+ // icon + a fixed display order so the sidebar is stable across reloads. Web is
27
+ // pinned first (it's where chats started from this UI land), then the remote
28
+ // channels, with the catch-all "other" last.
28
29
  export type ChannelGroupKey =
29
- | "live"
30
+ | "web"
30
31
  | "telegram"
31
- | "voice"
32
32
  | "desktop"
33
- | "web"
33
+ | "voice"
34
34
  | "a2a"
35
35
  | "schedule"
36
36
  | "other";
@@ -42,14 +42,13 @@ interface GroupMeta {
42
42
  }
43
43
 
44
44
  const GROUP_META: Record<ChannelGroupKey, GroupMeta> = {
45
- live: { label: "Live", icon: Sparkles, order: 0 },
45
+ web: { label: "Web", icon: MessageSquare, order: 0 },
46
46
  telegram: { label: "Telegram", icon: Send, order: 1 },
47
- voice: { label: "Voice", icon: Mic, order: 2 },
48
- desktop: { label: "Desktop", icon: Monitor, order: 3 },
49
- web: { label: "Web", icon: MessageSquare, order: 4 },
50
- a2a: { label: "Agent ↔ Agent", icon: Bot, order: 5 },
51
- schedule: { label: "Schedule", icon: Timer, order: 6 },
52
- other: { label: "Other", icon: FolderOpen, order: 7 },
47
+ desktop: { label: "Desktop", icon: Monitor, order: 2 },
48
+ voice: { label: "Voice", icon: Mic, order: 3 },
49
+ a2a: { label: "Agent ↔ Agent", icon: Bot, order: 4 },
50
+ schedule: { label: "Schedule", icon: Timer, order: 5 },
51
+ other: { label: "Other", icon: FolderOpen, order: 6 },
53
52
  };
54
53
 
55
54
  function channelGroup(channel?: string): ChannelGroupKey {
@@ -78,6 +77,14 @@ export function chatKeyToString(k: ChatKey): string {
78
77
  return `thread:${k.channel}:${k.threadId}`;
79
78
  }
80
79
 
80
+ /** Display metadata carried alongside a selection so the right-pane header can
81
+ * show "Created {date} · {channel} · {agent}" without a second fetch. */
82
+ export interface ChatSelectionMeta {
83
+ channel?: string;
84
+ createdAt?: string;
85
+ title?: string;
86
+ }
87
+
81
88
  interface Props {
82
89
  pid: string;
83
90
  agents: AgentEntry[];
@@ -85,8 +92,10 @@ interface Props {
85
92
  superAgentSlug: string;
86
93
  superAgentLabel: string;
87
94
  selected: ChatKey;
88
- onSelect: (key: ChatKey) => void;
89
- onNewChat: () => void;
95
+ onSelect: (key: ChatKey, meta?: ChatSelectionMeta) => void;
96
+ /** Start a fresh in-memory session with the chosen agent (super-agent or a
97
+ * project agent). It materialises in the Web group on the first message. */
98
+ onNewChat: (agentSlug: string) => void;
90
99
  }
91
100
 
92
101
  // Per-agent SWR fetcher. Lives in a child so adding/removing agents doesn't
@@ -125,6 +134,7 @@ export function ChatList({
125
134
  const [agentFilter, setAgentFilter] = useState("");
126
135
  const [collapsed, setCollapsed] = useState<Partial<Record<ChannelGroupKey, boolean>>>({});
127
136
  const [byAgent, setByAgent] = useState<Record<string, ConversationListEntry[]>>({});
137
+ const [pickerOpen, setPickerOpen] = useState(false);
128
138
 
129
139
  // Super-agent channel threads (telegram, web quick-chat, desktop …) come from
130
140
  // the global message ledger, which is daemon-level and NOT project-scoped.
@@ -208,20 +218,16 @@ export function ChatList({
208
218
  .sort((a, b) => GROUP_META[a.key].order - GROUP_META[b.key].order);
209
219
  }, [filteredConvs, filteredThreads]);
210
220
 
211
- // "Live" agents shown at the top: super-agent + project agents matching the
212
- // current agent filter (so "filter by foo" hides the others everywhere).
213
- const liveAgents = useMemo(() => {
214
- if (agentFilter === superAgentSlug) {
215
- return [{ slug: superAgentSlug, label: superAgentLabel }];
216
- }
217
- const out: { slug: string; label: string }[] = [];
218
- if (!agentFilter) out.push({ slug: superAgentSlug, label: superAgentLabel });
219
- for (const a of agents) {
220
- if (agentFilter && a.slug !== agentFilter) continue;
221
- out.push({ slug: a.slug, label: a.slug });
222
- }
223
- return out;
224
- }, [agents, agentFilter, superAgentSlug, superAgentLabel]);
221
+ // Agents offered by the "+ New" picker: the super-agent (always available,
222
+ // listed first) followed by this project's own agents. Not affected by the
223
+ // sidebar filter you can always start a fresh chat with any of them.
224
+ const newChatAgents = useMemo(
225
+ () => [
226
+ { slug: superAgentSlug, label: superAgentLabel },
227
+ ...agents.map((a) => ({ slug: a.slug, label: a.slug })),
228
+ ],
229
+ [agents, superAgentSlug, superAgentLabel],
230
+ );
225
231
 
226
232
  const agentOptions = useMemo(
227
233
  () => [
@@ -232,7 +238,7 @@ export function ChatList({
232
238
  [agents, superAgentSlug, superAgentLabel],
233
239
  );
234
240
 
235
- const totalCount = allConvs.length + (threadsQ.data?.length || 0) + liveAgents.length;
241
+ const totalCount = allConvs.length + (threadsQ.data?.length || 0);
236
242
  const anyLoaded =
237
243
  Object.keys(byAgent).length > 0 || agents.length === 0 || !!threadsQ.data;
238
244
 
@@ -251,13 +257,48 @@ export function ChatList({
251
257
  {t("project.chat.list.count", { n: totalCount })}
252
258
  </p>
253
259
  </div>
254
- <button
255
- type="button"
256
- onClick={onNewChat}
257
- className="inline-flex items-center gap-1 rounded-md border border-border bg-accent/60 px-2 py-1 text-[11px] font-medium hover:bg-accent"
258
- >
259
- <Plus className="size-3" /> {t("project.chat.list.new")}
260
- </button>
260
+ <div className="relative">
261
+ <button
262
+ type="button"
263
+ onClick={() => setPickerOpen((o) => !o)}
264
+ className="inline-flex items-center gap-1 rounded-md border border-border bg-accent/60 px-2 py-1 text-[11px] font-medium hover:bg-accent"
265
+ >
266
+ <Plus className="size-3" /> {t("project.chat.list.new")}
267
+ </button>
268
+ {pickerOpen && (
269
+ <>
270
+ {/* Click-away layer. */}
271
+ <button
272
+ type="button"
273
+ aria-hidden
274
+ tabIndex={-1}
275
+ className="fixed inset-0 z-10 cursor-default"
276
+ onClick={() => setPickerOpen(false)}
277
+ />
278
+ <div className="absolute right-0 top-full z-20 mt-1 w-56 rounded-md border border-border bg-card p-1 shadow-lg">
279
+ <p className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-muted-fg">
280
+ {t("project.chat.list.pick_agent")}
281
+ </p>
282
+ <div className="max-h-64 overflow-y-auto">
283
+ {newChatAgents.map((a) => (
284
+ <button
285
+ key={`new-${a.slug}`}
286
+ type="button"
287
+ onClick={() => {
288
+ setPickerOpen(false);
289
+ onNewChat(a.slug);
290
+ }}
291
+ className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs hover:bg-accent/50"
292
+ >
293
+ <Bot className="size-3 shrink-0 text-muted-fg" />
294
+ <span className="truncate">{a.label}</span>
295
+ </button>
296
+ ))}
297
+ </div>
298
+ </div>
299
+ </>
300
+ )}
301
+ </div>
261
302
  </header>
262
303
 
263
304
  <div className="space-y-2 border-b border-border p-2">
@@ -276,32 +317,6 @@ export function ChatList({
276
317
  </div>
277
318
  )}
278
319
 
279
- {/* Live group — synthetic, always first. */}
280
- {liveAgents.length > 0 && (
281
- <ChannelGroup
282
- keyName="live"
283
- count={liveAgents.length}
284
- collapsed={!!collapsed.live}
285
- onToggle={() => setCollapsed((p) => ({ ...p, live: !p.live }))}
286
- >
287
- {liveAgents.map((a) => {
288
- const isRoby = a.slug === superAgentSlug;
289
- const active =
290
- selected.kind === "live" && selected.agentSlug === a.slug;
291
- return (
292
- <ChatListItem
293
- key={`live-${a.slug}`}
294
- title={isRoby ? a.label : t("project.chat.list.live_with", { slug: a.slug })}
295
- subtitle={t("project.chat.list.live_subtitle")}
296
- badge={isRoby ? "super" : a.slug}
297
- selected={active}
298
- onClick={() => onSelect({ kind: "live", agentSlug: a.slug })}
299
- />
300
- );
301
- })}
302
- </ChannelGroup>
303
- )}
304
-
305
320
  {/* Stored conversations + super-agent channel threads, grouped by channel. */}
306
321
  {groups.map((g) => (
307
322
  <ChannelGroup
@@ -327,7 +342,10 @@ export function ChatList({
327
342
  timeAgo={th.last_ts}
328
343
  selected={active}
329
344
  onClick={() =>
330
- onSelect({ kind: "thread", channel: th.channel, threadId: th.id })
345
+ onSelect(
346
+ { kind: "thread", channel: th.channel, threadId: th.id },
347
+ { channel: th.channel, createdAt: th.started_at, title: th.title },
348
+ )
331
349
  }
332
350
  />
333
351
  );
@@ -348,7 +366,10 @@ export function ChatList({
348
366
  timeAgo={c.started_at}
349
367
  selected={active}
350
368
  onClick={() =>
351
- onSelect({ kind: "conv", agentSlug: c.agent_slug, convId: c.id })
369
+ onSelect(
370
+ { kind: "conv", agentSlug: c.agent_slug, convId: c.id },
371
+ { channel: c.channel, createdAt: c.started_at, title: c.title },
372
+ )
352
373
  }
353
374
  />
354
375
  );
@@ -65,7 +65,7 @@ export function TabNav({ sections, active, onChange, collapsed = false }: TabNav
65
65
  <nav
66
66
  className={cn(
67
67
  "hidden md:flex shrink-0 flex-col gap-1 py-3 transition-all",
68
- collapsed ? "w-12 items-center px-1" : "w-52 px-2",
68
+ collapsed ? "w-12 items-center px-1" : "w-44 px-2",
69
69
  )}
70
70
  >
71
71
  {sections.map((section, si) => (
@@ -130,6 +130,44 @@ function isErrorResult(result: unknown): boolean {
130
130
  return "error" in r && !!r.error;
131
131
  }
132
132
 
133
+ /** Reconstruct chat turns from a persisted channel thread (global ledger).
134
+ * User rows start a new turn; consecutive assistant/tool rows collapse into a
135
+ * single assistant bubble with interleaved text + tool parts — mirroring how a
136
+ * live streamed turn is shaped, so tool executions render the same on reload as
137
+ * they did in real time. Persisted rows carry no live status, so it's derived
138
+ * from the stored result (error → "error", else "done"). */
139
+ function threadToChatMsgs(messages: ConversationMessage[]): ChatMsg[] {
140
+ const out: ChatMsg[] = [];
141
+ let turn: ChatMsg | null = null;
142
+ let toolSeq = 0;
143
+ for (const m of messages) {
144
+ const ts = m.ts || new Date().toISOString();
145
+ if (m.role === "user") {
146
+ turn = null;
147
+ out.push({ role: "user", parts: userPart(m.content), ts });
148
+ } else if (m.role === "assistant" || m.role === "tool") {
149
+ if (!turn) {
150
+ turn = { role: "assistant", parts: [], ts };
151
+ out.push(turn);
152
+ }
153
+ if (m.role === "tool") {
154
+ turn.parts.push({
155
+ kind: "tool",
156
+ id: `hist-${toolSeq++}`,
157
+ tool: m.tool || "tool",
158
+ args: m.args,
159
+ result: m.result,
160
+ status: isErrorResult(m.result) ? "error" : "done",
161
+ });
162
+ } else if (m.content) {
163
+ turn.parts.push({ kind: "text", text: m.content });
164
+ }
165
+ }
166
+ // system/compact rows are context-only; not rendered in the thread viewer.
167
+ }
168
+ return out;
169
+ }
170
+
133
171
  /**
134
172
  * Pure reducer: apply one NDJSON stream event to an assistant turn and return
135
173
  * the next turn. Every surface that consumes the super-agent stream (ChatTab,
@@ -398,13 +436,7 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
398
436
  try {
399
437
  const detail = await Conversations.thread(pid, channel, threadId);
400
438
  if (seq !== loadSeqRef.current) return; // superseded by a newer pick
401
- const loaded: ChatMsg[] = (detail.messages ?? [])
402
- .filter((m) => m.role === "user" || m.role === "assistant")
403
- .map((m) => ({
404
- role: m.role as "user" | "assistant",
405
- parts: [{ kind: "text", text: m.content }],
406
- ts: m.ts || new Date().toISOString(),
407
- }));
439
+ const loaded = threadToChatMsgs(detail.messages ?? []);
408
440
  // Ledger threads have no conversation file — sends continue as fresh
409
441
  // web turns with this history as previousMessages.
410
442
  convoRef.current = undefined;
@@ -319,6 +319,7 @@ export const en = {
319
319
  chat: {
320
320
  title: "Chat with agent",
321
321
  subtitle: "Direct conversations with project agents. The super-agent does not intervene.",
322
+ live_title: "Chat with {agent}",
322
323
  superagent_title: "Chat with {persona}",
323
324
  superagent_subtitle: "Chat with {persona} — the APX super-agent. Can use tools (projects, tasks, mcps, agents).",
324
325
  loaded_subtitle: "Loaded conversation with {slug}. Sending will append to this thread.",
@@ -327,7 +328,13 @@ export const en = {
327
328
  placeholder: "Type something and press enter to send (shift+enter = new line)",
328
329
  send: "Send",
329
330
  stop: "Stop",
330
- clear: "Clear",
331
+ new_session: "New session",
332
+ delete: "Delete",
333
+ delete_confirm_title: "Delete chat",
334
+ delete_confirm_desc: "This can't be undone. This chat's history will be permanently deleted.",
335
+ deleted: "Chat deleted.",
336
+ meta_created: "Created {date} · {channel}",
337
+ meta_new: "New chat · {channel}",
331
338
  copy: "copy",
332
339
  copied: "Copied.",
333
340
  stopped_marker: " [stopped]",
@@ -345,8 +352,7 @@ export const en = {
345
352
  all_agents: "All agents",
346
353
  empty: "No conversations yet. Start one from the right.",
347
354
  count: "{n} total",
348
- live_with: "Live · {slug}",
349
- live_subtitle: "In-memory session",
355
+ pick_agent: "Pick an agent",
350
356
  },
351
357
  },
352
358
 
@@ -320,6 +320,7 @@ export const es = {
320
320
  chat: {
321
321
  title: "Chat con agente",
322
322
  subtitle: "Chat directo con el agente del proyecto.",
323
+ live_title: "Chat con {agent}",
323
324
  superagent_title: "Chat con {persona}",
324
325
  superagent_subtitle: "Chat con {persona} — el super-agente APX. Puede usar tools (proyectos, tasks, mcps, agentes).",
325
326
  loaded_subtitle: "Conversación cargada con {slug}. Lo que mandes se agrega a este chat.",
@@ -328,7 +329,13 @@ export const es = {
328
329
  placeholder: "Escribí algo y enter para enviar (shift+enter = nueva línea)",
329
330
  send: "Enviar",
330
331
  stop: "Stop",
331
- clear: "Limpiar",
332
+ new_session: "Nueva sesión",
333
+ delete: "Borrar",
334
+ delete_confirm_title: "Borrar chat",
335
+ delete_confirm_desc: "Esta acción no se puede deshacer. Se elimina para siempre el historial de este chat.",
336
+ deleted: "Chat borrado.",
337
+ meta_created: "Creado {date} · {channel}",
338
+ meta_new: "Chat nuevo · {channel}",
332
339
  copy: "copiar",
333
340
  copied: "Copiado.",
334
341
  stopped_marker: " [detenido]",
@@ -346,8 +353,7 @@ export const es = {
346
353
  all_agents: "Todos los agentes",
347
354
  empty: "No hay conversaciones todavía. Arrancá una desde la derecha.",
348
355
  count: "{n} en total",
349
- live_with: "Live · {slug}",
350
- live_subtitle: "Sesión en memoria",
356
+ pick_agent: "Elegí un agente",
351
357
  },
352
358
  },
353
359
 
@@ -12,6 +12,12 @@ export const Conversations = {
12
12
  http.get<ThreadListEntry[]>(`/projects/${pid}/super-agent/threads`),
13
13
  thread: (pid: string, channel: string, id: string) =>
14
14
  http.get<ThreadDetail>(`/projects/${pid}/super-agent/threads/${channel}/${id}`),
15
+ // Delete a persisted agent conversation (its `.md` file).
16
+ remove: (pid: string, slug: string, id: string) =>
17
+ http.del<{ ok: boolean }>(`/projects/${pid}/agents/${slug}/conversations/${id}`),
18
+ // Delete a super-agent channel thread (its channel+day ledger file).
19
+ removeThread: (pid: string, channel: string, id: string) =>
20
+ http.del<{ ok: boolean }>(`/projects/${pid}/super-agent/threads/${channel}/${id}`),
15
21
  compact: (pid: string, slug: string, id?: string) =>
16
22
  http.post<{ ok?: boolean }>(
17
23
  id
@@ -72,8 +72,8 @@ export function ProjectScreen() {
72
72
  { key: "memories", label: t("project.nav.memories"), icon: Brain },
73
73
  { key: "routines", label: t("project.nav.routines"), icon: Heart },
74
74
  { key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
75
- { key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
76
75
  { key: "vars", label: t("project.nav.vars"), icon: KeyRound },
76
+ { key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
77
77
  { key: "config", label: t("project.nav.config"), icon: Settings },
78
78
  ],
79
79
  },
@@ -96,8 +96,8 @@ export function ProjectScreen() {
96
96
  { key: "routines", label: t("project.nav.routines"), icon: Heart },
97
97
  { key: "tasks", label: t("project.nav.tasks"), icon: Zap },
98
98
  { key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
99
- { key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
100
99
  { key: "vars", label: t("project.nav.vars"), icon: KeyRound },
100
+ { key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
101
101
  { key: "logs", label: t("project.nav.logs"), icon: ScrollText },
102
102
  ],
103
103
  },
@@ -141,7 +141,7 @@ export function ProjectScreen() {
141
141
  onChange={onTabChange}
142
142
  collapsed={collapsed}
143
143
  onToggleCollapse={toggle}
144
- contentClassName="w-full space-y-6 p-6 pt-3"
144
+ contentClassName="w-full space-y-6 py-6 pt-3 pr-6 pl-1"
145
145
  testId={`project-tab-${active || "overview"}`}
146
146
  >
147
147
  <Routes>
@@ -101,7 +101,7 @@ export function SettingsScreen() {
101
101
  onChange={(k) => navigate(k === "identity" ? "/settings" : `/settings/${pathFromTab(k as TabKey)}`)}
102
102
  collapsed={collapsed}
103
103
  onToggleCollapse={toggle}
104
- contentClassName={`w-full ${WIDE_TABS.has(active) ? "" : "mx-auto max-w-3xl"} space-y-6 p-6 pt-3`}
104
+ contentClassName={`w-full ${WIDE_TABS.has(active) ? "" : "mx-auto max-w-3xl"} space-y-6 py-6 pt-3 pr-6 pl-4`}
105
105
  testId={`settings-tab-${active}`}
106
106
  >
107
107
  <Panel />