@dsh-workbench/shared 0.1.8 → 0.1.10

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.
Files changed (61) hide show
  1. package/dist/api/blocker-table.js +10 -0
  2. package/dist/api/dsh-client.d.ts +6 -3
  3. package/dist/api/workbench-contract.d.ts +1 -1
  4. package/dist/components/ApprovalDetail.js +6 -2
  5. package/dist/components/CardBoundary.d.ts +21 -0
  6. package/dist/components/CardBoundary.js +34 -0
  7. package/dist/host/unit-libs.d.ts +1 -1
  8. package/dist/i18n/en.js +20 -1
  9. package/dist/i18n/index.d.ts +21 -2
  10. package/dist/i18n/merge.d.ts +9 -0
  11. package/dist/i18n/merge.js +22 -0
  12. package/dist/i18n/zh.d.ts +20 -1
  13. package/dist/i18n/zh.js +20 -1
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +1 -0
  16. package/dist/lib/react.d.ts +1 -0
  17. package/dist/lib/react.js +1 -0
  18. package/package.json +1 -1
  19. package/src/api/__tests__/blocker-table.test.ts +13 -0
  20. package/src/api/blocker-table.ts +11 -0
  21. package/src/api/dsh-client.ts +7 -1
  22. package/src/api/workbench-contract.ts +1 -1
  23. package/src/components/ApprovalDetail.tsx +9 -1
  24. package/src/components/CardBoundary.tsx +56 -0
  25. package/src/host/unit-libs.ts +1 -1
  26. package/src/i18n/en.ts +20 -1
  27. package/src/i18n/merge.test.ts +62 -1
  28. package/src/i18n/merge.ts +36 -0
  29. package/src/i18n/zh.ts +20 -1
  30. package/src/index.ts +1 -0
  31. package/src/lib/react.ts +2 -0
  32. package/dist/components/StateDot.d.ts +0 -5
  33. package/dist/components/StateDot.js +0 -10
  34. package/dist/host/__tests__/fakes.d.ts +0 -41
  35. package/dist/host/__tests__/fakes.js +0 -76
  36. package/dist/host/__tests__/preflight-fixtures.d.ts +0 -19
  37. package/dist/host/__tests__/preflight-fixtures.js +0 -47
  38. package/dist/host/context-inject.d.ts +0 -24
  39. package/dist/host/context-inject.js +0 -74
  40. package/dist/host/events.d.ts +0 -9
  41. package/dist/host/events.js +0 -70
  42. package/dist/host/mount-units.d.ts +0 -52
  43. package/dist/host/mount-units.js +0 -186
  44. package/dist/host/preflight.d.ts +0 -58
  45. package/dist/host/preflight.js +0 -233
  46. package/dist/host/probe-cache.d.ts +0 -6
  47. package/dist/host/probe-cache.js +0 -19
  48. package/dist/host/probe-libs.d.ts +0 -45
  49. package/dist/host/probe-libs.js +0 -61
  50. package/dist/host/probe-units.d.ts +0 -17
  51. package/dist/host/probe-units.js +0 -102
  52. package/dist/host/probe-verdict.d.ts +0 -7
  53. package/dist/host/probe-verdict.js +0 -143
  54. package/dist/host/tool-scope.d.ts +0 -32
  55. package/dist/host/tool-scope.js +0 -84
  56. package/dist/host/unit-readiness.d.ts +0 -7
  57. package/dist/host/unit-readiness.js +0 -5
  58. package/dist/host/unit-registry.d.ts +0 -9
  59. package/dist/host/unit-registry.js +0 -33
  60. package/dist/host/version-range.d.ts +0 -2
  61. package/dist/host/version-range.js +0 -73
@@ -99,6 +99,16 @@ export function blockerOfCapability(capability, t) {
99
99
  ...(sourceUnknown ? [{ kind: 'open-market', label: t('blocker.market.open'), unit: capability.unit }] : []),
100
100
  ...(capability.command === undefined || sourceUnknown ? [] : [copyAction(t, capability.command)]),
101
101
  ];
102
+ if (capability.status === 'failed') {
103
+ return {
104
+ id: 'unit-mount-failed',
105
+ severity: 'block',
106
+ title: t('env.unit.failed'),
107
+ hint: t('env.unit.failedHint'),
108
+ ...(capability.detail === undefined ? {} : { detail: capability.detail }),
109
+ actions: [retryAction(t)],
110
+ };
111
+ }
102
112
  if (capability.status === 'missing-plugin') {
103
113
  return {
104
114
  id: 'unit-missing-plugin',
@@ -99,12 +99,15 @@ export interface DshUiWorkspace {
99
99
  startSession(workspaceId: string): void;
100
100
  pickDirectory(): Promise<string | null>;
101
101
  }
102
+ export interface DshWorkspaceWritten {
103
+ readonly workspaceId?: string;
104
+ readonly title?: string;
105
+ }
102
106
  export interface DshWorkspaces {
103
107
  create(request: {
104
108
  path: string;
105
- }): Promise<{
106
- workspaceId?: string;
107
- } | undefined>;
109
+ }): Promise<DshWorkspaceWritten | undefined>;
110
+ rename(workspaceId: string, title: string): Promise<DshWorkspaceWritten>;
108
111
  }
109
112
  export interface DshReferenceInsert {
110
113
  readonly source: string;
@@ -10,7 +10,7 @@ export interface PluginStatePayload {
10
10
  }
11
11
  export interface CapabilityPayload {
12
12
  readonly unit: string;
13
- readonly status: 'ok' | 'missing-commands' | 'missing-plugin' | 'unchecked';
13
+ readonly status: 'ok' | 'failed' | 'missing-commands' | 'missing-plugin' | 'unchecked';
14
14
  readonly plugin?: string;
15
15
  readonly required: readonly string[];
16
16
  readonly missing: readonly string[];
@@ -1,5 +1,6 @@
1
1
  import { h } from '../lib/react.js';
2
2
  import { CodeBlock } from '@deepseek-ai/dsh-client-ui-primitives';
3
+ import { CardBoundary } from './CardBoundary.js';
3
4
  import './ApprovalDetail.css';
4
5
  function callOf(snapshot, callId) {
5
6
  if (callId === undefined)
@@ -29,8 +30,11 @@ export function ApprovalDetail(props) {
29
30
  const input = { tool: call?.name ?? '', args };
30
31
  const command = typeof args.command === 'string' ? args.command : '';
31
32
  const owner = (props.units ?? []).find((face) => face.approval?.tools.includes(input.tool) === true);
32
- if (owner?.approval !== undefined)
33
- return h(owner.approval.render, { input: input, t: t });
33
+ if (owner?.approval !== undefined) {
34
+ const Render = owner.approval.render;
35
+ return (h(CardBoundary, { unit: owner.manifest.id, resetKey: props.callId ?? '', t: t },
36
+ h(Render, { input: input, t: t })));
37
+ }
34
38
  if (command === '')
35
39
  return null;
36
40
  return (h(CodeBlock, { code: command, className: "wb-approvalCommand", copyLabel: t('blocker.copy'), copiedLabel: t('env.copied') }));
@@ -0,0 +1,21 @@
1
+ import type { Translate } from '../api/dsh-client.js';
2
+ import type { ReactNode } from '../lib/react.js';
3
+ import { Component } from '../lib/react.js';
4
+ export interface CardBoundaryProps {
5
+ readonly unit: string;
6
+ readonly resetKey: string;
7
+ readonly t: Translate;
8
+ readonly children?: ReactNode | undefined;
9
+ }
10
+ export interface CardBoundaryState {
11
+ readonly reason: string | null;
12
+ }
13
+ export declare function errorReasonOf(error: unknown): string;
14
+ export declare class CardBoundary extends Component<CardBoundaryProps, CardBoundaryState> {
15
+ state: CardBoundaryState;
16
+ static getDerivedStateFromError(error: unknown): CardBoundaryState;
17
+ componentDidCatch(error: unknown): void;
18
+ componentDidUpdate(previous: CardBoundaryProps): void;
19
+ private readonly retry;
20
+ render(): ReactNode;
21
+ }
@@ -0,0 +1,34 @@
1
+ import { Button } from '@deepseek-ai/dsh-client-ui-primitives';
2
+ import { Component, h } from '../lib/react.js';
3
+ import { Note } from './Note.js';
4
+ export function errorReasonOf(error) {
5
+ if (error instanceof Error)
6
+ return error.message;
7
+ return typeof error === 'string' ? error : JSON.stringify(error) ?? String(error);
8
+ }
9
+ export class CardBoundary extends Component {
10
+ state = { reason: null };
11
+ static getDerivedStateFromError(error) {
12
+ return { reason: errorReasonOf(error) };
13
+ }
14
+ componentDidCatch(error) {
15
+ console.error(`[dsh-workbench] 模块 ${this.props.unit} 渲染失败:${errorReasonOf(error)}`, error);
16
+ }
17
+ componentDidUpdate(previous) {
18
+ if (previous.resetKey !== this.props.resetKey && this.state.reason !== null) {
19
+ this.setState({ reason: null });
20
+ }
21
+ }
22
+ retry = () => {
23
+ this.setState({ reason: null });
24
+ };
25
+ render() {
26
+ const { reason } = this.state;
27
+ if (reason === null)
28
+ return this.props.children;
29
+ return (h(Note, { tone: "problem" },
30
+ h("span", null, this.props.t('panels.cardBroken', { unit: this.props.unit, reason })),
31
+ ' ',
32
+ h(Button, { type: "button", variant: "outline", size: "sm", onClick: this.retry }, this.props.t('panels.cardRetry'))));
33
+ }
34
+ }
@@ -23,7 +23,7 @@ export interface UnitPluginState {
23
23
  }
24
24
  export interface UnitCapability {
25
25
  readonly unit: string;
26
- readonly status: "ok" | "missing-commands" | "missing-plugin" | "unchecked";
26
+ readonly status: "ok" | "failed" | "missing-commands" | "missing-plugin" | "unchecked";
27
27
  readonly plugin?: string;
28
28
  readonly required: readonly string[];
29
29
  readonly missing: readonly string[];
package/dist/i18n/en.js CHANGED
@@ -7,6 +7,20 @@ export const en = {
7
7
  'project.none': 'No project',
8
8
  'project.empty': 'No projects yet. Use “Add project” below.',
9
9
  'project.add': 'Add project…',
10
+ 'project.search.label': 'Search workspaces',
11
+ 'project.search.placeholder': 'Search workspaces (folder / name / path)',
12
+ 'project.search.empty': 'No workspace matches “{query}”.',
13
+ 'project.rename.row': 'Rename “{name}”',
14
+ 'project.rename.current': 'Give this workspace a display name…',
15
+ 'project.rename.title': 'Give “{name}” a display name',
16
+ 'project.rename.description': 'Shown after the folder name. It renames the workspace itself.',
17
+ 'project.rename.field': 'Display name',
18
+ 'project.rename.placeholder': 'e.g. Workbench shell',
19
+ 'project.rename.hint': 'Empty = folder name only',
20
+ 'project.rename.save': 'Save',
21
+ 'project.rename.saving': 'Saving…',
22
+ 'project.rename.cancel': 'Cancel',
23
+ 'project.rename.failed': 'Rename failed. Try again.',
10
24
  'session.new': 'New session',
11
25
  'session.new.label': 'Start a session in the current project',
12
26
  'session.empty': 'No sessions in this project yet.',
@@ -35,6 +49,7 @@ export const en = {
35
49
  'panels.dragHint': 'Drag to reorder',
36
50
  'panels.unknownStep': 'No suite provides this view',
37
51
  'panels.labelWithHint': '{label} ({hint})',
52
+ 'panels.dep.failed': '⚠ failed to start',
38
53
  'panels.dep.missingPlugin': '⚠ not ready yet',
39
54
  'panels.dep.missingCommands': '⚠ needs an update',
40
55
  'panels.dep.unchecked': '⚠ cannot check',
@@ -79,7 +94,7 @@ export const en = {
79
94
  'panels.market.empty': 'No modules available',
80
95
  'panels.market.installedSection': 'Installed',
81
96
  'panels.market.moduleOutdated': 'update available',
82
- 'panels.market.moduleRestartHint': 'it may need a restart after installing',
97
+ 'panels.market.moduleRestartHint': 'installed but not active — restart dsh once to bring it up',
83
98
  'panels.market.restartNeeded': 'The new version is installed — restart once to pick it up.',
84
99
  'panels.market.foot': 'After installing, enable it in the panel page of that project.',
85
100
  'panels.market.recheck': 'Check for updates',
@@ -228,6 +243,8 @@ export const en = {
228
243
  'blocker.exec.hint': 'Make sure opencli is installed and on PATH (which opencli).',
229
244
  'blocker.unknown.title': 'Something went wrong',
230
245
  'panels.reading': 'Reading the panel setup…',
246
+ 'panels.cardBroken': 'This module will not render: {unit} ({reason})',
247
+ 'panels.cardRetry': 'Retry',
231
248
  'panels.empty': 'No modules on the panel yet',
232
249
  'panels.emptyHint': 'You can also just tell the agent: “add a module to the panel”.',
233
250
  'env.title': 'Preparing the workbench',
@@ -266,6 +283,8 @@ export const en = {
266
283
  'env.step.warm': 'Warming up site sessions',
267
284
  'env.warm.done': '{n} site(s) warmed up',
268
285
  'env.warm.short': 'Not warmed this time — harmless; the panel heals on first read',
286
+ 'env.unit.failed': 'This module cannot start',
287
+ 'env.unit.failedHint': 'It failed while assembling itself; other modules are unaffected.',
269
288
  'env.unit.missingPlugin': 'This module is not ready yet',
270
289
  'env.unit.missingCommandsHint': 'Updating it should fix this.',
271
290
  'env.unit.unchecked': 'Cannot check: no shell channel',
@@ -9,6 +9,20 @@ export declare const dictionaries: {
9
9
  readonly 'project.none': "未选择项目";
10
10
  readonly 'project.empty': "还没有项目。用下面的「添加项目」选一个目录。";
11
11
  readonly 'project.add': "添加项目…";
12
+ readonly 'project.search.label': "搜索工作区";
13
+ readonly 'project.search.placeholder': "搜索工作区(目录名 / 中文名 / 路径)";
14
+ readonly 'project.search.empty': "没有匹配「{query}」的工作区。";
15
+ readonly 'project.rename.row': "改「{name}」的中文名";
16
+ readonly 'project.rename.current': "改当前工作区的中文名…";
17
+ readonly 'project.rename.title': "给「{name}」配一个中文名";
18
+ readonly 'project.rename.description': "显示在目录名后面。改的是工作区本身的名字。";
19
+ readonly 'project.rename.field': "中文名";
20
+ readonly 'project.rename.placeholder': "例如:工作台外壳";
21
+ readonly 'project.rename.hint': "留空 = 只显示目录名";
22
+ readonly 'project.rename.save': "保存";
23
+ readonly 'project.rename.saving': "保存中…";
24
+ readonly 'project.rename.cancel': "取消";
25
+ readonly 'project.rename.failed': "改名失败了,再试一次。";
12
26
  readonly 'session.new': "新会话";
13
27
  readonly 'session.new.label': "在当前项目新建会话";
14
28
  readonly 'session.empty': "这个项目还没有会话。";
@@ -37,6 +51,7 @@ export declare const dictionaries: {
37
51
  readonly 'panels.dragHint': "拖动调整顺序";
38
52
  readonly 'panels.unknownStep': "这一层没有套件提供";
39
53
  readonly 'panels.labelWithHint': "{label}({hint})";
54
+ readonly 'panels.dep.failed': "⚠ 起不来";
40
55
  readonly 'panels.dep.missingPlugin': "⚠ 还没装好";
41
56
  readonly 'panels.dep.missingCommands': "⚠ 需要更新";
42
57
  readonly 'panels.dep.unchecked': "⚠ 检测不了";
@@ -81,7 +96,7 @@ export declare const dictionaries: {
81
96
  readonly 'panels.market.empty': "暂无可用模块";
82
97
  readonly 'panels.market.installedSection': "已装";
83
98
  readonly 'panels.market.moduleOutdated': "有新版";
84
- readonly 'panels.market.moduleRestartHint': "安装完成后可能需要重启才生效";
99
+ readonly 'panels.market.moduleRestartHint': "装好了但还没生效 —— 重启一次 dsh 就会出来";
85
100
  readonly 'panels.market.restartNeeded': "新版装好了,重启一次才生效。";
86
101
  readonly 'panels.market.foot': "装好后到那个项目的面板页勾选它,模块才会出现。";
87
102
  readonly 'panels.market.recheck': "重新检查更新";
@@ -230,6 +245,8 @@ export declare const dictionaries: {
230
245
  readonly 'blocker.exec.hint': "确认 opencli 已安装且在 PATH 里(which opencli)。";
231
246
  readonly 'blocker.unknown.title': "出了点问题";
232
247
  readonly 'panels.reading': "正在读取面板配置…";
248
+ readonly 'panels.cardBroken': "这个模块起不来:{unit}({reason})";
249
+ readonly 'panels.cardRetry': "重试";
233
250
  readonly 'panels.empty': "面板里还没有模块";
234
251
  readonly 'panels.emptyHint': "也可以直接跟 agent 说:「把某个模块加到面板」。";
235
252
  readonly 'env.title': "准备工作台";
@@ -268,6 +285,8 @@ export declare const dictionaries: {
268
285
  readonly 'env.step.warm': "预热站点登录态";
269
286
  readonly 'env.warm.done': "已预热 {n} 个站点";
270
287
  readonly 'env.warm.short': "这次没热上 —— 不影响用,面板第一次读取会自己补";
288
+ readonly 'env.unit.failed': "这个模块起不来";
289
+ readonly 'env.unit.failedHint': "它自己装配时报了错,别的模块不受影响。";
271
290
  readonly 'env.unit.missingPlugin': "这个模块还没装好";
272
291
  readonly 'env.unit.missingCommandsHint': "更新之后就能用了。";
273
292
  readonly 'env.unit.unchecked': "检测不了:拿不到执行通道";
@@ -290,7 +309,7 @@ export declare const dictionaries: {
290
309
  readonly 'panel.conversation': "对话";
291
310
  readonly 'panels.label': "全局面板";
292
311
  };
293
- en: Record<"time.now" | "time.minutes" | "time.hours" | "time.days" | "blocker.retry" | "blocker.copy" | "blocker.unknown.title" | "blocker.login.go" | "blocker.login.titleGeneric" | "blocker.login.title" | "blocker.login.manual" | "blocker.login.hint" | "blocker.forbidden.open" | "blocker.forbidden.titleGeneric" | "blocker.forbidden.title" | "blocker.forbidden.hint" | "blocker.timeout.hint" | "blocker.exec.hint" | "blocker.fix.install" | "blocker.fix.update" | "blocker.market.open" | "env.unit.missingPlugin" | "env.unit.hint" | "env.unit.missingCommands" | "env.unit.missingCommandsHint" | "env.unit.unchecked" | "env.unit.uncheckedHint" | "brand.name" | "toggle.open" | "toggle.collapse" | "project.label" | "workspace.section" | "project.none" | "project.empty" | "project.add" | "session.new" | "session.new.label" | "session.empty" | "session.noProject" | "session.pinned" | "session.actions" | "status.idle" | "status.running" | "status.waitingApproval" | "status.planReview" | "status.waitingAnswer" | "status.completed" | "status.subagentsRunning.one" | "status.subagentsRunning.other" | "panels.title" | "panels.reset" | "panels.enabled" | "panels.disabled" | "panels.choose" | "panels.failed" | "panels.hintEffect" | "panels.dragHint" | "panels.unknownStep" | "panels.labelWithHint" | "panels.dep.missingPlugin" | "panels.dep.missingCommands" | "panels.dep.unchecked" | "panels.tab.modules" | "panels.tab.market" | "panels.browseMarket" | "panels.noneInstalled" | "panels.noneInstalledHint" | "panels.market.installed" | "panels.market.outdated" | "panels.market.hasNew" | "panels.market.missing" | "panels.market.unchecked" | "panels.market.checking" | "panels.market.sectionInstalled" | "panels.market.sectionPending" | "panels.market.sectionShared" | "panels.market.sharedUsedBy" | "panels.market.listSeparator" | "panels.market.sharedMissing" | "panels.market.sharedOutdated" | "panels.market.sharedUnsatisfied" | "panels.market.sharedOk" | "panels.market.versionArrow" | "panels.market.lead" | "panels.market.install" | "panels.market.update" | "panels.market.uninstall" | "panels.market.uninstallLabel" | "panels.market.installing" | "panels.market.updating" | "panels.market.uninstalling" | "panels.market.confirmUninstallTitle" | "panels.market.confirmUninstallDetail" | "panels.market.confirmUninstallAck" | "panels.market.confirmYes" | "panels.market.confirmNo" | "panels.market.failed" | "panels.market.needEnv" | "panels.market.noSource" | "panels.market.badSource" | "panels.market.empty" | "panels.market.installedSection" | "panels.market.moduleOutdated" | "panels.market.moduleRestartHint" | "panels.market.restartNeeded" | "panels.market.foot" | "panels.market.recheck" | "panels.market.checkUpdates" | "panels.market.checkedAt" | "panels.market.justNow" | "panels.market.minutesAgo" | "panels.market.hoursAgo" | "panels.market.add.description" | "panels.market.add.specPlaceholder" | "panels.market.add.guideToggle" | "panels.market.add.guideHide" | "panels.market.add.guideIdTitle" | "panels.market.add.guideIdExample" | "panels.market.add.guideIdHint" | "panels.market.add.guideGitTitle" | "panels.market.add.guideGitExample" | "panels.market.add.guideGitHint" | "panels.market.add.guidePathTitle" | "panels.market.add.guidePathExample" | "panels.market.add.guidePathHint" | "panels.market.add.guideExampleLabel" | "panels.market.add.guideFill" | "panels.market.add.guideSafety" | "panels.market.add.registryLegend" | "panels.market.add.registryOfficial" | "panels.market.add.registryMirror" | "panels.market.add.registryWithHost" | "panels.market.add.registryToggle" | "panels.market.add.installVersion" | "panels.market.add.registryDefault" | "panels.market.add.registryCustom" | "panels.market.add.registryCustomPlaceholder" | "panels.market.add.registryCustomHint" | "panels.market.add.registryCustomInvalid" | "panels.market.add.run" | "panels.market.add.edit" | "panels.market.add.changeRegistry" | "panels.market.add.installingTitle" | "panels.market.add.installedTitle" | "panels.market.add.failedTitle" | "panels.market.add.cancelledTitle" | "panels.market.add.failureNetworkHost" | "panels.market.add.problemInvalid" | "panels.market.add.problemInstalled" | "panels.market.add.problemNotFound" | "panels.market.add.problemNotPackage" | "panels.market.add.problemNotBundle" | "panels.market.add.problemNetwork" | "panels.market.add.problemUnknown" | "panels.market.add.failureNetwork" | "panels.market.add.failureNotFound" | "panels.market.add.failureNoMatchingVersion" | "panels.market.add.failureDiskFull" | "panels.market.add.failurePermission" | "panels.market.add.failureBuildBlocked" | "panels.market.add.failureIntegrity" | "panels.market.add.failureTimeout" | "panels.market.add.failurePnpmMissing" | "panels.market.add.failureGeneric" | "panels.market.add.approveTitle" | "panels.market.add.approveCaution" | "panels.market.add.approveRun" | "panels.market.add.restart" | "panels.market.add.tookEffect" | "panels.market.add.details" | "panels.market.add.open" | "panels.market.add.title" | "panels.market.add.intro" | "panels.market.add.specLabel" | "panels.market.add.specHint" | "panels.market.add.exampleTitle" | "panels.market.add.exampleFill" | "panels.market.add.registryLabel" | "panels.market.add.registryProfile" | "panels.market.add.registryPnpm" | "panels.market.add.check" | "panels.market.add.checking" | "panels.market.add.change" | "panels.market.add.previewTitle" | "panels.market.add.fieldName" | "panels.market.add.fieldVersion" | "panels.market.add.fieldDescription" | "panels.market.add.fieldSource" | "panels.market.add.install" | "panels.market.add.installing" | "panels.market.add.cancel" | "panels.market.add.close" | "panels.market.add.retry" | "panels.market.add.changeSource" | "panels.market.add.approveBuilds" | "panels.market.add.approveHint" | "panels.market.add.doneApplied" | "panels.market.add.doneRestart" | "panels.market.add.cancelled" | "panels.market.add.failed" | "panels.market.add.removed" | "panels.market.add.supplyPending" | "panels.market.add.supplyFailed" | "panels.market.add.notModule" | "panels.market.add.removedRestart" | "panels.market.add.remove" | "panels.market.add.problem.invalidSpec" | "panels.market.add.problem.alreadyInstalled" | "panels.market.add.problem.notFound" | "panels.market.add.problem.notAPackage" | "panels.market.add.problem.notABundle" | "panels.market.add.problem.network" | "panels.market.add.problem.unknown" | "panels.market.add.reason.invalidSpec" | "panels.market.add.reason.incompatible" | "panels.market.add.reason.notBundle" | "panels.market.add.reason.notRemovable" | "panels.market.add.reason.bundleInUse" | "panels.market.add.reason.stopProfile" | "panels.market.add.reason.staleApproval" | "panels.market.add.reason.operationError" | "panels.market.add.reason.buildBlocked" | "panels.market.add.reason.pnpmMissing" | "panels.market.add.reason.noMatchingVersion" | "panels.market.add.reason.diskFull" | "panels.market.add.reason.permission" | "panels.market.add.reason.integrity" | "panels.market.add.reason.timeout" | "panels.market.add.reason.network" | "panels.market.add.reason.notFound" | "panels.market.add.reason.unknown" | "blocker.fix.running" | "blocker.fix.failed" | "blocker.retry.running" | "panels.reading" | "panels.empty" | "panels.emptyHint" | "env.title" | "env.checking" | "env.installing" | "env.failed" | "env.elapsed" | "env.stepFailed.install" | "env.stepFailed.path" | "env.stepFailed.shell" | "env.bridge.ok" | "env.bridge.short" | "env.bridge.why" | "env.bridge.daemon" | "env.bridge.extension" | "env.bridge.extensionLink" | "env.bridge.profile" | "env.bridge.stale" | "env.bridge.unknown" | "env.fix.title" | "env.fix.why" | "env.fix.whyPath" | "env.fix.whyShell" | "env.fix.step" | "env.fix.stepPath" | "env.fix.stepShell" | "env.fix.permission" | "env.fix.network" | "env.fix.details" | "env.retry" | "env.copyCommand" | "env.copied" | "env.step.opencli" | "env.step.codingPlugin" | "env.step.bridge" | "env.step.warm" | "env.warm.done" | "env.warm.short" | "rightbar.label" | "rightbar.expand" | "rightbar.collapse" | "rightbar.column.label" | "rightbar.column.workbench" | "rightbar.column.dsh" | "rightbar.refresh" | "rightbar.refreshing" | "rightbar.updatedAt" | "rightbar.noProject" | "coding.notARepo.title" | "error.unknown" | "board.title" | "panel.conversation" | "panels.label", string>;
312
+ en: Record<"time.now" | "time.minutes" | "time.hours" | "time.days" | "blocker.retry" | "blocker.copy" | "blocker.unknown.title" | "blocker.login.go" | "blocker.login.titleGeneric" | "blocker.login.title" | "blocker.login.manual" | "blocker.login.hint" | "blocker.forbidden.open" | "blocker.forbidden.titleGeneric" | "blocker.forbidden.title" | "blocker.forbidden.hint" | "blocker.timeout.hint" | "blocker.exec.hint" | "blocker.fix.install" | "blocker.fix.update" | "blocker.market.open" | "env.unit.failed" | "env.unit.failedHint" | "env.unit.missingPlugin" | "env.unit.hint" | "env.unit.missingCommands" | "env.unit.missingCommandsHint" | "env.unit.unchecked" | "env.unit.uncheckedHint" | "brand.name" | "toggle.open" | "toggle.collapse" | "project.label" | "workspace.section" | "project.none" | "project.empty" | "project.add" | "project.search.label" | "project.search.placeholder" | "project.search.empty" | "project.rename.row" | "project.rename.current" | "project.rename.title" | "project.rename.description" | "project.rename.field" | "project.rename.placeholder" | "project.rename.hint" | "project.rename.save" | "project.rename.saving" | "project.rename.cancel" | "project.rename.failed" | "session.new" | "session.new.label" | "session.empty" | "session.noProject" | "session.pinned" | "session.actions" | "status.idle" | "status.running" | "status.waitingApproval" | "status.planReview" | "status.waitingAnswer" | "status.completed" | "status.subagentsRunning.one" | "status.subagentsRunning.other" | "panels.title" | "panels.reset" | "panels.enabled" | "panels.disabled" | "panels.choose" | "panels.failed" | "panels.hintEffect" | "panels.dragHint" | "panels.unknownStep" | "panels.labelWithHint" | "panels.dep.failed" | "panels.dep.missingPlugin" | "panels.dep.missingCommands" | "panels.dep.unchecked" | "panels.tab.modules" | "panels.tab.market" | "panels.browseMarket" | "panels.noneInstalled" | "panels.noneInstalledHint" | "panels.market.installed" | "panels.market.outdated" | "panels.market.hasNew" | "panels.market.missing" | "panels.market.unchecked" | "panels.market.checking" | "panels.market.sectionInstalled" | "panels.market.sectionPending" | "panels.market.sectionShared" | "panels.market.sharedUsedBy" | "panels.market.listSeparator" | "panels.market.sharedMissing" | "panels.market.sharedOutdated" | "panels.market.sharedUnsatisfied" | "panels.market.sharedOk" | "panels.market.versionArrow" | "panels.market.lead" | "panels.market.install" | "panels.market.update" | "panels.market.uninstall" | "panels.market.uninstallLabel" | "panels.market.installing" | "panels.market.updating" | "panels.market.uninstalling" | "panels.market.confirmUninstallTitle" | "panels.market.confirmUninstallDetail" | "panels.market.confirmUninstallAck" | "panels.market.confirmYes" | "panels.market.confirmNo" | "panels.market.failed" | "panels.market.needEnv" | "panels.market.noSource" | "panels.market.badSource" | "panels.market.empty" | "panels.market.installedSection" | "panels.market.moduleOutdated" | "panels.market.moduleRestartHint" | "panels.market.restartNeeded" | "panels.market.foot" | "panels.market.recheck" | "panels.market.checkUpdates" | "panels.market.checkedAt" | "panels.market.justNow" | "panels.market.minutesAgo" | "panels.market.hoursAgo" | "panels.market.add.description" | "panels.market.add.specPlaceholder" | "panels.market.add.guideToggle" | "panels.market.add.guideHide" | "panels.market.add.guideIdTitle" | "panels.market.add.guideIdExample" | "panels.market.add.guideIdHint" | "panels.market.add.guideGitTitle" | "panels.market.add.guideGitExample" | "panels.market.add.guideGitHint" | "panels.market.add.guidePathTitle" | "panels.market.add.guidePathExample" | "panels.market.add.guidePathHint" | "panels.market.add.guideExampleLabel" | "panels.market.add.guideFill" | "panels.market.add.guideSafety" | "panels.market.add.registryLegend" | "panels.market.add.registryOfficial" | "panels.market.add.registryMirror" | "panels.market.add.registryWithHost" | "panels.market.add.registryToggle" | "panels.market.add.installVersion" | "panels.market.add.registryDefault" | "panels.market.add.registryCustom" | "panels.market.add.registryCustomPlaceholder" | "panels.market.add.registryCustomHint" | "panels.market.add.registryCustomInvalid" | "panels.market.add.run" | "panels.market.add.edit" | "panels.market.add.changeRegistry" | "panels.market.add.installingTitle" | "panels.market.add.installedTitle" | "panels.market.add.failedTitle" | "panels.market.add.cancelledTitle" | "panels.market.add.failureNetworkHost" | "panels.market.add.problemInvalid" | "panels.market.add.problemInstalled" | "panels.market.add.problemNotFound" | "panels.market.add.problemNotPackage" | "panels.market.add.problemNotBundle" | "panels.market.add.problemNetwork" | "panels.market.add.problemUnknown" | "panels.market.add.failureNetwork" | "panels.market.add.failureNotFound" | "panels.market.add.failureNoMatchingVersion" | "panels.market.add.failureDiskFull" | "panels.market.add.failurePermission" | "panels.market.add.failureBuildBlocked" | "panels.market.add.failureIntegrity" | "panels.market.add.failureTimeout" | "panels.market.add.failurePnpmMissing" | "panels.market.add.failureGeneric" | "panels.market.add.approveTitle" | "panels.market.add.approveCaution" | "panels.market.add.approveRun" | "panels.market.add.restart" | "panels.market.add.tookEffect" | "panels.market.add.details" | "panels.market.add.open" | "panels.market.add.title" | "panels.market.add.intro" | "panels.market.add.specLabel" | "panels.market.add.specHint" | "panels.market.add.exampleTitle" | "panels.market.add.exampleFill" | "panels.market.add.registryLabel" | "panels.market.add.registryProfile" | "panels.market.add.registryPnpm" | "panels.market.add.check" | "panels.market.add.checking" | "panels.market.add.change" | "panels.market.add.previewTitle" | "panels.market.add.fieldName" | "panels.market.add.fieldVersion" | "panels.market.add.fieldDescription" | "panels.market.add.fieldSource" | "panels.market.add.install" | "panels.market.add.installing" | "panels.market.add.cancel" | "panels.market.add.close" | "panels.market.add.retry" | "panels.market.add.changeSource" | "panels.market.add.approveBuilds" | "panels.market.add.approveHint" | "panels.market.add.doneApplied" | "panels.market.add.doneRestart" | "panels.market.add.cancelled" | "panels.market.add.failed" | "panels.market.add.removed" | "panels.market.add.supplyPending" | "panels.market.add.supplyFailed" | "panels.market.add.notModule" | "panels.market.add.removedRestart" | "panels.market.add.remove" | "panels.market.add.problem.invalidSpec" | "panels.market.add.problem.alreadyInstalled" | "panels.market.add.problem.notFound" | "panels.market.add.problem.notAPackage" | "panels.market.add.problem.notABundle" | "panels.market.add.problem.network" | "panels.market.add.problem.unknown" | "panels.market.add.reason.invalidSpec" | "panels.market.add.reason.incompatible" | "panels.market.add.reason.notBundle" | "panels.market.add.reason.notRemovable" | "panels.market.add.reason.bundleInUse" | "panels.market.add.reason.stopProfile" | "panels.market.add.reason.staleApproval" | "panels.market.add.reason.operationError" | "panels.market.add.reason.buildBlocked" | "panels.market.add.reason.pnpmMissing" | "panels.market.add.reason.noMatchingVersion" | "panels.market.add.reason.diskFull" | "panels.market.add.reason.permission" | "panels.market.add.reason.integrity" | "panels.market.add.reason.timeout" | "panels.market.add.reason.network" | "panels.market.add.reason.notFound" | "panels.market.add.reason.unknown" | "blocker.fix.running" | "blocker.fix.failed" | "blocker.retry.running" | "panels.reading" | "panels.cardBroken" | "panels.cardRetry" | "panels.empty" | "panels.emptyHint" | "env.title" | "env.checking" | "env.installing" | "env.failed" | "env.elapsed" | "env.stepFailed.install" | "env.stepFailed.path" | "env.stepFailed.shell" | "env.bridge.ok" | "env.bridge.short" | "env.bridge.why" | "env.bridge.daemon" | "env.bridge.extension" | "env.bridge.extensionLink" | "env.bridge.profile" | "env.bridge.stale" | "env.bridge.unknown" | "env.fix.title" | "env.fix.why" | "env.fix.whyPath" | "env.fix.whyShell" | "env.fix.step" | "env.fix.stepPath" | "env.fix.stepShell" | "env.fix.permission" | "env.fix.network" | "env.fix.details" | "env.retry" | "env.copyCommand" | "env.copied" | "env.step.opencli" | "env.step.codingPlugin" | "env.step.bridge" | "env.step.warm" | "env.warm.done" | "env.warm.short" | "rightbar.label" | "rightbar.expand" | "rightbar.collapse" | "rightbar.column.label" | "rightbar.column.workbench" | "rightbar.column.dsh" | "rightbar.refresh" | "rightbar.refreshing" | "rightbar.updatedAt" | "rightbar.noProject" | "coding.notARepo.title" | "error.unknown" | "board.title" | "panel.conversation" | "panels.label", string>;
294
313
  };
295
314
  export type { WorkbenchMessages };
296
315
  export interface Dictionaries {
@@ -4,3 +4,12 @@ export interface MessagesContribution {
4
4
  readonly messages?: Dictionaries | undefined;
5
5
  }
6
6
  export declare function mergeMessages(base: Dictionaries, contributions: readonly MessagesContribution[]): Dictionaries;
7
+ export interface MessagesProblem {
8
+ readonly unit: string;
9
+ readonly reason: string;
10
+ }
11
+ export interface MessagesMerge {
12
+ readonly dictionaries: Dictionaries;
13
+ readonly problems: readonly MessagesProblem[];
14
+ }
15
+ export declare function mergeMessagesSkipping(base: Dictionaries, contributions: readonly MessagesContribution[]): MessagesMerge;
@@ -38,3 +38,25 @@ export function mergeMessages(base, contributions) {
38
38
  }
39
39
  return { zh, en };
40
40
  }
41
+ function reasonOf(error) {
42
+ if (error instanceof Error)
43
+ return error.message;
44
+ return typeof error === 'string' ? error : String(error);
45
+ }
46
+ export function mergeMessagesSkipping(base, contributions) {
47
+ let dictionaries = base;
48
+ const accepted = [];
49
+ const problems = [];
50
+ for (const item of contributions) {
51
+ if (item.messages === undefined)
52
+ continue;
53
+ try {
54
+ dictionaries = mergeMessages(base, [...accepted, item]);
55
+ accepted.push(item);
56
+ }
57
+ catch (error) {
58
+ problems.push({ unit: item.unit, reason: reasonOf(error) });
59
+ }
60
+ }
61
+ return { dictionaries, problems };
62
+ }
package/dist/i18n/zh.d.ts CHANGED
@@ -7,6 +7,20 @@ export declare const zh: {
7
7
  readonly 'project.none': "未选择项目";
8
8
  readonly 'project.empty': "还没有项目。用下面的「添加项目」选一个目录。";
9
9
  readonly 'project.add': "添加项目…";
10
+ readonly 'project.search.label': "搜索工作区";
11
+ readonly 'project.search.placeholder': "搜索工作区(目录名 / 中文名 / 路径)";
12
+ readonly 'project.search.empty': "没有匹配「{query}」的工作区。";
13
+ readonly 'project.rename.row': "改「{name}」的中文名";
14
+ readonly 'project.rename.current': "改当前工作区的中文名…";
15
+ readonly 'project.rename.title': "给「{name}」配一个中文名";
16
+ readonly 'project.rename.description': "显示在目录名后面。改的是工作区本身的名字。";
17
+ readonly 'project.rename.field': "中文名";
18
+ readonly 'project.rename.placeholder': "例如:工作台外壳";
19
+ readonly 'project.rename.hint': "留空 = 只显示目录名";
20
+ readonly 'project.rename.save': "保存";
21
+ readonly 'project.rename.saving': "保存中…";
22
+ readonly 'project.rename.cancel': "取消";
23
+ readonly 'project.rename.failed': "改名失败了,再试一次。";
10
24
  readonly 'session.new': "新会话";
11
25
  readonly 'session.new.label': "在当前项目新建会话";
12
26
  readonly 'session.empty': "这个项目还没有会话。";
@@ -35,6 +49,7 @@ export declare const zh: {
35
49
  readonly 'panels.dragHint': "拖动调整顺序";
36
50
  readonly 'panels.unknownStep': "这一层没有套件提供";
37
51
  readonly 'panels.labelWithHint': "{label}({hint})";
52
+ readonly 'panels.dep.failed': "⚠ 起不来";
38
53
  readonly 'panels.dep.missingPlugin': "⚠ 还没装好";
39
54
  readonly 'panels.dep.missingCommands': "⚠ 需要更新";
40
55
  readonly 'panels.dep.unchecked': "⚠ 检测不了";
@@ -79,7 +94,7 @@ export declare const zh: {
79
94
  readonly 'panels.market.empty': "暂无可用模块";
80
95
  readonly 'panels.market.installedSection': "已装";
81
96
  readonly 'panels.market.moduleOutdated': "有新版";
82
- readonly 'panels.market.moduleRestartHint': "安装完成后可能需要重启才生效";
97
+ readonly 'panels.market.moduleRestartHint': "装好了但还没生效 —— 重启一次 dsh 就会出来";
83
98
  readonly 'panels.market.restartNeeded': "新版装好了,重启一次才生效。";
84
99
  readonly 'panels.market.foot': "装好后到那个项目的面板页勾选它,模块才会出现。";
85
100
  readonly 'panels.market.recheck': "重新检查更新";
@@ -228,6 +243,8 @@ export declare const zh: {
228
243
  readonly 'blocker.exec.hint': "确认 opencli 已安装且在 PATH 里(which opencli)。";
229
244
  readonly 'blocker.unknown.title': "出了点问题";
230
245
  readonly 'panels.reading': "正在读取面板配置…";
246
+ readonly 'panels.cardBroken': "这个模块起不来:{unit}({reason})";
247
+ readonly 'panels.cardRetry': "重试";
231
248
  readonly 'panels.empty': "面板里还没有模块";
232
249
  readonly 'panels.emptyHint': "也可以直接跟 agent 说:「把某个模块加到面板」。";
233
250
  readonly 'env.title': "准备工作台";
@@ -266,6 +283,8 @@ export declare const zh: {
266
283
  readonly 'env.step.warm': "预热站点登录态";
267
284
  readonly 'env.warm.done': "已预热 {n} 个站点";
268
285
  readonly 'env.warm.short': "这次没热上 —— 不影响用,面板第一次读取会自己补";
286
+ readonly 'env.unit.failed': "这个模块起不来";
287
+ readonly 'env.unit.failedHint': "它自己装配时报了错,别的模块不受影响。";
269
288
  readonly 'env.unit.missingPlugin': "这个模块还没装好";
270
289
  readonly 'env.unit.missingCommandsHint': "更新之后就能用了。";
271
290
  readonly 'env.unit.unchecked': "检测不了:拿不到执行通道";
package/dist/i18n/zh.js CHANGED
@@ -7,6 +7,20 @@ export const zh = {
7
7
  'project.none': '未选择项目',
8
8
  'project.empty': '还没有项目。用下面的「添加项目」选一个目录。',
9
9
  'project.add': '添加项目…',
10
+ 'project.search.label': '搜索工作区',
11
+ 'project.search.placeholder': '搜索工作区(目录名 / 中文名 / 路径)',
12
+ 'project.search.empty': '没有匹配「{query}」的工作区。',
13
+ 'project.rename.row': '改「{name}」的中文名',
14
+ 'project.rename.current': '改当前工作区的中文名…',
15
+ 'project.rename.title': '给「{name}」配一个中文名',
16
+ 'project.rename.description': '显示在目录名后面。改的是工作区本身的名字。',
17
+ 'project.rename.field': '中文名',
18
+ 'project.rename.placeholder': '例如:工作台外壳',
19
+ 'project.rename.hint': '留空 = 只显示目录名',
20
+ 'project.rename.save': '保存',
21
+ 'project.rename.saving': '保存中…',
22
+ 'project.rename.cancel': '取消',
23
+ 'project.rename.failed': '改名失败了,再试一次。',
10
24
  'session.new': '新会话',
11
25
  'session.new.label': '在当前项目新建会话',
12
26
  'session.empty': '这个项目还没有会话。',
@@ -35,6 +49,7 @@ export const zh = {
35
49
  'panels.dragHint': '拖动调整顺序',
36
50
  'panels.unknownStep': '这一层没有套件提供',
37
51
  'panels.labelWithHint': '{label}({hint})',
52
+ 'panels.dep.failed': '⚠ 起不来',
38
53
  'panels.dep.missingPlugin': '⚠ 还没装好',
39
54
  'panels.dep.missingCommands': '⚠ 需要更新',
40
55
  'panels.dep.unchecked': '⚠ 检测不了',
@@ -79,7 +94,7 @@ export const zh = {
79
94
  'panels.market.empty': '暂无可用模块',
80
95
  'panels.market.installedSection': '已装',
81
96
  'panels.market.moduleOutdated': '有新版',
82
- 'panels.market.moduleRestartHint': '安装完成后可能需要重启才生效',
97
+ 'panels.market.moduleRestartHint': '装好了但还没生效 —— 重启一次 dsh 就会出来',
83
98
  'panels.market.restartNeeded': '新版装好了,重启一次才生效。',
84
99
  'panels.market.foot': '装好后到那个项目的面板页勾选它,模块才会出现。',
85
100
  'panels.market.recheck': '重新检查更新',
@@ -228,6 +243,8 @@ export const zh = {
228
243
  'blocker.exec.hint': '确认 opencli 已安装且在 PATH 里(which opencli)。',
229
244
  'blocker.unknown.title': '出了点问题',
230
245
  'panels.reading': '正在读取面板配置…',
246
+ 'panels.cardBroken': '这个模块起不来:{unit}({reason})',
247
+ 'panels.cardRetry': '重试',
231
248
  'panels.empty': '面板里还没有模块',
232
249
  'panels.emptyHint': '也可以直接跟 agent 说:「把某个模块加到面板」。',
233
250
  'env.title': '准备工作台',
@@ -266,6 +283,8 @@ export const zh = {
266
283
  'env.step.warm': '预热站点登录态',
267
284
  'env.warm.done': '已预热 {n} 个站点',
268
285
  'env.warm.short': '这次没热上 —— 不影响用,面板第一次读取会自己补',
286
+ 'env.unit.failed': '这个模块起不来',
287
+ 'env.unit.failedHint': '它自己装配时报了错,别的模块不受影响。',
269
288
  'env.unit.missingPlugin': '这个模块还没装好',
270
289
  'env.unit.missingCommandsHint': '更新之后就能用了。',
271
290
  'env.unit.unchecked': '检测不了:拿不到执行通道',
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export * from './components/Activity.js';
14
14
  export * from './components/ApprovalDetail.js';
15
15
  export * from './components/ApprovalPlan.js';
16
16
  export * from './components/Card.js';
17
+ export * from './components/CardBoundary.js';
17
18
  export * from './components/CardHead.js';
18
19
  export * from './components/ChoiceGroup.js';
19
20
  export * from './components/LongTaskRow.js';
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ export * from './components/Activity.js';
14
14
  export * from './components/ApprovalDetail.js';
15
15
  export * from './components/ApprovalPlan.js';
16
16
  export * from './components/Card.js';
17
+ export * from './components/CardBoundary.js';
17
18
  export * from './components/CardHead.js';
18
19
  export * from './components/ChoiceGroup.js';
19
20
  export * from './components/LongTaskRow.js';
@@ -1,6 +1,7 @@
1
1
  import type * as ReactTypes from 'react';
2
2
  export declare const h: typeof ReactTypes.createElement;
3
3
  export declare const Fragment: ReactTypes.ExoticComponent<ReactTypes.FragmentProps>;
4
+ export declare const Component: typeof ReactTypes.Component;
4
5
  export declare const useState: typeof ReactTypes.useState;
5
6
  export declare const useEffect: typeof ReactTypes.useEffect;
6
7
  export declare const useLayoutEffect: typeof ReactTypes.useLayoutEffect;
package/dist/lib/react.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const react = require('react');
2
2
  export const h = react.createElement;
3
3
  export const Fragment = react.Fragment;
4
+ export const Component = react.Component;
4
5
  export const useState = react.useState;
5
6
  export const useEffect = react.useEffect;
6
7
  export const useLayoutEffect = react.useLayoutEffect;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/shared",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "工作台与模块共享的纯代码与契约类型:类型、纯逻辑、哑组件、平台解析。宿主侧的东西从 /host 进(不影响浏览器产物)。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -141,6 +141,19 @@ describe('blockerOfCapability', () => {
141
141
  expect(blockerOfCapability(capability({ status: 'ok', plugin: 'coding' }), t)).toBeUndefined();
142
142
  });
143
143
 
144
+ it('★ 起不来(模块自己装配炸了)→ 只说"起不来" + 把宿主给的原因原样附上,不给装/卸的按钮', () => {
145
+ const blocker = blockerOfCapability(
146
+ capability({ status: 'failed', detail: '模块自己的 bug:kit.components.StateBlock 取不到' }),
147
+ t,
148
+ );
149
+
150
+ expect(blocker?.id).toBe('unit-mount-failed');
151
+ expect(blocker?.title).toBe('env.unit.failed');
152
+ expect(blocker?.detail).toBe('模块自己的 bug:kit.components.StateBlock 取不到');
153
+ expect(blocker?.hint).toBe('env.unit.failedHint');
154
+ expect(blocker?.actions.map((action) => action.kind)).toEqual(['retry']);
155
+ });
156
+
144
157
  it('★ 没装好 → 标题只说"这个模块还没装好",**不点名插件**(用户不认识插件)', () => {
145
158
  const blocker = blockerOfCapability(
146
159
  capability({
@@ -122,6 +122,17 @@ export function blockerOfCapability(capability: CapabilityPayload, t: Translate)
122
122
  ...(capability.command === undefined || sourceUnknown ? [] : [copyAction(t, capability.command)]),
123
123
  ];
124
124
 
125
+ if (capability.status === 'failed') {
126
+ return {
127
+ id: 'unit-mount-failed',
128
+ severity: 'block',
129
+ title: t('env.unit.failed'),
130
+ hint: t('env.unit.failedHint'),
131
+ ...(capability.detail === undefined ? {} : { detail: capability.detail }),
132
+ actions: [retryAction(t)],
133
+ };
134
+ }
135
+
125
136
  if (capability.status === 'missing-plugin') {
126
137
  return {
127
138
  id: 'unit-missing-plugin',
@@ -113,8 +113,14 @@ export interface DshUiWorkspace {
113
113
  pickDirectory(): Promise<string | null>;
114
114
  }
115
115
 
116
+ export interface DshWorkspaceWritten {
117
+ readonly workspaceId?: string;
118
+ readonly title?: string;
119
+ }
120
+
116
121
  export interface DshWorkspaces {
117
- create(request: { path: string }): Promise<{ workspaceId?: string } | undefined>;
122
+ create(request: { path: string }): Promise<DshWorkspaceWritten | undefined>;
123
+ rename(workspaceId: string, title: string): Promise<DshWorkspaceWritten>;
118
124
  }
119
125
 
120
126
  export interface DshReferenceInsert {
@@ -21,7 +21,7 @@ export interface PluginStatePayload {
21
21
 
22
22
  export interface CapabilityPayload {
23
23
  readonly unit: string;
24
- readonly status: 'ok' | 'missing-commands' | 'missing-plugin' | 'unchecked';
24
+ readonly status: 'ok' | 'failed' | 'missing-commands' | 'missing-plugin' | 'unchecked';
25
25
  readonly plugin?: string;
26
26
  readonly required: readonly string[];
27
27
  readonly missing: readonly string[];
@@ -2,6 +2,7 @@ import type { Translate } from '../api/dsh-client.js';
2
2
  import type { ApprovalInput, UnitClientFace } from '../components/board-module.js';
3
3
  import { h } from '../lib/react.js';
4
4
  import { CodeBlock } from '@deepseek-ai/dsh-client-ui-primitives';
5
+ import { CardBoundary } from './CardBoundary.js';
5
6
  import './ApprovalDetail.css';
6
7
 
7
8
  interface ToolCallRoot {
@@ -53,7 +54,14 @@ export function ApprovalDetail(props: ApprovalDetailProps) {
53
54
  const command = typeof args.command === 'string' ? args.command : '';
54
55
 
55
56
  const owner = (props.units ?? []).find((face) => face.approval?.tools.includes(input.tool) === true);
56
- if (owner?.approval !== undefined) return <owner.approval.render input={input} t={t} />;
57
+ if (owner?.approval !== undefined) {
58
+ const Render = owner.approval.render;
59
+ return (
60
+ <CardBoundary unit={owner.manifest.id} resetKey={props.callId ?? ''} t={t}>
61
+ <Render input={input} t={t} />
62
+ </CardBoundary>
63
+ );
64
+ }
57
65
 
58
66
  if (command === '') return null;
59
67
  return (