[eric] share: minimal import modal, name lives in the title, one type+counts line, inline need chips, single-line trust note

This commit is contained in:
ciregenz
2026-06-16 05:08:09 -07:00
parent e53e11d28b
commit 374ded0214
3 changed files with 68 additions and 142 deletions
+1 -1
View File
@@ -30,5 +30,5 @@ def scan_app_files(files: dict[str, bytes]) -> ReviewSummary:
verdict = "warn" if findings else "clean"
if runnable:
verdict = "warn"
findings.insert(0, "This app runs code on your computer when you open it. Only import apps you trust.")
findings.insert(0, "This app runs code on your computer. Only import apps you trust.")
return ReviewSummary(verdict=verdict, findings=findings, scanned_files=scanned)
@@ -56,26 +56,11 @@ const ImportModal: React.FC<Props> = ({ preflight, open, committing, onConfirm,
<Box sx={{ px: 3, pb: 3 }}>
<IncludesList summary={preflight.summary} />
{preflight.review && preflight.review.findings.length > 0 && (
<Box
sx={{
mt: 1.5,
p: 1.25,
display: 'flex',
gap: 1,
alignItems: 'flex-start',
borderRadius: `${c.radius.md}px`,
border: `1px solid ${c.status.warning}33`,
bgcolor: `${c.status.warning}12`,
}}
>
<ShieldOutlinedIcon sx={{ fontSize: 16, color: c.status.warning, mt: '2px', flexShrink: 0 }} />
<Box>
{preflight.review.findings.map((f, i) => (
<Typography key={`rv-${i}`} sx={{ fontSize: '0.78rem', color: c.text.secondary, lineHeight: 1.5 }}>
{f}
</Typography>
))}
</Box>
<Box sx={{ mt: 1.75, display: 'flex', gap: 0.85, alignItems: 'center' }}>
<ShieldOutlinedIcon sx={{ fontSize: 15, color: c.status.warning, flexShrink: 0 }} />
<Typography sx={{ fontSize: '0.78rem', color: c.text.muted, lineHeight: 1.4 }}>
{preflight.review.findings.join(' ')}
</Typography>
</Box>
)}
{preflight.conflicts.length > 0 && (
@@ -1,9 +1,8 @@
// The "what's inside this bundle" panel, shared by the Share and Import modals:
// the root entity, the dependencies pulled in with it, and any environment
// requirements (an Action the importer must enable themselves). Long bundles
// (a dashboard pulls in every agent) read as a wall, so the contents collapse
// to a one-line count by default and expand on demand. Requirements always
// show, they're the part the importer has to act on.
// The "what's inside this bundle" panel, shared by the Share and Import modals.
// Kept deliberately spare: the bundle's name already lives in the modal title, so
// here it's just one line of type + counts, the requirements as small icon chips,
// and an optional expand for the full contents. No boxes, the modal's whitespace
// does the grouping.
import React, { useState } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
@@ -17,14 +16,6 @@ import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { BundleSummary } from './shareTypes';
// One glyph per requirement kind, so a row of needs reads as icons + names
// instead of a stack of explanatory sentences. The sentence moves to a hover.
const REQ_ICON: Record<string, React.ReactNode> = {
mcp_action: <ExtensionOutlinedIcon sx={{ fontSize: 14 }} />,
api_key: <KeyOutlinedIcon sx={{ fontSize: 14 }} />,
builtin_mode: <TuneOutlinedIcon sx={{ fontSize: 14 }} />,
};
const KIND_LABEL: Record<string, string> = {
skill: 'Skill',
app: 'App',
@@ -34,6 +25,13 @@ const KIND_LABEL: Record<string, string> = {
session: 'Agent',
};
// One glyph per requirement kind, so needs read as icons + names, not sentences.
const REQ_ICON: Record<string, React.ReactNode> = {
mcp_action: <ExtensionOutlinedIcon sx={{ fontSize: 14 }} />,
api_key: <KeyOutlinedIcon sx={{ fontSize: 14 }} />,
builtin_mode: <TuneOutlinedIcon sx={{ fontSize: 14 }} />,
};
const pluralize = (label: string, n: number): string => (n === 1 ? label : `${label}s`);
const IncludesList: React.FC<{ summary: BundleSummary }> = ({ summary }) => {
@@ -41,7 +39,7 @@ const IncludesList: React.FC<{ summary: BundleSummary }> = ({ summary }) => {
const [expanded, setExpanded] = useState(false);
const includes = summary.includes;
// One quiet line: "9 agents · 1 app", in the bundle's own type order.
// "9 agents · 1 app", in the bundle's own type order.
const order: string[] = [];
const byType = new Map<string, number>();
for (const it of includes) {
@@ -51,121 +49,64 @@ const IncludesList: React.FC<{ summary: BundleSummary }> = ({ summary }) => {
const countLine = order
.map((t) => `${byType.get(t)} ${pluralize((KIND_LABEL[t] || t).toLowerCase(), byType.get(t) || 0)}`)
.join(' · ');
const Row: React.FC<{ tag: string; name: string; detail?: string; faded?: boolean }> = ({
tag,
name,
detail,
faded,
}) => (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, py: 0.55 }}>
<Typography
sx={{
fontSize: '0.62rem',
fontWeight: 700,
letterSpacing: '0.06em',
textTransform: 'uppercase',
color: c.text.tertiary,
minWidth: 64,
flexShrink: 0,
}}
>
{tag}
</Typography>
<Typography
sx={{
fontSize: '0.85rem',
color: faded ? c.text.muted : c.text.primary,
flex: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{name}
</Typography>
{detail && (
<Typography sx={{ fontSize: '0.72rem', color: c.text.muted, flexShrink: 0 }}>{detail}</Typography>
)}
</Box>
);
const rootLabel = KIND_LABEL[summary.root.type] || summary.root.type;
return (
<Box
sx={{
border: `1px solid ${c.border.subtle}`,
borderRadius: `${c.radius.lg}px`,
bgcolor: c.bg.surface,
px: 2,
py: 1,
}}
>
<Row tag={KIND_LABEL[summary.root.type] || summary.root.type} name={summary.root.name} />
{includes.length > 0 && !expanded && (
<Box
onClick={() => setExpanded(true)}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 1,
py: 0.55,
cursor: 'pointer',
'&:hover .show-toggle': { color: c.accent.primary },
}}
>
<Typography sx={{ fontSize: '0.82rem', color: c.text.muted }}>{countLine}</Typography>
<Box className="show-toggle" sx={{ display: 'flex', alignItems: 'center', gap: 0.25, color: c.text.tertiary, transition: 'color 0.15s' }}>
<Typography sx={{ fontSize: '0.72rem' }}>Show</Typography>
<KeyboardArrowDownIcon sx={{ fontSize: 14 }} />
<Box>
{/* Lead: the bundle's type + a one-line count. The name is in the title. */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
<Typography sx={{ fontSize: '0.85rem', color: c.text.muted }}>
<Box component="span" sx={{ color: c.text.primary, fontWeight: 600 }}>{rootLabel}</Box>
{countLine && ` · ${countLine}`}
</Typography>
{includes.length > 0 && (
<Box
onClick={() => setExpanded((v) => !v)}
sx={{ display: 'flex', alignItems: 'center', gap: 0.25, color: c.text.tertiary, cursor: 'pointer', '&:hover': { color: c.accent.primary } }}
>
<Typography sx={{ fontSize: '0.72rem' }}>{expanded ? 'Hide' : 'Show'}</Typography>
<KeyboardArrowDownIcon sx={{ fontSize: 14, transform: expanded ? 'rotate(180deg)' : 'none', transition: 'transform 0.18s' }} />
</Box>
)}
</Box>
{expanded && includes.length > 0 && (
<Box sx={{ mt: 0.5, maxHeight: 184, overflowY: 'auto' }}>
{includes.map((it, i) => (
<Box key={`inc-${i}`} sx={{ display: 'flex', gap: 1, py: 0.4 }}>
<Typography sx={{ fontSize: '0.6rem', fontWeight: 700, letterSpacing: '0.05em', textTransform: 'uppercase', color: c.text.tertiary, minWidth: 56, flexShrink: 0, mt: '2px' }}>
{KIND_LABEL[it.type] || it.type}
</Typography>
<Typography sx={{ fontSize: '0.82rem', color: c.text.secondary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{it.name}
</Typography>
</Box>
))}
</Box>
)}
{includes.length > 0 && expanded && (
<>
{includes.map((it, i) => (
<Row key={`inc-${i}`} tag={KIND_LABEL[it.type] || it.type} name={it.name} detail={it.detail} />
))}
<Box
onClick={() => setExpanded(false)}
sx={{ py: 0.4, cursor: 'pointer', color: c.text.tertiary, '&:hover': { color: c.accent.primary } }}
>
<Typography sx={{ fontSize: '0.72rem' }}>Hide</Typography>
</Box>
</>
)}
{summary.requirements.length > 0 && (
<Box sx={{ mt: 0.75, pt: 0.75, borderTop: `1px solid ${c.border.subtle}` }}>
<Typography
sx={{
fontSize: '0.62rem', fontWeight: 700, letterSpacing: '0.06em',
textTransform: 'uppercase', color: c.text.tertiary, mb: 0.75,
}}
>
<Box sx={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 0.75, mt: 1.5 }}>
<Typography sx={{ fontSize: '0.62rem', fontWeight: 700, letterSpacing: '0.06em', textTransform: 'uppercase', color: c.text.tertiary, mr: 0.25 }}>
Needs
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.75 }}>
{summary.requirements.map((r, i) => (
<Tooltip key={`req-${i}`} title={r.detail || ''} placement="top" arrow disableInteractive>
<Box
sx={{
display: 'inline-flex', alignItems: 'center', gap: 0.5,
px: 1, py: 0.4, borderRadius: '999px',
border: `1px solid ${c.border.subtle}`, bgcolor: c.bg.elevated,
color: c.text.tertiary, cursor: 'default',
}}
>
{REQ_ICON[r.kind] || <ExtensionOutlinedIcon sx={{ fontSize: 14 }} />}
<Typography sx={{ fontSize: '0.76rem', color: c.text.secondary, whiteSpace: 'nowrap' }}>
{r.label}
</Typography>
</Box>
</Tooltip>
))}
</Box>
{summary.requirements.map((r, i) => (
<Tooltip key={`req-${i}`} title={r.detail || ''} placement="top" arrow disableInteractive>
<Box
sx={{
display: 'inline-flex', alignItems: 'center', gap: 0.5,
px: 1, py: 0.4, borderRadius: '999px',
border: `1px solid ${c.border.subtle}`, bgcolor: c.bg.elevated,
color: c.text.tertiary, cursor: 'default',
}}
>
{REQ_ICON[r.kind] || <ExtensionOutlinedIcon sx={{ fontSize: 14 }} />}
<Typography sx={{ fontSize: '0.76rem', color: c.text.secondary, whiteSpace: 'nowrap' }}>
{r.label}
</Typography>
</Box>
</Tooltip>
))}
</Box>
)}
</Box>