Jelajahi Sumber

fix(web): start the approval retry with a fresh log and keep the install dialog in view

The retry that allows blocked install scripts drops the failed run from
the dialog instead of stacking the new run under it. The dialog no longer
grows past the viewport: the card is capped and everything under the
head scrolls, so the close and edit controls stay reachable however long
pnpm's output is.
Yichen Jiang 3 minggu lalu
induk
melakukan
5e5f0a2393

+ 15 - 1
packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css

@@ -375,9 +375,11 @@
   outline-offset: 1px;
 }
 
-/* Wider than the default dialog: pnpm's lines are long, and the terminal keeps them unwrapped. */
+/* Wider than the default dialog: pnpm's lines are long, and the terminal keeps them unwrapped. The card never
+   outgrows the viewport; the running screens scroll inside it so the head's controls stay reachable. */
 .installDialog {
   width: min(600px, 100%);
+  max-height: calc(100vh - 48px);
 }
 
 .installBody {
@@ -560,12 +562,24 @@
    state, the subject card, then the details toggle beside the screen's action. */
 .wizard {
   display: flex;
+  flex: 1 1 auto;
   flex-direction: column;
   gap: 16px;
   min-width: 0;
+  min-height: 0;
   padding: 16px 20px 20px;
 }
 
+/* Everything under the head: it scrolls when the runs' output makes the screen taller than the card. */
+.wizardScroll {
+  display: flex;
+  flex: 1 1 auto;
+  flex-direction: column;
+  gap: 16px;
+  min-height: 0;
+  overflow-y: auto;
+}
+
 .wizardHead {
   display: flex;
   align-items: center;

+ 69 - 67
packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx

@@ -549,78 +549,80 @@ function InstallDialog({ install, t, onClose, onEditSpec, onRun, onCancel, onTog
             <IconCloseOutline16 size={14} />
           </button>
         </div>
-        <div className={css.wizardHero}>
-          <span className={css.wizardIcon} data-tone={pending ? 'pending' : phase} aria-hidden="true">
+        <div className={css.wizardScroll}>
+          <div className={css.wizardHero}>
+            <span className={css.wizardIcon} data-tone={pending ? 'pending' : phase} aria-hidden="true">
+              {pending
+                ? <span className={css.spinnerLarge} />
+                : phase === 'done' ? <IconCheckOutline16 size={28} /> : <IconWarningOutline16 size={28} />}
+            </span>
+            <h2 className={css.wizardTitle} role={phase === 'failed' ? 'alert' : 'status'}>{heading}</h2>
+            {phase === 'failed' ? <p className={css.wizardSub}>{failureText(install.failure, t)}</p> : null}
+            {unconfirmed === undefined ? null : <p className={css.wizardSub} role="alert">{t('installCancelUnconfirmed', { reason: unconfirmed })}</p>}
+          </div>
+          {install.subject === null ? null : <SubjectCard subject={install.subject} t={t} />}
+          {approvable
+            ? (
+              <section className={css.approval} role="group" aria-labelledby={approvalId} data-install-approval>
+                <h3 id={approvalId} className={css.approvalTitle}>{t('installApprovalTitle')}</h3>
+                <p className={css.approvalText}>{t('installApprovalDescription')}</p>
+                <ul className={css.approvalList}>
+                  {pendingBuilds.map(name => <li key={name}><code>{name}</code></li>)}
+                </ul>
+                <p className={css.approvalText}>{t('installApprovalConsequence')}</p>
+                <p className={css.approvalCaution}>{t('installApprovalCaution')}</p>
+                <Button variant="primary" className={css.wide} onClick={onApproveBuilds}>{t('installApproveAndRetry')}</Button>
+              </section>
+            )
+            : null}
+          {phase === 'done' && install.installed === null
+            ? <p className={css.result} role="status">{t('installDoneNothing')}</p>
+            : null}
+          {phase === 'done' && install.restartRequired
+            ? <p className={css.resultWarn} role="status">{t('installDoneRestart')}</p>
+            : null}
+          {phase === 'done' && install.approvedBuilds.length > 0
+            ? <p className={css.result} role="status">{t('installDoneApproved', { names: install.approvedBuilds.join(', ') })}</p>
+            : null}
+          <div className={css.wizardFoot}>
+            <button type="button" className={css.detailsToggle} aria-expanded={install.detailsOpen} onClick={onToggleDetails}>
+              <span>{t(install.detailsOpen ? 'installDetailsHide' : 'installDetailsShow')}</span>
+              <IconChevronDownOutline14 className={css.detailsChevron} aria-hidden="true" />
+            </button>
             {pending
-              ? <span className={css.spinnerLarge} />
-              : phase === 'done' ? <IconCheckOutline16 size={28} /> : <IconWarningOutline16 size={28} />}
-          </span>
-          <h2 className={css.wizardTitle} role={phase === 'failed' ? 'alert' : 'status'}>{heading}</h2>
-          {phase === 'failed' ? <p className={css.wizardSub}>{failureText(install.failure, t)}</p> : null}
-          {unconfirmed === undefined ? null : <p className={css.wizardSub} role="alert">{t('installCancelUnconfirmed', { reason: unconfirmed })}</p>}
-        </div>
-        {install.subject === null ? null : <SubjectCard subject={install.subject} t={t} />}
-        {approvable
-          ? (
-            <section className={css.approval} role="group" aria-labelledby={approvalId} data-install-approval>
-              <h3 id={approvalId} className={css.approvalTitle}>{t('installApprovalTitle')}</h3>
-              <p className={css.approvalText}>{t('installApprovalDescription')}</p>
-              <ul className={css.approvalList}>
-                {pendingBuilds.map(name => <li key={name}><code>{name}</code></li>)}
-              </ul>
-              <p className={css.approvalText}>{t('installApprovalConsequence')}</p>
-              <p className={css.approvalCaution}>{t('installApprovalCaution')}</p>
-              <Button variant="primary" className={css.wide} onClick={onApproveBuilds}>{t('installApproveAndRetry')}</Button>
-            </section>
-          )
-          : null}
-        {phase === 'done' && install.installed === null
-          ? <p className={css.result} role="status">{t('installDoneNothing')}</p>
-          : null}
-        {phase === 'done' && install.restartRequired
-          ? <p className={css.resultWarn} role="status">{t('installDoneRestart')}</p>
-          : null}
-        {phase === 'done' && install.approvedBuilds.length > 0
-          ? <p className={css.result} role="status">{t('installDoneApproved', { names: install.approvedBuilds.join(', ') })}</p>
-          : null}
-        <div className={css.wizardFoot}>
-          <button type="button" className={css.detailsToggle} aria-expanded={install.detailsOpen} onClick={onToggleDetails}>
-            <span>{t(install.detailsOpen ? 'installDetailsHide' : 'installDetailsShow')}</span>
-            <IconChevronDownOutline14 className={css.detailsChevron} aria-hidden="true" />
-          </button>
-          {pending
+              ? (
+                <Button variant="outline" size="sm" disabled={phase !== 'running'} onClick={onCancel}>
+                  {t(phase === 'cancelling' ? 'installCancelling' : 'installCancel')}
+                </Button>
+              )
+              : null}
+            {phase === 'failed' && !approvable ? <Button variant="primary" size="sm" onClick={onRun}>{t('installRetry')}</Button> : null}
+          </div>
+          {install.detailsOpen
             ? (
-              <Button variant="outline" size="sm" disabled={phase !== 'running'} onClick={onCancel}>
-                {t(phase === 'cancelling' ? 'installCancelling' : 'installCancel')}
-              </Button>
+              <div className={css.detailsBody}>
+                <p className={css.installLocation}>{firstRun === undefined ? t('terminalNoOutput') : t('installLocation', { dir: firstRun.cwd })}</p>
+                {install.runs.map(run => (
+                  <TerminalBlock
+                    key={run.jobId}
+                    command={run.command}
+                    output={run.output}
+                    running={run.exitCode === undefined}
+                    exitCode={run.exitCode}
+                    maxLines={INSTALL_TERMINAL_LINES}
+                    labels={{ ...terminalLabels(t), ...phase === 'cancelling' ? { failed: t('installCancelledShort') } : {} }}
+                    className={css.terminal}
+                  />
+                ))}
+              </div>
             )
             : null}
-          {phase === 'failed' && !approvable ? <Button variant="primary" size="sm" onClick={onRun}>{t('installRetry')}</Button> : null}
+          {phase !== 'done'
+            ? null
+            : install.installed !== null
+              ? <Button variant="primary" className={css.wide} disabled={install.enabling} aria-busy={install.enabling} onClick={onEnableNow}>{t('installEnableNow')}</Button>
+              : <Button variant="primary" className={css.wide} onClick={onClose}>{t('installClose')}</Button>}
         </div>
-        {install.detailsOpen
-          ? (
-            <div className={css.detailsBody}>
-              <p className={css.installLocation}>{firstRun === undefined ? t('terminalNoOutput') : t('installLocation', { dir: firstRun.cwd })}</p>
-              {install.runs.map(run => (
-                <TerminalBlock
-                  key={run.jobId}
-                  command={run.command}
-                  output={run.output}
-                  running={run.exitCode === undefined}
-                  exitCode={run.exitCode}
-                  maxLines={INSTALL_TERMINAL_LINES}
-                  labels={{ ...terminalLabels(t), ...phase === 'cancelling' ? { failed: t('installCancelledShort') } : {} }}
-                  className={css.terminal}
-                />
-              ))}
-            </div>
-          )
-          : null}
-        {phase !== 'done'
-          ? null
-          : install.installed !== null
-            ? <Button variant="primary" className={css.wide} disabled={install.enabling} aria-busy={install.enabling} onClick={onEnableNow}>{t('installEnableNow')}</Button>
-            : <Button variant="primary" className={css.wide} onClick={onClose}>{t('installClose')}</Button>}
       </div>
     </Modal>
   )

+ 1 - 1
packages/client/ui-plugin-manager/src/client/manager-store.ts

@@ -545,7 +545,7 @@ export class PluginManagerController {
   private async startInstall(subject: InstallSubject, approvedBuilds?: readonly string[]): Promise<void> {
     const { spec } = subject
     const requestId = randomUUID() as PluginInstallRequestId
-    this.patchInstall({ phase: 'starting', requestId, subject, failure: null, installed: null, approvedBuilds: [] })
+    this.patchInstall({ phase: 'starting', requestId, subject, runs: [], failure: null, installed: null, approvedBuilds: [] })
     // The Host announces `plugin-manager/changed` while the run is still on
     // the wire, and every such event reads again; those reads must not cancel
     // the run's settlement.