From 5deb5285da366df46c2970b5b99323edd15ac423 Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Mon, 21 Sep 2026 00:19:25 +0800 Subject: [PATCH] refactor(tasks-ui): align the page with the latest shadcn/ui defaults The previous round kept a hand-trimmed, shadow-less, 11-13px variant of the component set. This replaces it with the registry's own defaults and the registry's own composition primitives, keeping only the adaptations this host actually requires. Registry refresh (npx shadcn@latest add --overwrite): the whole vendored set is back to upstream sources, plus label, field, input-group, item and button-group. Re-applied adaptations (documented in the design doc): - cn from ./utils and explicit ./x.tsx specifiers (the registry writes virtual aliases this repo has no resolver for); - host IconXxx instead of lucide (skin contract); - no dark: variants (the DSH tokens already flip) and no palette literals; - React.forwardRef on Button (the host is React 18, so radix Slot needs it); - tw-animate-css animation utilities stripped: installing that dependency would inject generic keyframes into a page we do not own. Restored to stock: the full Button variant/size set, shadow-xs on outline, transition-all, badge ghost/link. Recomposition on the newest primitives (per-file, verified here): - board: full Card composition, Item rows, ToggleGroup filter, dropdown items grouped, Empty for the empty state; - task window: Card composition, FieldGroup/Field with data-invalid validation, ToggleGroup owner (default variant: the outline variant's hover and pressed states measured identically, hiding the selection), destructive-armed delete, FieldError for conflicts; - popovers: both portal cards use the same Card composition, rows are Item; - drawer/header: Item rows with ItemActions, Separator bands, Spinner, Empty; - graph/tree: stock node recipe, outline buttons, cn() for conditional classes. Two no-preflight traps found by rendering, not by types: - stock Empty ships border-dashed without a width, so the UA's 3px medium dashed frame wrapped the whole page: border-0 on that Empty; - stayed type-clean after the refresh (size is a valid span attribute) while the glyph silently rendered at size-4: className="size-3". Verified: typecheck/lint clean; 136 files / 1515 tests; build with the core and chunk bundle guards (core 836.3 KiB, chunk 734.7 KiB); mount e2e 7/7; and the real-host-stylesheet mirror page re-shot for graph, tree, board, task window and the 77-node stress fixture at 360 and 720. --- ...09-14-tasks-graph-workflow-teams-design.md | 39 +- src/client/JobsDrawer.tsx | 190 ++++++---- src/client/SubagentView.tsx | 74 ++-- src/client/TaskWindow.tsx | 213 ++++++----- src/client/TasksGraph.tsx | 14 +- src/client/TasksPopovers.tsx | 233 +++++++----- src/client/TasksTree.tsx | 23 +- src/client/TeamBoard.tsx | 301 ++++++++-------- src/client/ui/badge.tsx | 72 ++-- src/client/ui/button-group.tsx | 83 +++++ src/client/ui/button.tsx | 115 +++--- src/client/ui/card.tsx | 130 +++---- src/client/ui/collapsible.tsx | 32 +- src/client/ui/dropdown-menu.tsx | 332 +++++++++++------- src/client/ui/empty.tsx | 152 ++++---- src/client/ui/field.tsx | 246 +++++++++++++ src/client/ui/input-group.tsx | 168 +++++++++ src/client/ui/input.tsx | 39 +- src/client/ui/item.tsx | 193 ++++++++++ src/client/ui/label.tsx | 21 ++ src/client/ui/popover.tsx | 91 +++-- src/client/ui/scroll-area.tsx | 84 ++--- src/client/ui/separator.tsx | 32 +- src/client/ui/skeleton.tsx | 14 +- src/client/ui/spinner.tsx | 30 +- src/client/ui/textarea.tsx | 20 +- src/client/ui/toggle-group.tsx | 122 +++---- src/client/ui/toggle.tsx | 68 ++-- src/client/ui/tooltip.tsx | 66 ++-- tests/ui-shadows.spec.ts | 7 +- 30 files changed, 2107 insertions(+), 1097 deletions(-) create mode 100644 src/client/ui/button-group.tsx create mode 100644 src/client/ui/field.tsx create mode 100644 src/client/ui/input-group.tsx create mode 100644 src/client/ui/item.tsx create mode 100644 src/client/ui/label.tsx diff --git a/docs/plans/2026-09-14-tasks-graph-workflow-teams-design.md b/docs/plans/2026-09-14-tasks-graph-workflow-teams-design.md index 13e131b..33b103b 100644 --- a/docs/plans/2026-09-14-tasks-graph-workflow-teams-design.md +++ b/docs/plans/2026-09-14-tasks-graph-workflow-teams-design.md @@ -364,17 +364,36 @@ npx -y shadcn@latest add button card badge separator input textarea tooltip popo **4. 模型去重(重复 React key 的根因)。** 可视化 harness 在压力 fixture 上抓到 `sub-live-011` 等三个重复 key:根因在 `tasks-model.ts` —— workflow member 的 `childId` 若**不在 run 发起者的 catalog 里**(但存在于树中别处),代码会走"合成节点"分支,生成一个与真实节点**同 id** 的第二张卡片。修法:先按整棵树收集 `knownAgentIds`,只对 catalog 完全不知道的 id 合成节点;并在返回前加一条"每个 session id 只出现一次"的兜底去重。新增两条单测(已知 member 不重复 / 未知 member 仍合成)。 -### 升级路径(未来用 shadcn CLI `--diff` 跟进上游) +### 升级路径与本地适配清单(stock 优先) -`components.json` 已把 CLI 上下文钉在这个仓库上,`npx -y shadcn@latest info` 能正确列出 16 个已装组件(`base: radix`、`tailwindVersion: v4`、`tailwindCss: src/client/ui/theme.css`),所以**不要手动从 GitHub 抄文件**,走 CLI: +**政策(2026-09-20 起):与上游 stock 保持一致优先,本地改动只保留"不这样就得坏"的那几条。** 早先那版"裁剪变体、去阴影、压字号"的本地取向已被真机反馈否掉(用户要求"改为 shadcn/ui 最新的默认样式对齐"),因此 `src/client/ui/**` 用 `npx shadcn@latest add … --overwrite` 整批刷成了最新 registry(new-york v4 / radix base),并**重新施加**下面这份最小适配清单: -```bash -npx -y shadcn@latest info # 当前上下文与已装组件清单 -npx -y shadcn@latest add --dry-run # 先看会动哪些文件 -npx -y shadcn@latest add --diff # 逐文件看上游 vs 本地差异 -npx -y shadcn@latest add # 确认后再落盘 -``` +| # | 本地适配 | 为什么必须 | +|---|---|---| +| 1 | `cn` 从 `./utils` 导入;跨组件 import 写显式 `./x.tsx` | registry 写的是虚拟别名 `cn` 与裸 `src/client/ui/...` 说明符,本项目没有那些解析规则 | +| 2 | 图标一律用宿主 `@deepseek-ai/dsh-client-ui-primitives` 的 `IconXxx` | 皮肤契约禁 lucide;宿主图标随主题变色。`spinner.tsx` 的 `Loader2Icon` → `IconLoadingOutline16`(外层 span 承接 props),`dropdown-menu.tsx` 的勾/箭头 → `IconCheckOutline16`/`IconChevronRightOutline14`,单选圆点用 `span.bg-current` | +| 3 | 删除所有 `dark:` 变体 | 令牌本身随宿主深浅主题翻转,`dark:` 是多余且会改写宿主主题语义(技能规则也禁手写 dark 覆盖) | +| 4 | `text-white` → `text-destructive-foreground` | 无颜色字面量是皮肤契约的硬线 | +| 5 | `Button` 保留 `React.forwardRef` | 宿主是 **React 18**,`asChild` 触发器要 ref 才能量到锚点(registry 面向 React 19,不写 forwardRef) | +| 6 | 删除 `tw-animate-css` 的动画工具类(`animate-in`/`fade-in-*`/`zoom-in-*`/`slide-in-from-*`) | 该依赖未安装;装了它会向**我们不拥有的宿主页面**注入通用 `@keyframes` 名字,有与宿主动画撞名的风险。缺省即"立即出现",与 reduced-motion 行为一致 | -**实测**(`npx -y shadcn@latest add button --diff`,本次迁移后执行):上游会把 `cn` 的 import 写回裸包名 `"cn"`、带回 `dark:bg-destructive/60` 与 `dark:focus-visible:ring-destructive/40`、把 `text-destructive-foreground` 换回 `text-white`、给 `outline` 加回 `shadow-xs`、并恢复被删的 `link` 变体与 `xs` / `lg` / `icon-xs` / `icon-sm` / `icon-lg` 五个尺寸——正是本文件记录的六类统一改动。所以升级的流程是**逐文件判断**:没有本地改动的文件可被覆盖,有本地改动的(几乎全部)读 diff 后**只取上游的结构性更新,重新施加本仓库的六类改动**——尤其 `input.tsx` 的 `file:` 类一旦回来,`tests/ui-bundle.spec.ts` 的 preflight 守卫会开始误报。落盘后必须复跑三个守卫 spec(`ui-foundation` / `ui-bundle` / `ui-shadows`)与 `pnpm build`(体积测量是 `ui-bundle` 的一部分),再跑 `pnpm vitest run tests/theme.spec.ts` 确认皮肤契约未被上游带回的颜色字面量破坏。 +**已恢复为 stock(此前被本地裁掉的)**:Button 的完整变体/尺寸集(`link`/`xs`/`lg`/`icon-xs|sm|lg`)、`outline` 的 `shadow-xs`、`transition-all`、Badge 的 `ghost`/`link` 变体。它们只是运行时字符串,未使用的工具类不会进 CSS。 -`--overwrite` 与本仓库不兼容:它会一次性抹掉上面全部改动。真要用,先 `git stash` 出可对比的基线。 +**升级流程**:`npx shadcn@latest add --dry-run` → `--diff` 逐文件看上游改动 → 有本地改动的按上表重新施加(`--overwrite` 后必须重跑下面四条)。落盘后必跑:`pnpm typecheck`、`pnpm vitest run tests/theme.spec.ts tests/ui-shadows.spec.ts tests/ui-foundation.spec.ts tests/ui-bundle.spec.ts`、`pnpm build`(体积与产物断言),以及**镜像页视觉复核**(真实宿主样式表 + 真实组件,见上一节)。 + +### 页面重组:用最新 stock 组件,而不是手搓 div(2026-09-20) + +按 shadcn 技能的硬规则把任务页的组成方式整体换成最新 registry 的构件(4 个并行代理按文件分区改,主线程统一验收): + +- **团队任务板**:`Card` 全组合(`CardHeader`/`CardTitle` + `Separator` 分带 + `CardContent` + `CardFooter`),任务行 = `Item asChild`(`ItemMedia` StateDot / `ItemContent`/`ItemTitle`/`ItemActions`),筛选 = `ToggleGroup`(radix radiogroup + roving tabindex),行内菜单 = `DropdownMenuGroup` 包住每个 `DropdownMenuItem`,空态 = `Empty`。 +- **任务窗口**:`Card` 全组合 + `FieldGroup`/`Field`/`FieldLabel`/`FieldDescription`/`FieldError`;标题空值走 `Field data-invalid` + `Input aria-invalid`(不新增 i18n key);负责人 = `ToggleGroup`(**default 变体**:实测 `outline` 变体的 hover 与 pressed 同色,选中态不可辨);删除 armed 用 `Button variant="destructive"`,冲突提示用 `FieldError(role=alert)`。 +- **浮窗**:两张 portal 卡同样 `Card` 全组合(`CardTitle` 放身份、`CardDescription` 放种类、`CardAction` 放状态 Badge、`CardFooter` 放跳转),行 = `Item`(可点击行 `asChild` 真按钮)。 +- **抽屉/页头**:行 = `Item`(终止按钮在 `ItemActions`),分隔全用 `Separator`,加载 = `Spinner`,目录失败横幅 = `Item variant="outline"`,空态 = `Empty`。 +- **图/树**:节点卡配方 `rounded-lg border bg-card p-2.5 shadow-xs`(不再重复声明 stock `Card` 已有的墨色),控制条全部 stock `Button variant="outline" size="sm|icon"`,`z-[6]` → `z-10`;树行的条件类名改走 `cn()`。 + +**本轮在真实浏览器里抓到的两个"无 preflight"陷阱**(都已修,且在测试/文档里留痕): + +1. **stock `Empty` 只有 `border-dashed` 没有宽度** → 没有 preflight 时露出 UA 的 `3px medium` 虚线框,整页像套了个粗虚线相框;`Empty` 上加 `border-0` 修掉(截图证实)。 +2. **`` 类型合法但静默失效**:刷新后的 Spinner 变成 `React.ComponentProps<"span">`,`size` 是合法 HTML 属性 → typecheck 全绿而图标按默认 `size-4` 渲染。改用 `className="size-3"`。**教训:组件 API 变更中"仍然类型合法但语义丢失"的那类最危险,只有看渲染结果才能发现。** + +**已知未修(记录在案)**:抽屉行在 ~420px 以下的窄宽度里会横向溢出(Radix ScrollArea 的 table 视口按 min-content 计算 + 行内 `nowrap` 的 mono 标题),修复需要重新决定 ScrollArea/行的最小宽度策略;宽面板(用户实机)不受影响。 diff --git a/src/client/JobsDrawer.tsx b/src/client/JobsDrawer.tsx index 46f1aa6..f2ab857 100644 --- a/src/client/JobsDrawer.tsx +++ b/src/client/JobsDrawer.tsx @@ -11,17 +11,21 @@ * cursor), with a copy action, a follow-latest switch, the two-click kill, * and a terminal-style tail while the job runs. * - * Visual language: Tailwind utilities over the shadcn tokens - * (src/client/ui/theme.css) with the vendored Collapsible / ScrollArea / - * Button for the interactive shells. Hierarchy comes from a 1px `border-border` - * hairline plus the surface ladder (`bg-background` → `bg-muted` on hover) — + * Composition: the vendored shadcn set carries every surface. The drawer is a + * `Collapsible` whose rows are `Item`s (`ItemGroup` → `Item` + `ItemActions`, + * the row whose output is open taking the `muted` variant); the popover's + * identity block is an `Item` (media / content / actions) and its footer an + * `Item` holding the host `Switch` and the kill `Button`; hairlines are + * `Separator`s, status ink rides the stock `Badge` variants, and the loading + * line is the vendored `Spinner`. Hierarchy comes from the 1px `border-border` + * hairline plus the surface ladder (`bg-background` → `bg-muted` on hover) — * the static drawer carries no shadow; the only float is the popover card, * whose surface/shadow belong to AnchoredPopover. Body copy is text-sm, meta * text-xs, identifiers/durations `font-mono tabular-nums`. * - * Every non-shadcn control is a host primitive (Switch / StateDot), and every - * icon is a host `IconXxx` glyph — the badges are the vendored shadcn `Badge` - * on its stock variants. + * `className` is used for layout only (the components own their color and + * type), every icon is a host `IconXxx` glyph, and every other control is a + * host primitive (`Switch` / `StateDot`). */ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { @@ -40,7 +44,10 @@ import { t } from './locales.ts' import { Badge } from './ui/badge.tsx' import { Button } from './ui/button.tsx' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible.tsx' +import { Item, ItemActions, ItemContent, ItemGroup, ItemMedia, ItemTitle } from './ui/item.tsx' import { ScrollArea } from './ui/scroll-area.tsx' +import { Separator } from './ui/separator.tsx' +import { Spinner } from './ui/spinner.tsx' import { cn } from './ui/utils.ts' /** Refresh cadence of an open job-output popover while its job runs. */ @@ -60,6 +67,15 @@ function statusVariant(job: SidebarJobView): 'default' | 'secondary' | 'destruct return 'secondary' } +/** The meta chip of one job kind: the stock `outline` variant, mono figures. */ +function KindBadge(props: { kind: string }): ReactNode { + return ( + + {props.kind} + + ) +} + export interface JobsDrawerProps { rows: readonly TreeJob[] /** Agent count of the tree (root included) — the auto-collapse signal. */ @@ -129,8 +145,11 @@ export function JobsDrawer(props: JobsDrawerProps): ReactNode { onOpenChange={(next: boolean) => { setManualOpen(next) }} role="region" aria-label={t('jobs')} - className="z-[5] flex-none border-t border-border bg-background" + className="z-[5] flex-none bg-background" > + {/* The drawer's top edge: the stock hairline (the bar itself carries the + surface, the rows own their own rhythm). */} + {/* The bar keeps the drawer's whole toggle affordance (title, running tally, count line) and Radix owns aria-expanded / aria-controls. */} {t('jobsAutoCollapsed')} )} - {/* Bounded log surface: one row per job, hairline-separated by the + + {/* Bounded log surface: one `Item` row per job, separated by the rows' own 4px rhythm rather than per-row borders. The height is definite (not `max-h`) because ScrollArea's viewport is `size-full`: a bare max-height leaves the viewport content-sized, so the rows would spill out of the drawer instead of scrolling. */} - -
+ + {rows.map((row) => { const { job } = row const live = isJobLive(job) @@ -181,23 +201,25 @@ export function JobsDrawer(props: JobsDrawerProps): ReactNode { ].join(' · ') const secondary = [context, duration].filter(Boolean).join(' · ') return ( -
{job.status === 'running' && ( - + + + )} {killFailed && ( {t('jobKillError')} )} -
+ ) })} -
+
@@ -277,9 +297,9 @@ export function JobsDrawer(props: JobsDrawerProps): ReactNode { * * The card fills the AnchoredPopover surface (rounded-lg + border + shadow * live there, since that host owns the float) and only places its content - * inside it: header, title line, meta line, the terminal tail, and the action - * footer. `data-popover-handle` / `data-popover-no-drag` keep the drag - * contract unchanged — the body drags, the controls do not. + * inside it: header, the `Item` identity block, the terminal tail, and the + * `Item` action footer. `data-popover-handle` / `data-popover-no-drag` keep + * the drag contract unchanged — the body drags, the controls do not. */ export function JobOutputPopoverContent(props: { ownerSessionId: string @@ -361,13 +381,13 @@ export function JobOutputPopoverContent(props: { // `dsw-tasks`: this card renders inside AnchoredPopover's portal at // document.body — OUTSIDE the page root — so it re-declares the page root // class for the scoped base reset (see TaskWindow.tsx). -
-
- {t('jobs')} - +
+
+ {t('jobs')} +
-
- - - {job.label} - - - {jobStatusLabel(job.status, t)} - -
-
- - {job.kind} - - {job.detail !== undefined && job.detail !== '' && {job.detail}} - {t('jobDragHint')} -
- {state === 'loading' &&
{t('loading')}
} + {/* The job's identity: the stock `Item` composition (media / content / + actions) rather than a hand-rolled row. */} + + + + + + + {job.label} + + + + {job.detail !== undefined && job.detail !== '' && ( + {job.detail} + )} + {t('jobDragHint')} + + + + {jobStatusLabel(job.status, t)} + + + {state === 'loading' && ( +
+ {/* The visible line carries the copy; the glyph is decoration. */} + + {t('loading')} +
+ )} {state === 'error' &&
{t('jobOutputError')}
} {typeof state === 'object' && ( <> @@ -406,7 +440,7 @@ export function JobOutputPopoverContent(props: { ? (
                 {state.text}
@@ -418,8 +452,11 @@ export function JobOutputPopoverContent(props: {
           {state.truncated && 
{t('jobOutputTruncated')}
} )} -
+ {/* The action footer: the follow-latest switch and the kill confirm. */} + { if (armed) void kill() else setArmed(true) }} > - + {armed ? t('jobKillConfirm') : t('jobKill')} )} -
+ {killFailed &&
{t('jobKillError')}
}
) diff --git a/src/client/SubagentView.tsx b/src/client/SubagentView.tsx index c63dec5..8867a0d 100644 --- a/src/client/SubagentView.tsx +++ b/src/client/SubagentView.tsx @@ -21,11 +21,17 @@ * ⓘ button opens the detail popover. Completed leaf agents fold into one * aggregate node per parent (click it or the control-cluster toggle to * expand/collapse). + * + * The shell itself is the vendored shadcn set: the header is a toolbar closed + * by a `Separator`, the catalog-failure banner is an `Item` (media / content / + * actions), the empty state is the stock `Empty` composition, and the page's + * only controls are the vendored `Button` + `Tooltip` — `className` carries + * layout only, and every glyph is a host `IconXxx` primitive. */ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { useSyncExternalStore } from 'react' import { - IconAgentPresetOutline16, IconRefreshOutline14, + IconAgentPresetOutline16, IconRefreshOutline14, IconWarningOutline16, } from '@deepseek-ai/dsh-client-ui-primitives' import type { Context, @@ -54,6 +60,8 @@ import type { SidebarStore } from './state.ts' import type { WorkflowRunView } from '../workflow-runs.ts' import { Button as UiButton } from './ui/button.tsx' import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from './ui/empty.tsx' +import { Item, ItemActions, ItemContent, ItemMedia, ItemTitle } from './ui/item.tsx' +import { Separator } from './ui/separator.tsx' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip.tsx' import legacy from './SubagentView.module.css' @@ -416,11 +424,11 @@ export function SubagentView(props: { return (
{/* - Page header: title + mono descendant count + refresh, closed by a 1px - hairline. The control cluster (view toggle / fold / zoom) stays on the - canvas itself, so it is visible in BOTH modes. + Page header: title + mono descendant count + refresh, closed by the + stock hairline `Separator`. The control cluster (view toggle / fold / + zoom) stays on the canvas itself, so it is visible in BOTH modes. */} -
+
{t('subagent')} {rootSummary?.displayTitle !== undefined && rootSummary.displayTitle !== '' @@ -448,13 +456,14 @@ export function SubagentView(props: { team.refresh() }} > - + {t('refresh')}
+ {rootId !== undefined && teamView?.available === true && teamView.team !== null && ( )} {failedParents.length > 0 && ( -
- {t('catalogLoadFailed', { count: failedParents.length })} - { for (const parent of failedParents) refresh(parent) }} - > - - {t('retry')} - -
+ + + + + + + {t('catalogLoadFailed', { count: failedParents.length })} + + + + { for (const parent of failedParents) refresh(parent) }} + > + + {t('retry')} + + + )} {readyEmpty && ( - - - - + // `border-0` is the no-preflight fix, not a style override: the stock + // `Empty` declares only `border-dashed`, and with preflight never + // shipped (see ui/theme.css) a bare `border-style: dashed` keeps the + // UA's 3px medium width — a thick dashed frame around the page. + + + + - {t('subagentEmpty')} - {t('subagentEmptyDesc')} + {t('subagentEmpty')} + {t('subagentEmptyDesc')} )} diff --git a/src/client/TaskWindow.tsx b/src/client/TaskWindow.tsx index b35ab67..ce43526 100644 --- a/src/client/TaskWindow.tsx +++ b/src/client/TaskWindow.tsx @@ -10,13 +10,20 @@ * Actions: owner reassignment (CAS-immediate), edit/save, reopen or complete, * and a two-step delete. * - * Visual base: the vendored shadcn/ui set — `Card` shell, `Badge` status, - * `Input` / `Textarea` fields, a `ToggleGroup` owner picker and `Button` - * actions — over the token bridge in src/client/ui/theme.css, plus the shared - * `MarkdownText` renderer with the plugin's copy labels. The shell paints no - * surface of its own: the window is portaled to `document.body`, where - * `AnchoredPopover` owns the floating border/radius/shadow, so this card is - * layout and content only (the page's "static panels cast no shadow" rule). + * Visual base: the vendored shadcn/ui set, composed the way the registry + * intends — the full `Card` (header + title + action slot + content + footer), + * the task itself as an `Item` slab, the editable half as a `FieldGroup` of + * `Field` rows (`FieldLabel` + control + `FieldDescription`), the owner picker + * as a `ToggleGroup`, status as a stock `Badge` variant and every action a + * stock `Button` variant (the armed delete takes the `destructive` variant + * instead of a hand-painted red outline). Colours and type therefore come from + * the components and the semantic tokens; `className` stays layout. + * + * Three resets ride the card root on purpose: the window paints NO surface of + * its own. It is portaled to `document.body`, where `AnchoredPopover` owns the + * floating border/radius/shadow, so `border-0 bg-transparent shadow-none` let + * that shell's surface show through instead of nesting a second, shadowed card + * inside it (the page's "static panels cast no shadow" rule). * * That portal also sits OUTSIDE the task page root, so the card re-declares * the page root class (`dsw-tasks`) to pick up the scoped base reset the @@ -24,7 +31,7 @@ * inheritance; without preflight a bare `