@dsh-workbench/shared 0.2.4 → 0.2.6

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.
@@ -3,6 +3,7 @@ import './Note.css';
3
3
  export interface NoteProps {
4
4
  readonly tone?: 'muted' | 'problem';
5
5
  readonly as?: 'p' | 'span';
6
+ readonly role?: 'alert' | 'status';
6
7
  readonly children?: ReactNode | undefined;
7
8
  }
8
- export declare function Note({ tone, as, children }: NoteProps): JSX.Element;
9
+ export declare function Note({ tone, as, role, children }: NoteProps): JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { h } from '../lib/react.js';
2
2
  import './Note.css';
3
- export function Note({ tone = 'muted', as, children }) {
3
+ export function Note({ tone = 'muted', as, role, children }) {
4
4
  const className = tone === 'problem' ? 'wb-problem' : 'wb-note';
5
- return as === 'span' ? h("span", { className: className }, children) : h("p", { className: className }, children);
5
+ return as === 'span' ? (h("span", { className: className, role: role }, children)) : (h("p", { className: className, role: role }, children));
6
6
  }
@@ -1,26 +1,26 @@
1
1
 
2
- .wb-settings {
2
+ section.wb-settings {
3
3
  display: flex;
4
4
  flex-direction: column;
5
- gap: var(--wb-len-8);
6
- padding: var(--wb-len-10);
7
- border: 0.5px solid var(--dsw-alias-border-l2);
5
+ gap: var(--wb-len-12);
6
+ padding: 0;
7
+ border: 0;
8
8
  border-radius: var(--dsw-radius-md);
9
- background: var(--dsw-alias-bg-layer-1);
9
+ background: var(--dsw-alias-settings-card-fill);
10
10
  }
11
11
  .wb-settingsHead {
12
12
  display: flex;
13
13
  align-items: center;
14
14
  gap: var(--wb-len-8);
15
15
  }
16
- .wb-settingsTitle {
16
+ .wb-settings .wb-settingsTitle {
17
17
  flex: 1 1 auto;
18
18
  min-width: 0;
19
19
  overflow: hidden;
20
20
  text-overflow: ellipsis;
21
21
  white-space: nowrap;
22
- font-size: var(--dsw-font-xxs-12-font-size);
23
- line-height: var(--wb-line-18);
22
+ font-size: var(--dsw-font-base-16-font-size);
23
+ line-height: var(--wb-line-24);
24
24
  font-weight: 500;
25
25
  color: var(--dsw-alias-label-primary);
26
26
  }
package/dist/i18n/en.js CHANGED
@@ -13,6 +13,7 @@ export const en = {
13
13
  'project.search.placeholder': 'Search workspaces (folder / name / path)',
14
14
  'project.search.empty': 'No workspace matches “{query}”.',
15
15
  'project.group.workspaces': 'Workspaces',
16
+ 'project.group.defaultWorkspace': 'Default workspace',
16
17
  'project.group.workshop': 'Workshop',
17
18
  'project.rename.row': 'Rename “{name}”',
18
19
  'project.rename.current': 'Give this workspace a display name…',
@@ -29,20 +30,32 @@ export const en = {
29
30
  'session.new.label': 'Start a session in the current project',
30
31
  'session.empty': 'No sessions in this project yet.',
31
32
  'session.noProject': 'Add a project first.',
33
+ 'session.list.label': 'Sessions',
34
+ 'session.copy': 'Copy',
32
35
  'session.pinned': 'Pinned',
33
36
  'session.actions': 'Actions for {name}',
34
37
  'status.idle': 'Idle',
35
38
  'status.running': 'Running',
36
39
  'status.waitingApproval': 'Waiting for approval',
37
- 'status.planReview': 'Waiting for plan review',
40
+ 'status.planReview': 'Plan awaiting review',
38
41
  'status.waitingAnswer': 'Waiting for your answer',
42
+ 'status.compact.approval': 'Approval',
43
+ 'status.compact.planReview': 'Plan review',
44
+ 'status.compact.answer': 'Answer',
39
45
  'status.completed': 'Completed',
40
46
  'status.subagentsRunning.one': '{n} subagent running',
41
47
  'status.subagentsRunning.other': '{n} subagents running',
48
+ 'hover.copied': 'Copied',
42
49
  'time.now': 'just now',
43
50
  'time.minutes': '{n} min ago',
44
51
  'time.hours': '{n} h ago',
45
52
  'time.days': '{n} d ago',
53
+ 'time.ago': '{t} ago',
54
+ 'time.short.minutes': '{n}min',
55
+ 'time.short.hours': '{n}h',
56
+ 'time.short.days': '{n}d',
57
+ 'time.short.months': '{n}mo',
58
+ 'time.short.years': '{n}y',
46
59
  'panels.title': 'Panel modules',
47
60
  'panels.groupBoardless': 'No-board modules',
48
61
  'panels.groupPlugins': 'opencli plugins',
@@ -343,4 +356,37 @@ export const en = {
343
356
  'board.title': 'Workbench panel',
344
357
  'panel.conversation': 'Conversation',
345
358
  'panels.label': 'Global panels',
359
+ 'configSync.nav': 'Config sync',
360
+ 'configSync.lead': 'Bring over the rows another profile configures explicitly — pick any profile as the source. A restart applies them.',
361
+ 'configSync.source': 'Source',
362
+ 'configSync.sourceBroken': 'its config is unreadable',
363
+ 'configSync.sourceRows': '{name}: {count} rows are new or different for the workbench',
364
+ 'configSync.sourceDead': 'unreadable',
365
+ 'configSync.refresh': 'Read again',
366
+ 'configSync.loading': 'Reading the source config…',
367
+ 'configSync.readHint': 'Pick another source, or read again.',
368
+ 'configSync.tag.new': 'New',
369
+ 'configSync.tag.differs': 'Differs',
370
+ 'configSync.conflictNote': 'Applying replaces this workbench copy with the source',
371
+ 'configSync.code.lang': 'yaml',
372
+ 'configSync.code.copy': 'Copy',
373
+ 'configSync.code.copied': 'Copied',
374
+ 'configSync.code.wrap': 'Wrap lines',
375
+ 'configSync.code.unwrap': 'No wrap',
376
+ 'configSync.code.collapse': 'Collapse',
377
+ 'configSync.code.collapseAria': 'Collapse this row',
378
+ 'configSync.code.expandAria': 'Expand the diff ({count} more lines)',
379
+ 'configSync.code.expand': 'Show {count} more lines',
380
+ 'configSync.missing': '{pkg} is not installed: the row will not take effect — run `dsh plugin --profile {profile} add {pkg}`, or install it from the module market',
381
+ 'configSync.apply': 'Sync {count} selected',
382
+ 'configSync.applyNone': 'Pick some rows first',
383
+ 'configSync.applying': 'Writing…',
384
+ 'configSync.applied': 'Written to the workbench config · restart to apply',
385
+ 'configSync.appliedIds': 'Written this time: {ids}',
386
+ 'configSync.skippedIds': 'Left alone (already here): {ids}',
387
+ 'configSync.nothing': 'Nothing to sync',
388
+ 'configSync.unreadable': 'Cannot read {path}: {reason}',
389
+ 'configSync.noSource': 'This machine has no other profile to sync from',
390
+ 'configSync.emptyProviders': 'The workbench has no model provider yet — sync the llm- rows from some profile, or configure one under Models.',
391
+ 'configSync.failed': 'The sync did not go through; try again.',
346
392
  };
@@ -15,6 +15,7 @@ export declare const dictionaries: {
15
15
  readonly 'project.search.placeholder': "搜索工作区(目录名 / 中文名 / 路径)";
16
16
  readonly 'project.search.empty': "没有匹配「{query}」的工作区。";
17
17
  readonly 'project.group.workspaces': "工作区";
18
+ readonly 'project.group.defaultWorkspace': "默认工作区";
18
19
  readonly 'project.group.workshop': "工坊";
19
20
  readonly 'project.rename.row': "改「{name}」的中文名";
20
21
  readonly 'project.rename.current': "改当前工作区的中文名…";
@@ -31,20 +32,32 @@ export declare const dictionaries: {
31
32
  readonly 'session.new.label': "在当前项目新建会话";
32
33
  readonly 'session.empty': "这个项目还没有会话。";
33
34
  readonly 'session.noProject': "先添加一个项目。";
35
+ readonly 'session.list.label': "会话";
36
+ readonly 'session.copy': "复制";
34
37
  readonly 'session.pinned': "已置顶";
35
38
  readonly 'session.actions': "{name} 的操作";
36
39
  readonly 'status.idle': "空闲";
37
- readonly 'status.running': "运行中";
38
- readonly 'status.waitingApproval': "等待授权";
39
- readonly 'status.planReview': "等待计划确认";
40
+ readonly 'status.running': "进行中";
41
+ readonly 'status.waitingApproval': "等待审批";
42
+ readonly 'status.planReview': "计划待审";
40
43
  readonly 'status.waitingAnswer': "等待回答";
44
+ readonly 'status.compact.approval': "待审批";
45
+ readonly 'status.compact.planReview': "计划待审";
46
+ readonly 'status.compact.answer': "待回答";
41
47
  readonly 'status.completed': "已完成";
42
48
  readonly 'status.subagentsRunning.one': "{n} 个子代理运行中";
43
49
  readonly 'status.subagentsRunning.other': "{n} 个子代理运行中";
50
+ readonly 'hover.copied': "已复制";
44
51
  readonly 'time.now': "刚刚";
45
52
  readonly 'time.minutes': "{n} 分钟前";
46
53
  readonly 'time.hours': "{n} 小时前";
47
54
  readonly 'time.days': "{n} 天前";
55
+ readonly 'time.ago': "{t}前";
56
+ readonly 'time.short.minutes': "{n}分钟";
57
+ readonly 'time.short.hours': "{n}小时";
58
+ readonly 'time.short.days': "{n}天";
59
+ readonly 'time.short.months': "{n}个月";
60
+ readonly 'time.short.years': "{n}年";
48
61
  readonly 'panels.title': "面板模块";
49
62
  readonly 'panels.groupBoardless': "无看板模块";
50
63
  readonly 'panels.groupPlugins': "opencli 插件";
@@ -345,8 +358,41 @@ export declare const dictionaries: {
345
358
  readonly 'board.title': "工作台面板";
346
359
  readonly 'panel.conversation': "对话";
347
360
  readonly 'panels.label': "全局面板";
361
+ readonly 'configSync.nav': "配置同步";
362
+ readonly 'configSync.lead': "把另一个 profile 里显式配过的行搬过来 —— 来源随你选。搬完要重启工作台才生效。";
363
+ readonly 'configSync.source': "来源";
364
+ readonly 'configSync.sourceBroken': "读不到它的配置";
365
+ readonly 'configSync.sourceRows': "{name} 这份里你有 {count} 行可搬";
366
+ readonly 'configSync.sourceDead': "读不到";
367
+ readonly 'configSync.refresh': "重新读一遍";
368
+ readonly 'configSync.loading': "正在读来源的配置…";
369
+ readonly 'configSync.readHint': "换个来源,或再读一遍。";
370
+ readonly 'configSync.tag.new': "新增";
371
+ readonly 'configSync.tag.differs': "差异";
372
+ readonly 'configSync.conflictNote': "应用后工作台这份会被来源覆盖";
373
+ readonly 'configSync.code.lang': "yaml";
374
+ readonly 'configSync.code.copy': "复制";
375
+ readonly 'configSync.code.copied': "已复制";
376
+ readonly 'configSync.code.wrap': "自动换行";
377
+ readonly 'configSync.code.unwrap': "不换行";
378
+ readonly 'configSync.code.collapse': "收起";
379
+ readonly 'configSync.code.collapseAria': "收起这一行的配置";
380
+ readonly 'configSync.code.expandAria': "展开差异(还有 {count} 行)";
381
+ readonly 'configSync.code.expand': "展开剩余 {count} 行";
382
+ readonly 'configSync.missing': "{pkg} 还没装:这一行搬过来也不会生效 —— 先 `dsh plugin --profile {profile} add {pkg}`,或在模块市场里装";
383
+ readonly 'configSync.apply': "同步选中的 {count} 行";
384
+ readonly 'configSync.applyNone': "先勾几行";
385
+ readonly 'configSync.applying': "正在写入…";
386
+ readonly 'configSync.applied': "已写入工作台配置 · 重启工作台后生效";
387
+ readonly 'configSync.appliedIds': "这次写进去:{ids}";
388
+ readonly 'configSync.skippedIds': "这几行工作台自己已有,没动:{ids}";
389
+ readonly 'configSync.nothing': "没有需要同步的内容";
390
+ readonly 'configSync.unreadable': "读不到 {path}:{reason}";
391
+ readonly 'configSync.noSource': "这台机器上只有工作台这一个 profile,没有可同步的来源";
392
+ readonly 'configSync.emptyProviders': "工作台还没有任何模型供应商 —— 去「配置同步」从某个 profile 把 llm- 那几行搬过来,或自己去「模型」里配。";
393
+ readonly 'configSync.failed': "同步没成功,再试一次。";
348
394
  };
349
- 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.add.busy" | "project.add.failed" | "project.search.label" | "project.search.placeholder" | "project.search.empty" | "project.group.workspaces" | "project.group.workshop" | "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.groupBoardless" | "panels.groupPlugins" | "panels.market.defaultHint" | "panels.market.defaultLabel" | "panels.market.defaultLocked" | "panels.followAll" | "panels.followAllConfirmTitle" | "panels.followAllConfirmDetail" | "panels.followAllConfirmAck" | "panels.followAllConfirmYes" | "panels.followAllConfirmNo" | "panels.enabled" | "panels.disabled" | "panels.choose" | "panels.buildModule" | "panels.failed" | "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.notActive" | "panels.market.notUsable" | "panels.market.broken" | "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.needCommands" | "panels.market.localMark" | "panels.market.localHint" | "panels.market.noSource" | "panels.market.badSource" | "panels.market.empty" | "panels.market.sectionPlugins" | "panels.market.moduleOutdated" | "panels.market.moduleRefused" | "panels.market.moduleRefusedDetail" | "panels.market.moduleRestartHint" | "panels.market.workbenchName" | "panels.market.workbenchLocal" | "panels.market.pluginKeptInUse" | "panels.market.pluginKeptUnsure" | "panels.market.pluginKeptHeld" | "panels.market.pluginKeptUnnamed" | "panels.market.pluginBroken" | "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.problemNotWorkbenchModule" | "panels.market.add.problemNotModuleOrPlugin" | "panels.market.add.failureNotModuleOrPlugin" | "panels.market.add.installedPlugin" | "panels.market.add.pluginPreview" | "panels.market.add.pluginAlias" | "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.failureInstalled" | "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.classifying" | "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" | "rightbar.openPlatform" | "coding.notARepo.title" | "error.unknown" | "board.title" | "panel.conversation" | "panels.label", string>;
395
+ en: Record<"time.now" | "time.ago" | "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.add.busy" | "project.add.failed" | "project.search.label" | "project.search.placeholder" | "project.search.empty" | "project.group.workspaces" | "project.group.defaultWorkspace" | "project.group.workshop" | "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.list.label" | "session.copy" | "session.pinned" | "session.actions" | "status.idle" | "status.running" | "status.waitingApproval" | "status.planReview" | "status.waitingAnswer" | "status.compact.approval" | "status.compact.planReview" | "status.compact.answer" | "status.completed" | "status.subagentsRunning.one" | "status.subagentsRunning.other" | "hover.copied" | "time.short.minutes" | "time.short.hours" | "time.short.days" | "time.short.months" | "time.short.years" | "panels.title" | "panels.groupBoardless" | "panels.groupPlugins" | "panels.market.defaultHint" | "panels.market.defaultLabel" | "panels.market.defaultLocked" | "panels.followAll" | "panels.followAllConfirmTitle" | "panels.followAllConfirmDetail" | "panels.followAllConfirmAck" | "panels.followAllConfirmYes" | "panels.followAllConfirmNo" | "panels.enabled" | "panels.disabled" | "panels.choose" | "panels.buildModule" | "panels.failed" | "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.notActive" | "panels.market.notUsable" | "panels.market.broken" | "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.needCommands" | "panels.market.localMark" | "panels.market.localHint" | "panels.market.noSource" | "panels.market.badSource" | "panels.market.empty" | "panels.market.sectionPlugins" | "panels.market.moduleOutdated" | "panels.market.moduleRefused" | "panels.market.moduleRefusedDetail" | "panels.market.moduleRestartHint" | "panels.market.workbenchName" | "panels.market.workbenchLocal" | "panels.market.pluginKeptInUse" | "panels.market.pluginKeptUnsure" | "panels.market.pluginKeptHeld" | "panels.market.pluginKeptUnnamed" | "panels.market.pluginBroken" | "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.problemNotWorkbenchModule" | "panels.market.add.problemNotModuleOrPlugin" | "panels.market.add.failureNotModuleOrPlugin" | "panels.market.add.installedPlugin" | "panels.market.add.pluginPreview" | "panels.market.add.pluginAlias" | "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.failureInstalled" | "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.classifying" | "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" | "rightbar.openPlatform" | "coding.notARepo.title" | "error.unknown" | "board.title" | "panel.conversation" | "panels.label" | "configSync.nav" | "configSync.lead" | "configSync.source" | "configSync.sourceBroken" | "configSync.sourceRows" | "configSync.sourceDead" | "configSync.refresh" | "configSync.loading" | "configSync.readHint" | "configSync.tag.new" | "configSync.tag.differs" | "configSync.conflictNote" | "configSync.code.lang" | "configSync.code.copy" | "configSync.code.copied" | "configSync.code.wrap" | "configSync.code.unwrap" | "configSync.code.collapse" | "configSync.code.collapseAria" | "configSync.code.expandAria" | "configSync.code.expand" | "configSync.missing" | "configSync.apply" | "configSync.applyNone" | "configSync.applying" | "configSync.applied" | "configSync.appliedIds" | "configSync.skippedIds" | "configSync.nothing" | "configSync.unreadable" | "configSync.noSource" | "configSync.emptyProviders" | "configSync.failed", string>;
350
396
  };
351
397
  export type { WorkbenchMessages };
352
398
  export interface Dictionaries {
package/dist/i18n/zh.d.ts CHANGED
@@ -13,6 +13,7 @@ export declare const zh: {
13
13
  readonly 'project.search.placeholder': "搜索工作区(目录名 / 中文名 / 路径)";
14
14
  readonly 'project.search.empty': "没有匹配「{query}」的工作区。";
15
15
  readonly 'project.group.workspaces': "工作区";
16
+ readonly 'project.group.defaultWorkspace': "默认工作区";
16
17
  readonly 'project.group.workshop': "工坊";
17
18
  readonly 'project.rename.row': "改「{name}」的中文名";
18
19
  readonly 'project.rename.current': "改当前工作区的中文名…";
@@ -29,20 +30,32 @@ export declare const zh: {
29
30
  readonly 'session.new.label': "在当前项目新建会话";
30
31
  readonly 'session.empty': "这个项目还没有会话。";
31
32
  readonly 'session.noProject': "先添加一个项目。";
33
+ readonly 'session.list.label': "会话";
34
+ readonly 'session.copy': "复制";
32
35
  readonly 'session.pinned': "已置顶";
33
36
  readonly 'session.actions': "{name} 的操作";
34
37
  readonly 'status.idle': "空闲";
35
- readonly 'status.running': "运行中";
36
- readonly 'status.waitingApproval': "等待授权";
37
- readonly 'status.planReview': "等待计划确认";
38
+ readonly 'status.running': "进行中";
39
+ readonly 'status.waitingApproval': "等待审批";
40
+ readonly 'status.planReview': "计划待审";
38
41
  readonly 'status.waitingAnswer': "等待回答";
42
+ readonly 'status.compact.approval': "待审批";
43
+ readonly 'status.compact.planReview': "计划待审";
44
+ readonly 'status.compact.answer': "待回答";
39
45
  readonly 'status.completed': "已完成";
40
46
  readonly 'status.subagentsRunning.one': "{n} 个子代理运行中";
41
47
  readonly 'status.subagentsRunning.other': "{n} 个子代理运行中";
48
+ readonly 'hover.copied': "已复制";
42
49
  readonly 'time.now': "刚刚";
43
50
  readonly 'time.minutes': "{n} 分钟前";
44
51
  readonly 'time.hours': "{n} 小时前";
45
52
  readonly 'time.days': "{n} 天前";
53
+ readonly 'time.ago': "{t}前";
54
+ readonly 'time.short.minutes': "{n}分钟";
55
+ readonly 'time.short.hours': "{n}小时";
56
+ readonly 'time.short.days': "{n}天";
57
+ readonly 'time.short.months': "{n}个月";
58
+ readonly 'time.short.years': "{n}年";
46
59
  readonly 'panels.title': "面板模块";
47
60
  readonly 'panels.groupBoardless': "无看板模块";
48
61
  readonly 'panels.groupPlugins': "opencli 插件";
@@ -343,5 +356,38 @@ export declare const zh: {
343
356
  readonly 'board.title': "工作台面板";
344
357
  readonly 'panel.conversation': "对话";
345
358
  readonly 'panels.label': "全局面板";
359
+ readonly 'configSync.nav': "配置同步";
360
+ readonly 'configSync.lead': "把另一个 profile 里显式配过的行搬过来 —— 来源随你选。搬完要重启工作台才生效。";
361
+ readonly 'configSync.source': "来源";
362
+ readonly 'configSync.sourceBroken': "读不到它的配置";
363
+ readonly 'configSync.sourceRows': "{name} 这份里你有 {count} 行可搬";
364
+ readonly 'configSync.sourceDead': "读不到";
365
+ readonly 'configSync.refresh': "重新读一遍";
366
+ readonly 'configSync.loading': "正在读来源的配置…";
367
+ readonly 'configSync.readHint': "换个来源,或再读一遍。";
368
+ readonly 'configSync.tag.new': "新增";
369
+ readonly 'configSync.tag.differs': "差异";
370
+ readonly 'configSync.conflictNote': "应用后工作台这份会被来源覆盖";
371
+ readonly 'configSync.code.lang': "yaml";
372
+ readonly 'configSync.code.copy': "复制";
373
+ readonly 'configSync.code.copied': "已复制";
374
+ readonly 'configSync.code.wrap': "自动换行";
375
+ readonly 'configSync.code.unwrap': "不换行";
376
+ readonly 'configSync.code.collapse': "收起";
377
+ readonly 'configSync.code.collapseAria': "收起这一行的配置";
378
+ readonly 'configSync.code.expandAria': "展开差异(还有 {count} 行)";
379
+ readonly 'configSync.code.expand': "展开剩余 {count} 行";
380
+ readonly 'configSync.missing': "{pkg} 还没装:这一行搬过来也不会生效 —— 先 `dsh plugin --profile {profile} add {pkg}`,或在模块市场里装";
381
+ readonly 'configSync.apply': "同步选中的 {count} 行";
382
+ readonly 'configSync.applyNone': "先勾几行";
383
+ readonly 'configSync.applying': "正在写入…";
384
+ readonly 'configSync.applied': "已写入工作台配置 · 重启工作台后生效";
385
+ readonly 'configSync.appliedIds': "这次写进去:{ids}";
386
+ readonly 'configSync.skippedIds': "这几行工作台自己已有,没动:{ids}";
387
+ readonly 'configSync.nothing': "没有需要同步的内容";
388
+ readonly 'configSync.unreadable': "读不到 {path}:{reason}";
389
+ readonly 'configSync.noSource': "这台机器上只有工作台这一个 profile,没有可同步的来源";
390
+ readonly 'configSync.emptyProviders': "工作台还没有任何模型供应商 —— 去「配置同步」从某个 profile 把 llm- 那几行搬过来,或自己去「模型」里配。";
391
+ readonly 'configSync.failed': "同步没成功,再试一次。";
346
392
  };
347
393
  export type WorkbenchMessages = typeof zh;
package/dist/i18n/zh.js CHANGED
@@ -13,6 +13,7 @@ export const zh = {
13
13
  'project.search.placeholder': '搜索工作区(目录名 / 中文名 / 路径)',
14
14
  'project.search.empty': '没有匹配「{query}」的工作区。',
15
15
  'project.group.workspaces': '工作区',
16
+ 'project.group.defaultWorkspace': '默认工作区',
16
17
  'project.group.workshop': '工坊',
17
18
  'project.rename.row': '改「{name}」的中文名',
18
19
  'project.rename.current': '改当前工作区的中文名…',
@@ -29,20 +30,32 @@ export const zh = {
29
30
  'session.new.label': '在当前项目新建会话',
30
31
  'session.empty': '这个项目还没有会话。',
31
32
  'session.noProject': '先添加一个项目。',
33
+ 'session.list.label': '会话',
34
+ 'session.copy': '复制',
32
35
  'session.pinned': '已置顶',
33
36
  'session.actions': '{name} 的操作',
34
37
  'status.idle': '空闲',
35
- 'status.running': '运行中',
36
- 'status.waitingApproval': '等待授权',
37
- 'status.planReview': '等待计划确认',
38
+ 'status.running': '进行中',
39
+ 'status.waitingApproval': '等待审批',
40
+ 'status.planReview': '计划待审',
38
41
  'status.waitingAnswer': '等待回答',
42
+ 'status.compact.approval': '待审批',
43
+ 'status.compact.planReview': '计划待审',
44
+ 'status.compact.answer': '待回答',
39
45
  'status.completed': '已完成',
40
46
  'status.subagentsRunning.one': '{n} 个子代理运行中',
41
47
  'status.subagentsRunning.other': '{n} 个子代理运行中',
48
+ 'hover.copied': '已复制',
42
49
  'time.now': '刚刚',
43
50
  'time.minutes': '{n} 分钟前',
44
51
  'time.hours': '{n} 小时前',
45
52
  'time.days': '{n} 天前',
53
+ 'time.ago': '{t}前',
54
+ 'time.short.minutes': '{n}分钟',
55
+ 'time.short.hours': '{n}小时',
56
+ 'time.short.days': '{n}天',
57
+ 'time.short.months': '{n}个月',
58
+ 'time.short.years': '{n}年',
46
59
  'panels.title': '面板模块',
47
60
  'panels.groupBoardless': '无看板模块',
48
61
  'panels.groupPlugins': 'opencli 插件',
@@ -343,4 +356,37 @@ export const zh = {
343
356
  'board.title': '工作台面板',
344
357
  'panel.conversation': '对话',
345
358
  'panels.label': '全局面板',
359
+ 'configSync.nav': '配置同步',
360
+ 'configSync.lead': '把另一个 profile 里显式配过的行搬过来 —— 来源随你选。搬完要重启工作台才生效。',
361
+ 'configSync.source': '来源',
362
+ 'configSync.sourceBroken': '读不到它的配置',
363
+ 'configSync.sourceRows': '{name} 这份里你有 {count} 行可搬',
364
+ 'configSync.sourceDead': '读不到',
365
+ 'configSync.refresh': '重新读一遍',
366
+ 'configSync.loading': '正在读来源的配置…',
367
+ 'configSync.readHint': '换个来源,或再读一遍。',
368
+ 'configSync.tag.new': '新增',
369
+ 'configSync.tag.differs': '差异',
370
+ 'configSync.conflictNote': '应用后工作台这份会被来源覆盖',
371
+ 'configSync.code.lang': 'yaml',
372
+ 'configSync.code.copy': '复制',
373
+ 'configSync.code.copied': '已复制',
374
+ 'configSync.code.wrap': '自动换行',
375
+ 'configSync.code.unwrap': '不换行',
376
+ 'configSync.code.collapse': '收起',
377
+ 'configSync.code.collapseAria': '收起这一行的配置',
378
+ 'configSync.code.expandAria': '展开差异(还有 {count} 行)',
379
+ 'configSync.code.expand': '展开剩余 {count} 行',
380
+ 'configSync.missing': '{pkg} 还没装:这一行搬过来也不会生效 —— 先 `dsh plugin --profile {profile} add {pkg}`,或在模块市场里装',
381
+ 'configSync.apply': '同步选中的 {count} 行',
382
+ 'configSync.applyNone': '先勾几行',
383
+ 'configSync.applying': '正在写入…',
384
+ 'configSync.applied': '已写入工作台配置 · 重启工作台后生效',
385
+ 'configSync.appliedIds': '这次写进去:{ids}',
386
+ 'configSync.skippedIds': '这几行工作台自己已有,没动:{ids}',
387
+ 'configSync.nothing': '没有需要同步的内容',
388
+ 'configSync.unreadable': '读不到 {path}:{reason}',
389
+ 'configSync.noSource': '这台机器上只有工作台这一个 profile,没有可同步的来源',
390
+ 'configSync.emptyProviders': '工作台还没有任何模型供应商 —— 去「配置同步」从某个 profile 把 llm- 那几行搬过来,或自己去「模型」里配。',
391
+ 'configSync.failed': '同步没成功,再试一次。',
346
392
  };
package/dist/lib/ui.d.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  export declare function cx(...parts: Array<string | false | null | undefined>): string;
2
2
  export type TimeTranslate = (key: string, params?: Record<string, unknown>) => string;
3
+ export type RelativeTimeUnit = 'now' | 'minutes' | 'hours' | 'days' | 'months' | 'years';
4
+ export interface RelativeTimeBucket {
5
+ readonly unit: RelativeTimeUnit;
6
+ readonly n: number;
7
+ }
8
+ export declare function relativeTimeBucket(epochMs: number, now?: number): RelativeTimeBucket | null;
9
+ export declare function relativeTimeCompact(epochMs: number, t: TimeTranslate, now?: number): string;
10
+ export declare function relativeTimeAgo(epochMs: number, t: TimeTranslate, now?: number): string;
3
11
  export declare function relativeTime(epochMs: number, t: TimeTranslate): string;
4
12
  export declare function shortenPath(path: unknown): string;
5
13
  export declare function parseLocalTimestamp(text: string | null | undefined): number;
package/dist/lib/ui.js CHANGED
@@ -2,6 +2,40 @@ export function cx(...parts) {
2
2
  return parts.filter(Boolean).join(' ');
3
3
  }
4
4
  const MINUTE = 60 * 1000;
5
+ const HOUR = 60 * MINUTE;
6
+ const DAY = 24 * HOUR;
7
+ const MONTH = 30 * DAY;
8
+ const YEAR = 365 * DAY;
9
+ export function relativeTimeBucket(epochMs, now = Date.now()) {
10
+ if (!Number.isFinite(epochMs) || !Number.isFinite(now))
11
+ return null;
12
+ const diff = Math.max(0, now - epochMs);
13
+ if (diff < MINUTE)
14
+ return { unit: 'now', n: 0 };
15
+ if (diff < HOUR)
16
+ return { unit: 'minutes', n: Math.floor(diff / MINUTE) };
17
+ if (diff < DAY)
18
+ return { unit: 'hours', n: Math.floor(diff / HOUR) };
19
+ if (diff < MONTH)
20
+ return { unit: 'days', n: Math.floor(diff / DAY) };
21
+ if (diff < YEAR)
22
+ return { unit: 'months', n: Math.floor(diff / MONTH) };
23
+ return { unit: 'years', n: Math.floor(diff / YEAR) };
24
+ }
25
+ function bucketLabel(bucket, t) {
26
+ return bucket.unit === 'now' ? t('time.now') : t(`time.short.${bucket.unit}`, { n: bucket.n });
27
+ }
28
+ export function relativeTimeCompact(epochMs, t, now) {
29
+ const bucket = relativeTimeBucket(epochMs, now);
30
+ return bucket === null ? '' : bucketLabel(bucket, t);
31
+ }
32
+ export function relativeTimeAgo(epochMs, t, now) {
33
+ const bucket = relativeTimeBucket(epochMs, now);
34
+ if (bucket === null)
35
+ return '';
36
+ const label = bucketLabel(bucket, t);
37
+ return bucket.unit === 'now' ? label : t('time.ago', { t: label });
38
+ }
5
39
  export function relativeTime(epochMs, t) {
6
40
  if (!Number.isFinite(epochMs))
7
41
  return '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/shared",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "工作台与模块共享的纯代码与契约类型:类型、纯逻辑、哑组件、平台解析。宿主侧的东西从 /host 进(不影响浏览器产物)。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -5,10 +5,19 @@ import './Note.css';
5
5
  export interface NoteProps {
6
6
  readonly tone?: 'muted' | 'problem';
7
7
  readonly as?: 'p' | 'span';
8
+ readonly role?: 'alert' | 'status';
8
9
  readonly children?: ReactNode | undefined;
9
10
  }
10
11
 
11
- export function Note({ tone = 'muted', as, children }: NoteProps) {
12
+ export function Note({ tone = 'muted', as, role, children }: NoteProps) {
12
13
  const className = tone === 'problem' ? 'wb-problem' : 'wb-note';
13
- return as === 'span' ? <span className={className}>{children}</span> : <p className={className}>{children}</p>;
14
+ return as === 'span' ? (
15
+ <span className={className} role={role}>
16
+ {children}
17
+ </span>
18
+ ) : (
19
+ <p className={className} role={role}>
20
+ {children}
21
+ </p>
22
+ );
14
23
  }
@@ -1,26 +1,26 @@
1
1
 
2
- .wb-settings {
2
+ section.wb-settings {
3
3
  display: flex;
4
4
  flex-direction: column;
5
- gap: var(--wb-len-8);
6
- padding: var(--wb-len-10);
7
- border: 0.5px solid var(--dsw-alias-border-l2);
5
+ gap: var(--wb-len-12);
6
+ padding: 0;
7
+ border: 0;
8
8
  border-radius: var(--dsw-radius-md);
9
- background: var(--dsw-alias-bg-layer-1);
9
+ background: var(--dsw-alias-settings-card-fill);
10
10
  }
11
11
  .wb-settingsHead {
12
12
  display: flex;
13
13
  align-items: center;
14
14
  gap: var(--wb-len-8);
15
15
  }
16
- .wb-settingsTitle {
16
+ .wb-settings .wb-settingsTitle {
17
17
  flex: 1 1 auto;
18
18
  min-width: 0;
19
19
  overflow: hidden;
20
20
  text-overflow: ellipsis;
21
21
  white-space: nowrap;
22
- font-size: var(--dsw-font-xxs-12-font-size);
23
- line-height: var(--wb-line-18);
22
+ font-size: var(--dsw-font-base-16-font-size);
23
+ line-height: var(--wb-line-24);
24
24
  font-weight: 500;
25
25
  color: var(--dsw-alias-label-primary);
26
26
  }
package/src/i18n/en.ts CHANGED
@@ -16,6 +16,7 @@ export const en: Record<keyof WorkbenchMessages, string> = {
16
16
  'project.search.placeholder': 'Search workspaces (folder / name / path)',
17
17
  'project.search.empty': 'No workspace matches “{query}”.',
18
18
  'project.group.workspaces': 'Workspaces',
19
+ 'project.group.defaultWorkspace': 'Default workspace',
19
20
  'project.group.workshop': 'Workshop',
20
21
  'project.rename.row': 'Rename “{name}”',
21
22
  'project.rename.current': 'Give this workspace a display name…',
@@ -32,21 +33,33 @@ export const en: Record<keyof WorkbenchMessages, string> = {
32
33
  'session.new.label': 'Start a session in the current project',
33
34
  'session.empty': 'No sessions in this project yet.',
34
35
  'session.noProject': 'Add a project first.',
36
+ 'session.list.label': 'Sessions',
37
+ 'session.copy': 'Copy',
35
38
  'session.pinned': 'Pinned',
36
39
  'session.actions': 'Actions for {name}',
37
40
  'status.idle': 'Idle',
38
41
  'status.running': 'Running',
39
42
  'status.waitingApproval': 'Waiting for approval',
40
- 'status.planReview': 'Waiting for plan review',
43
+ 'status.planReview': 'Plan awaiting review',
41
44
  'status.waitingAnswer': 'Waiting for your answer',
45
+ 'status.compact.approval': 'Approval',
46
+ 'status.compact.planReview': 'Plan review',
47
+ 'status.compact.answer': 'Answer',
42
48
  'status.completed': 'Completed',
43
49
  'status.subagentsRunning.one': '{n} subagent running',
44
50
  'status.subagentsRunning.other': '{n} subagents running',
45
51
 
52
+ 'hover.copied': 'Copied',
46
53
  'time.now': 'just now',
47
54
  'time.minutes': '{n} min ago',
48
55
  'time.hours': '{n} h ago',
49
56
  'time.days': '{n} d ago',
57
+ 'time.ago': '{t} ago',
58
+ 'time.short.minutes': '{n}min',
59
+ 'time.short.hours': '{n}h',
60
+ 'time.short.days': '{n}d',
61
+ 'time.short.months': '{n}mo',
62
+ 'time.short.years': '{n}y',
50
63
 
51
64
  'panels.title': 'Panel modules',
52
65
  'panels.groupBoardless': 'No-board modules',
@@ -355,4 +368,38 @@ export const en: Record<keyof WorkbenchMessages, string> = {
355
368
 
356
369
  'panel.conversation': 'Conversation',
357
370
  'panels.label': 'Global panels',
371
+
372
+ 'configSync.nav': 'Config sync',
373
+ 'configSync.lead': 'Bring over the rows another profile configures explicitly — pick any profile as the source. A restart applies them.',
374
+ 'configSync.source': 'Source',
375
+ 'configSync.sourceBroken': 'its config is unreadable',
376
+ 'configSync.sourceRows': '{name}: {count} rows are new or different for the workbench',
377
+ 'configSync.sourceDead': 'unreadable',
378
+ 'configSync.refresh': 'Read again',
379
+ 'configSync.loading': 'Reading the source config…',
380
+ 'configSync.readHint': 'Pick another source, or read again.',
381
+ 'configSync.tag.new': 'New',
382
+ 'configSync.tag.differs': 'Differs',
383
+ 'configSync.conflictNote': 'Applying replaces this workbench copy with the source',
384
+ 'configSync.code.lang': 'yaml',
385
+ 'configSync.code.copy': 'Copy',
386
+ 'configSync.code.copied': 'Copied',
387
+ 'configSync.code.wrap': 'Wrap lines',
388
+ 'configSync.code.unwrap': 'No wrap',
389
+ 'configSync.code.collapse': 'Collapse',
390
+ 'configSync.code.collapseAria': 'Collapse this row',
391
+ 'configSync.code.expandAria': 'Expand the diff ({count} more lines)',
392
+ 'configSync.code.expand': 'Show {count} more lines',
393
+ 'configSync.missing': '{pkg} is not installed: the row will not take effect — run `dsh plugin --profile {profile} add {pkg}`, or install it from the module market',
394
+ 'configSync.apply': 'Sync {count} selected',
395
+ 'configSync.applyNone': 'Pick some rows first',
396
+ 'configSync.applying': 'Writing…',
397
+ 'configSync.applied': 'Written to the workbench config · restart to apply',
398
+ 'configSync.appliedIds': 'Written this time: {ids}',
399
+ 'configSync.skippedIds': 'Left alone (already here): {ids}',
400
+ 'configSync.nothing': 'Nothing to sync',
401
+ 'configSync.unreadable': 'Cannot read {path}: {reason}',
402
+ 'configSync.noSource': 'This machine has no other profile to sync from',
403
+ 'configSync.emptyProviders': 'The workbench has no model provider yet — sync the llm- rows from some profile, or configure one under Models.',
404
+ 'configSync.failed': 'The sync did not go through; try again.',
358
405
  };
package/src/i18n/zh.ts CHANGED
@@ -14,6 +14,7 @@ export const zh = {
14
14
  'project.search.placeholder': '搜索工作区(目录名 / 中文名 / 路径)',
15
15
  'project.search.empty': '没有匹配「{query}」的工作区。',
16
16
  'project.group.workspaces': '工作区',
17
+ 'project.group.defaultWorkspace': '默认工作区',
17
18
  'project.group.workshop': '工坊',
18
19
  'project.rename.row': '改「{name}」的中文名',
19
20
  'project.rename.current': '改当前工作区的中文名…',
@@ -30,21 +31,33 @@ export const zh = {
30
31
  'session.new.label': '在当前项目新建会话',
31
32
  'session.empty': '这个项目还没有会话。',
32
33
  'session.noProject': '先添加一个项目。',
34
+ 'session.list.label': '会话',
35
+ 'session.copy': '复制',
33
36
  'session.pinned': '已置顶',
34
37
  'session.actions': '{name} 的操作',
35
38
  'status.idle': '空闲',
36
- 'status.running': '运行中',
37
- 'status.waitingApproval': '等待授权',
38
- 'status.planReview': '等待计划确认',
39
+ 'status.running': '进行中',
40
+ 'status.waitingApproval': '等待审批',
41
+ 'status.planReview': '计划待审',
39
42
  'status.waitingAnswer': '等待回答',
43
+ 'status.compact.approval': '待审批',
44
+ 'status.compact.planReview': '计划待审',
45
+ 'status.compact.answer': '待回答',
40
46
  'status.completed': '已完成',
41
47
  'status.subagentsRunning.one': '{n} 个子代理运行中',
42
48
  'status.subagentsRunning.other': '{n} 个子代理运行中',
43
49
 
50
+ 'hover.copied': '已复制',
44
51
  'time.now': '刚刚',
45
52
  'time.minutes': '{n} 分钟前',
46
53
  'time.hours': '{n} 小时前',
47
54
  'time.days': '{n} 天前',
55
+ 'time.ago': '{t}前',
56
+ 'time.short.minutes': '{n}分钟',
57
+ 'time.short.hours': '{n}小时',
58
+ 'time.short.days': '{n}天',
59
+ 'time.short.months': '{n}个月',
60
+ 'time.short.years': '{n}年',
48
61
 
49
62
  'panels.title': '面板模块',
50
63
  'panels.groupBoardless': '无看板模块',
@@ -351,6 +364,40 @@ export const zh = {
351
364
 
352
365
  'panel.conversation': '对话',
353
366
  'panels.label': '全局面板',
367
+
368
+ 'configSync.nav': '配置同步',
369
+ 'configSync.lead': '把另一个 profile 里显式配过的行搬过来 —— 来源随你选。搬完要重启工作台才生效。',
370
+ 'configSync.source': '来源',
371
+ 'configSync.sourceBroken': '读不到它的配置',
372
+ 'configSync.sourceRows': '{name} 这份里你有 {count} 行可搬',
373
+ 'configSync.sourceDead': '读不到',
374
+ 'configSync.refresh': '重新读一遍',
375
+ 'configSync.loading': '正在读来源的配置…',
376
+ 'configSync.readHint': '换个来源,或再读一遍。',
377
+ 'configSync.tag.new': '新增',
378
+ 'configSync.tag.differs': '差异',
379
+ 'configSync.conflictNote': '应用后工作台这份会被来源覆盖',
380
+ 'configSync.code.lang': 'yaml',
381
+ 'configSync.code.copy': '复制',
382
+ 'configSync.code.copied': '已复制',
383
+ 'configSync.code.wrap': '自动换行',
384
+ 'configSync.code.unwrap': '不换行',
385
+ 'configSync.code.collapse': '收起',
386
+ 'configSync.code.collapseAria': '收起这一行的配置',
387
+ 'configSync.code.expandAria': '展开差异(还有 {count} 行)',
388
+ 'configSync.code.expand': '展开剩余 {count} 行',
389
+ 'configSync.missing': '{pkg} 还没装:这一行搬过来也不会生效 —— 先 `dsh plugin --profile {profile} add {pkg}`,或在模块市场里装',
390
+ 'configSync.apply': '同步选中的 {count} 行',
391
+ 'configSync.applyNone': '先勾几行',
392
+ 'configSync.applying': '正在写入…',
393
+ 'configSync.applied': '已写入工作台配置 · 重启工作台后生效',
394
+ 'configSync.appliedIds': '这次写进去:{ids}',
395
+ 'configSync.skippedIds': '这几行工作台自己已有,没动:{ids}',
396
+ 'configSync.nothing': '没有需要同步的内容',
397
+ 'configSync.unreadable': '读不到 {path}:{reason}',
398
+ 'configSync.noSource': '这台机器上只有工作台这一个 profile,没有可同步的来源',
399
+ 'configSync.emptyProviders': '工作台还没有任何模型供应商 —— 去「配置同步」从某个 profile 把 llm- 那几行搬过来,或自己去「模型」里配。',
400
+ 'configSync.failed': '同步没成功,再试一次。',
354
401
  } as const;
355
402
 
356
403
  export type WorkbenchMessages = typeof zh;
@@ -3,12 +3,17 @@ import {
3
3
  cx,
4
4
  parseLocalTimestamp,
5
5
  relativeTime,
6
+ relativeTimeAgo,
7
+ relativeTimeBucket,
8
+ relativeTimeCompact,
6
9
  shortenPath,
7
10
  type TimeTranslate,
8
11
  } from './ui.js';
9
12
 
10
- const t: TimeTranslate = (key, params) =>
11
- params === undefined ? key : `${key}:${String(params.n ?? '')}`;
13
+ const t: TimeTranslate = (key, params) => {
14
+ if (params === undefined) return key;
15
+ return `${key}:${String(params.n ?? params.t ?? '')}`;
16
+ };
12
17
 
13
18
  const NOW = Date.parse('2026-09-27T12:00:00.000Z');
14
19
 
@@ -59,6 +64,48 @@ describe('relativeTime', () => {
59
64
  });
60
65
  });
61
66
 
67
+ describe('relativeTimeBucket / relativeTimeCompact / relativeTimeAgo(会话行那一列)', () => {
68
+ const at = (ago: number): number => NOW - ago;
69
+ const MIN = 60_000;
70
+ const HOUR = 3_600_000;
71
+ const DAY = 86_400_000;
72
+
73
+ it('★ 档位与 dsh 的 relative-time 逐条一致:now / minutes / hours / days / months / years', () => {
74
+ expect(relativeTimeBucket(at(30_000), NOW)).toEqual({ unit: 'now', n: 0 });
75
+ expect(relativeTimeBucket(at(3 * MIN), NOW)).toEqual({ unit: 'minutes', n: 3 });
76
+ expect(relativeTimeBucket(at(5 * HOUR), NOW)).toEqual({ unit: 'hours', n: 5 });
77
+ expect(relativeTimeBucket(at(3 * DAY), NOW)).toEqual({ unit: 'days', n: 3 });
78
+ expect(relativeTimeBucket(at(40 * DAY), NOW)).toEqual({ unit: 'months', n: 1 });
79
+ expect(relativeTimeBucket(at(400 * DAY), NOW)).toEqual({ unit: 'years', n: 1 });
80
+ });
81
+
82
+ it('★ 边界在 30 天与 365 天那一侧(dsh 的分档,不是我们的 7 天)', () => {
83
+ expect(relativeTimeBucket(at(29 * DAY), NOW)?.unit).toBe('days');
84
+ expect(relativeTimeBucket(at(30 * DAY), NOW)?.unit).toBe('months');
85
+ expect(relativeTimeBucket(at(364 * DAY), NOW)?.unit).toBe('months');
86
+ expect(relativeTimeBucket(at(365 * DAY), NOW)?.unit).toBe('years');
87
+ });
88
+
89
+ it('未来时间钳到 now(不当成负档)', () => {
90
+ expect(relativeTimeBucket(NOW + MIN, NOW)).toEqual({ unit: 'now', n: 0 });
91
+ });
92
+
93
+ it('非法时间戳:桶给 null、两句都给空串', () => {
94
+ expect(relativeTimeBucket(Number.NaN, NOW)).toBeNull();
95
+ expect(relativeTimeBucket(NOW, Number.NaN)).toBeNull();
96
+ expect(relativeTimeCompact(Number.NaN, t, NOW)).toBe('');
97
+ expect(relativeTimeAgo(Number.NaN, t, NOW)).toBe('');
98
+ });
99
+
100
+ it('★ 行里是紧凑写法,悬停卡才补「前」;now 那一档两处都不加', () => {
101
+ expect(relativeTimeCompact(at(3 * MIN), t, NOW)).toBe('time.short.minutes:3');
102
+ expect(relativeTimeCompact(at(40 * DAY), t, NOW)).toBe('time.short.months:1');
103
+ expect(relativeTimeAgo(at(3 * MIN), t, NOW)).toBe('time.ago:time.short.minutes:3');
104
+ expect(relativeTimeCompact(at(30_000), t, NOW)).toBe('time.now');
105
+ expect(relativeTimeAgo(at(30_000), t, NOW)).toBe('time.now');
106
+ });
107
+ });
108
+
62
109
  describe('shortenPath', () => {
63
110
  it('把用户主目录缩成 ~', () => {
64
111
  expect(shortenPath('/Users/you/dev/app')).toBe('~/dev/app');
package/src/lib/ui.ts CHANGED
@@ -3,9 +3,47 @@ export function cx(...parts: Array<string | false | null | undefined>): string {
3
3
  }
4
4
 
5
5
  const MINUTE = 60 * 1000;
6
+ const HOUR = 60 * MINUTE;
7
+ const DAY = 24 * HOUR;
8
+ const MONTH = 30 * DAY;
9
+ const YEAR = 365 * DAY;
6
10
 
7
11
  export type TimeTranslate = (key: string, params?: Record<string, unknown>) => string;
8
12
 
13
+ export type RelativeTimeUnit = 'now' | 'minutes' | 'hours' | 'days' | 'months' | 'years';
14
+
15
+ export interface RelativeTimeBucket {
16
+ readonly unit: RelativeTimeUnit;
17
+ readonly n: number;
18
+ }
19
+
20
+ export function relativeTimeBucket(epochMs: number, now: number = Date.now()): RelativeTimeBucket | null {
21
+ if (!Number.isFinite(epochMs) || !Number.isFinite(now)) return null;
22
+ const diff = Math.max(0, now - epochMs);
23
+ if (diff < MINUTE) return { unit: 'now', n: 0 };
24
+ if (diff < HOUR) return { unit: 'minutes', n: Math.floor(diff / MINUTE) };
25
+ if (diff < DAY) return { unit: 'hours', n: Math.floor(diff / HOUR) };
26
+ if (diff < MONTH) return { unit: 'days', n: Math.floor(diff / DAY) };
27
+ if (diff < YEAR) return { unit: 'months', n: Math.floor(diff / MONTH) };
28
+ return { unit: 'years', n: Math.floor(diff / YEAR) };
29
+ }
30
+
31
+ function bucketLabel(bucket: RelativeTimeBucket, t: TimeTranslate): string {
32
+ return bucket.unit === 'now' ? t('time.now') : t(`time.short.${bucket.unit}`, { n: bucket.n });
33
+ }
34
+
35
+ export function relativeTimeCompact(epochMs: number, t: TimeTranslate, now?: number): string {
36
+ const bucket = relativeTimeBucket(epochMs, now);
37
+ return bucket === null ? '' : bucketLabel(bucket, t);
38
+ }
39
+
40
+ export function relativeTimeAgo(epochMs: number, t: TimeTranslate, now?: number): string {
41
+ const bucket = relativeTimeBucket(epochMs, now);
42
+ if (bucket === null) return '';
43
+ const label = bucketLabel(bucket, t);
44
+ return bucket.unit === 'now' ? label : t('time.ago', { t: label });
45
+ }
46
+
9
47
  export function relativeTime(epochMs: number, t: TimeTranslate): string {
10
48
  if (!Number.isFinite(epochMs)) return '';
11
49
  const diff = Date.now() - epochMs;