Files
deepseek-harness/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css
T
Yichen Jiang ac2bed35e2 feat(web): guide plugin installation with a pre-install check and classified failures
The install dialog asks the Host to read a spec before installing it:
`plugins/inspect` sorts the spec (registry name, absolute path, git address,
tarball), asks the registry through `pnpm view` in the profile directory, or
reads a directory's package.json, and refuses what would fail with
`plugins/inspect-rejected` and a problem the dialog words under the field.
A failed pnpm run carries a `kind` classified from how it ended and what pnpm
printed, shown as one line with the output folded behind the details.
Enabling happens after the fact from the installed screen, which scrolls the
list to the first new bundle. Refusals of the moment and the confirmed
cancellation are toasts; the run is stopped through the manager's
`cancelInstall` and the dialog offers the spec again once the Host confirms.
The non-pack dependency group folds by default under its own name.

The host config gains `inspectTimeoutMs`; the CLI passes it explicitly.
2026-09-15 12:43:44 +08:00

926 lines
17 KiB
CSS

/* Plugin manager page: a centred column with its header, notices, and the package cards. */
.page {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
box-sizing: border-box;
height: 100%;
overflow: auto;
padding: 28px clamp(24px, 4vw, 48px) 48px;
color: var(--dsw-alias-label-primary);
}
.page > * {
width: 100%;
max-width: 960px;
}
.pageHead {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.pageTitle {
margin: 0;
font-size: 20px;
font-weight: 600;
line-height: 28px;
}
.pageIntro {
margin: 4px 0 0;
font-size: 13px;
line-height: 20px;
color: var(--dsw-alias-label-secondary);
}
.toolbar {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.status,
.failure p,
.empty,
.groupSub {
margin: 0;
font-size: 12.5px;
line-height: 18px;
color: var(--dsw-alias-label-tertiary);
}
.status,
.failure p,
.empty {
font-size: 13px;
line-height: 20px;
}
.failure {
display: flex;
align-items: center;
gap: 10px;
color: var(--dsw-alias-state-error-primary);
}
.banner {
margin: 0;
padding: 8px 12px;
border-radius: 10px;
font-size: 12px;
line-height: 18px;
background: color-mix(in srgb, var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary)) 12%, transparent);
color: var(--dsw-alias-label-primary);
}
.group {
display: flex;
flex-direction: column;
gap: 8px;
}
.group + .group {
margin-top: 8px;
padding-top: 18px;
border-top: 0.5px solid var(--dsw-alias-border-l2);
}
.groupTitleRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.groupTitle {
margin: 0;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.count {
font-size: 12.5px;
color: var(--dsw-alias-label-tertiary);
font-variant-numeric: tabular-nums;
}
/* The folded group's header is its toggle; the chevron turns as it opens. */
.groupToggle {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0;
border: 0;
background: none;
font: inherit;
color: inherit;
cursor: pointer;
}
.groupChevron {
flex: none;
transform: rotate(-90deg);
transition: transform 160ms ease;
color: var(--dsw-alias-label-tertiary);
}
.groupToggle[aria-expanded='true'] .groupChevron {
transform: none;
}
.groupIntro {
margin: -2px 0 0;
font-size: 12.5px;
line-height: 18px;
color: var(--dsw-alias-label-tertiary);
}
/* The package an install just enabled: a ring that fades while the list scrolls to it. */
.card[data-plugin-highlight] {
animation: dsh-plugin-highlight 2400ms ease-out;
}
@keyframes dsh-plugin-highlight {
0%,
55% {
box-shadow: 0 0 0 2px var(--dsw-alias-state-business-primary);
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
@media (prefers-reduced-motion: reduce) {
.card[data-plugin-highlight] {
animation: none;
box-shadow: 0 0 0 2px var(--dsw-alias-state-business-primary);
}
.groupChevron {
transition: none;
}
}
.groupSub {
margin: -4px 0 4px;
}
.iconButton:focus-visible {
outline: 2px solid var(--dsw-alias-brand-primary);
outline-offset: 1px;
}
.cards,
.chips {
margin: 0;
padding: 0;
list-style: none;
}
.cards {
display: flex;
flex-direction: column;
gap: 10px;
}
.card {
min-width: 0;
border: 0.5px solid var(--dsw-alias-border-l4);
border-radius: 14px;
background: var(--dsw-alias-bg-layer-3);
/* The card is the elevated surface here; the page itself scrolls on the base
surface. Anything that scrolls inside a card draws the l2 thumb (ui-theme
styles/scrollbar.css rebinding contract). */
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
}
.cardHead {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
}
.cardMain {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.titleRow {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
min-width: 0;
}
.cardTitle {
font-size: 14px;
line-height: 20px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/*
* A package card opens its page: the title button stretches over the whole
* card, and the controls at the card's end sit above that stretch, so the
* switch and the Add to… menu keep their own clicks.
*/
.cardLink {
position: relative;
}
.cardLink:hover {
border-color: var(--dsw-alias-border-l3);
}
.cardOpen {
max-width: 100%;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: 600;
text-align: left;
cursor: pointer;
}
.cardOpen::after {
content: '';
position: absolute;
inset: 0;
border-radius: 14px;
}
.cardOpen:focus-visible {
outline: none;
}
.cardOpen:focus-visible::after {
outline: 2px solid var(--dsw-alias-brand-primary);
outline-offset: 2px;
}
.cardDesc {
font-size: 12.5px;
line-height: 18px;
color: var(--dsw-alias-label-tertiary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cardEnd {
position: relative;
z-index: 1;
display: inline-flex;
flex: none;
align-items: center;
gap: 8px;
}
.iconButton {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
}
.iconButton:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
.iconButton:disabled {
opacity: 0.5;
cursor: default;
}
.iconWrap {
display: inline-flex;
}
/* The destructive text action in a card body. */
.danger {
color: var(--dsw-alias-state-error-primary);
border-color: var(--dsw-alias-state-error-primary);
}
/* Retry, uninstall, and the switch at the end of the page's head. */
.detailActions {
display: flex;
flex: none;
align-items: center;
gap: 8px;
}
.actions {
display: flex;
align-items: center;
gap: 8px;
}
/* The delete of a row the person added: red on its own, not only on hover. */
.deleteButton {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--dsw-alias-state-error-primary);
cursor: pointer;
}
.deleteButton:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover-danger, var(--dsw-alias-interactive-bg-hover));
}
.deleteButton:disabled {
opacity: 0.5;
cursor: default;
}
.deleteButton:focus-visible {
outline: 2px solid var(--dsw-alias-brand-primary);
outline-offset: 1px;
}
.facts {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
column-gap: 14px;
row-gap: 4px;
margin: 0;
font-size: 12.5px;
line-height: 18px;
}
.facts dt {
color: var(--dsw-alias-label-tertiary);
}
.facts dd {
margin: 0;
min-width: 0;
overflow-wrap: anywhere;
color: var(--dsw-alias-label-primary);
}
.reason {
margin: 0;
font-size: 12px;
line-height: 18px;
color: var(--dsw-alias-state-error-primary);
overflow-wrap: anywhere;
/* The Host's captured tail, when no output streamed, keeps its lines. */
white-space: pre-wrap;
}
.partsHead .subLabel {
margin: 0;
}
.partsFilter {
width: 200px;
}
.partsFilter:focus-visible {
outline: 2px solid var(--dsw-alias-brand-primary);
outline-offset: 1px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
height: 24px;
padding: 0 9px 0 8px;
border-radius: 999px;
corner-shape: round;
background: var(--dsw-alias-bg-layer-1);
color: var(--dsw-alias-label-secondary);
font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
font-size: 11.5px;
}
.chip[data-state='off'] {
border: 0.5px dashed var(--dsw-alias-border-l4);
background: transparent;
color: var(--dsw-alias-label-tertiary);
}
/* Wider than the default dialog: pnpm's lines are long, and the terminal keeps them unwrapped. */
.installDialog {
width: min(600px, 100%);
}
.installBody {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
/* Where pnpm installs: the profile directory the first run names. */
.installLocation {
margin: 0;
font-size: 12px;
line-height: 18px;
color: var(--dsw-alias-label-tertiary);
overflow-wrap: anywhere;
}
.installField {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 13px;
}
.installField input[type='text'] {
height: 36px;
padding: 0 12px;
border: 0.5px solid var(--dsw-alias-border-l4);
border-radius: 10px;
background: var(--dsw-alias-bg-layer-3);
font: inherit;
font-size: 13px;
color: var(--dsw-alias-label-primary);
}
.installField input[aria-invalid='true'] {
border-color: var(--dsw-alias-state-error-primary);
}
/* What the check refused, under the field. */
.inputError {
margin: -4px 0 0;
font-size: 12px;
line-height: 18px;
color: var(--dsw-alias-state-error-primary);
}
/* The footer's only action spans the dialog, as the primary action of a short form does. */
.wide {
width: 100%;
justify-content: center;
}
/* The installing, installed, and failed screens: a head row, the centred
state, the subject card, then the details toggle beside the screen's action. */
.wizard {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
padding: 16px 20px 20px;
}
.wizardHead {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 24px;
}
.wizardBack {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0;
border: 0;
background: none;
font: inherit;
font-size: 15px;
font-weight: 600;
color: var(--dsw-alias-label-primary);
cursor: pointer;
}
.wizardClose {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
border: 0;
border-radius: 6px;
background: none;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
}
.wizardClose:hover {
background: var(--dsw-alias-bg-layer-3);
color: var(--dsw-alias-label-primary);
}
.wizardHero {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 8px 0 4px;
text-align: center;
}
.wizardIcon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
color: var(--dsw-alias-label-secondary);
}
.wizardIcon[data-tone='done'] {
color: var(--dsw-alias-state-success-primary);
}
.wizardIcon[data-tone='failed'] {
color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary));
}
.wizardTitle {
margin: 0;
font-size: 18px;
line-height: 26px;
font-weight: 600;
}
.wizardSub {
margin: 0;
font-size: 13px;
line-height: 20px;
color: var(--dsw-alias-label-secondary);
overflow-wrap: anywhere;
}
.subject {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 16px;
border: 0.5px solid var(--dsw-alias-border-l3);
border-radius: 12px;
text-align: center;
}
.subjectName {
margin: 0;
font-size: 15px;
line-height: 22px;
font-weight: 600;
overflow-wrap: anywhere;
}
.subjectDesc,
.subjectMeta {
margin: 0;
font-size: 13px;
line-height: 20px;
color: var(--dsw-alias-label-secondary);
overflow-wrap: anywhere;
}
.wizardFoot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.detailsToggle {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0;
border: 0;
background: none;
font: inherit;
font-size: 13px;
color: var(--dsw-alias-label-secondary);
cursor: pointer;
}
.detailsChevron {
transition: transform 160ms ease;
}
.detailsToggle[aria-expanded='true'] .detailsChevron {
transform: rotate(180deg);
}
.detailsBody {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}
.spinnerLarge {
width: 28px;
height: 28px;
border: 3px solid color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);
border-top-color: var(--dsw-alias-label-primary);
border-radius: 50%;
corner-shape: round;
animation: spin 900ms linear infinite;
}
.spinner {
width: 14px;
height: 14px;
flex: none;
border: 2px solid color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);
border-top-color: var(--dsw-alias-label-primary);
border-radius: 50%;
corner-shape: round;
animation: spin 900ms linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.spinner,
.spinnerLarge {
animation: none;
}
.detailsChevron {
transition: none;
}
}
.result,
.resultWarn {
margin: 0;
padding: 8px 12px;
border-radius: 10px;
font-size: 13px;
line-height: 20px;
background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);
color: var(--dsw-alias-label-primary);
}
.resultWarn {
background: color-mix(in srgb, var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary)) 12%, transparent);
}
/* The install terminal: the shared TerminalBlock at the bash card's smaller
size, its output capped so an expanded log scrolls inside the dialog. */
.terminal {
--dsl-terminal-font: var(--dsw-font-markdown-code-block-small);
--dsl-terminal-line-height: 18px;
--dsl-terminal-output-max-height: 240px;
margin: 4px 0 0;
border: 0.5px solid var(--dsw-alias-border-l1);
}
.dependents {
margin: 8px 0 0;
padding-left: 18px;
font-size: 13px;
line-height: 20px;
color: var(--dsw-alias-label-secondary);
}
.dangerButton {
--dsw-alias-button-primary-fill: var(--dsw-alias-state-error-primary);
--dsw-alias-button-primary-hover: var(--dsw-alias-state-error-primary);
}
.detail {
display: flex;
flex-direction: column;
}
.crumb {
display: inline-flex;
align-items: center;
gap: 6px;
border: 0;
padding: 0;
background: none;
color: var(--dsw-alias-label-tertiary);
font: inherit;
font-size: 12.5px;
cursor: pointer;
}
.crumb:hover {
color: var(--dsw-alias-label-primary);
}
.crumb:focus-visible {
outline: 2px solid var(--dsw-alias-brand-primary);
outline-offset: 2px;
}
.crumbIcon {
transform: rotate(90deg);
}
.crumbSep {
width: 0.5px;
height: 12px;
background: var(--dsw-alias-border-l3);
}
.crumbHere {
color: var(--dsw-alias-label-primary);
}
.detailHead {
display: flex;
align-items: flex-start;
gap: 12px;
margin: 12px 0 8px;
}
.detailMain {
display: flex;
flex: 1;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.detailTitle {
margin: 0;
font-size: 20px;
line-height: 28px;
font-weight: 600;
}
.detailDesc {
margin: 0;
color: var(--dsw-alias-label-secondary);
font-size: 14px;
line-height: 22px;
}
.detailSections {
display: flex;
flex-direction: column;
gap: 18px;
margin-top: 16px;
}
.detailSections > * + * {
padding-top: 18px;
border-top: 0.5px solid var(--dsw-alias-border-l2);
}
.detailSection {
display: flex;
flex-direction: column;
gap: 8px;
}
.sectionHead {
display: flex;
align-items: baseline;
gap: 10px;
}
.sectionTitle {
margin: 0;
font-size: 14px;
line-height: 20px;
font-weight: 600;
}
.sectionCount {
font-size: 12px;
line-height: 18px;
color: var(--dsw-alias-label-secondary);
}
/* The way into a package's page. */
.cardArrow {
flex: none;
color: var(--dsw-alias-label-tertiary);
}
/* Rows sit on the page ground, one hairline apart, so they read on a white page too. */
.rows {
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
list-style: none;
}
.row {
padding: 10px 2px;
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
}
.row:last-child {
border-bottom: 0;
}
.rowLine {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.rowIcon {
display: inline-flex;
flex: none;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 0.5px solid var(--dsw-alias-border-l3);
border-radius: 8px;
color: var(--dsw-alias-label-secondary);
}
.rowMain {
display: flex;
flex: 1;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.rowId {
font-size: 13.5px;
line-height: 20px;
font-weight: 600;
color: var(--dsw-alias-label-primary);
overflow-wrap: anywhere;
}
.row[data-state='off'] .rowId {
color: var(--dsw-alias-label-secondary);
}
.rowModule {
font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
font-size: 11.5px;
line-height: 16px;
color: var(--dsw-alias-label-tertiary);
overflow-wrap: anywhere;
}
.rowState {
display: inline-flex;
flex: none;
align-items: center;
gap: 6px;
font-size: 12.5px;
line-height: 18px;
color: var(--dsw-alias-label-secondary);
white-space: nowrap;
}
.row[data-state='failed'] .rowState {
color: var(--dsw-alias-state-error-primary);
}
.rowFailure {
margin: 4px 0 0 44px;
font-size: 12px;
line-height: 18px;
color: var(--dsw-alias-state-error-primary);
overflow-wrap: anywhere;
}