@chatcode/cco-better-sidebar 0.18.0-rc.3 → 0.18.0-rc.5

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 (47) hide show
  1. package/README.md +19 -1
  2. package/README_EN.md +19 -1
  3. package/lib/client-editor.js +326 -258
  4. package/lib/client-mermaid.js +312 -259
  5. package/lib/client-registry.js +1392 -605
  6. package/lib/client-terminal.js +328 -260
  7. package/lib/client.js +1352 -565
  8. package/lib/index.js +162 -5
  9. package/lib/types/client/LogoutConfirmDialog.d.ts +5 -0
  10. package/lib/types/client/PluginMarketSettingsSection.d.ts +7 -0
  11. package/lib/types/client/PluginMarketView.d.ts +7 -0
  12. package/lib/types/client/api.d.ts +15 -0
  13. package/lib/types/client/builtins/tabs.d.ts +6 -3
  14. package/lib/types/client/locales.d.ts +23 -0
  15. package/lib/types/client/plugin-market-catalog.d.ts +25 -0
  16. package/lib/types/client/ui-tweaks/branding.d.ts +1 -1
  17. package/lib/types/client/ui-tweaks/index.d.ts +1 -1
  18. package/lib/types/client/ui-tweaks/sidebar-header.d.ts +6 -0
  19. package/lib/types/client/ui-tweaks/typography.d.ts +9 -0
  20. package/lib/types/login-page-script.d.ts +2 -0
  21. package/lib/types/login-route.d.ts +2 -0
  22. package/lib/types/plugin-market-api.d.ts +5 -0
  23. package/lib/types/plugin-market-shared.d.ts +37 -0
  24. package/lib/types/ui-tweaks/config.d.ts +5 -6
  25. package/package.json +1 -1
  26. package/src/client/AccountMenu.tsx +12 -2
  27. package/src/client/LogoutConfirmDialog.tsx +39 -0
  28. package/src/client/PluginMarketSettingsSection.tsx +55 -0
  29. package/src/client/PluginMarketView.module.css +35 -0
  30. package/src/client/PluginMarketView.tsx +154 -0
  31. package/src/client/api.ts +7 -0
  32. package/src/client/builtins/tabs.tsx +16 -3
  33. package/src/client/index.tsx +10 -1
  34. package/src/client/locales-ja.ts +23 -0
  35. package/src/client/locales.ts +48 -0
  36. package/src/client/plugin-market-catalog.ts +90 -0
  37. package/src/client/sidebar.module.css +81 -19
  38. package/src/client/ui-tweaks/branding.tsx +25 -16
  39. package/src/client/ui-tweaks/index.tsx +36 -99
  40. package/src/client/ui-tweaks/sidebar-header.ts +73 -0
  41. package/src/client/ui-tweaks/typography.ts +38 -0
  42. package/src/index.ts +2 -0
  43. package/src/login-page-script.ts +107 -0
  44. package/src/login-route.ts +7 -4
  45. package/src/plugin-market-api.ts +42 -0
  46. package/src/plugin-market-shared.ts +21 -0
  47. package/src/ui-tweaks/config.ts +10 -10
@@ -442,13 +442,11 @@
442
442
  box-sizing: border-box;
443
443
  gap: 8px;
444
444
  width: 100%;
445
- min-height: 52px;
446
- padding: 10px 14px max(10px, env(safe-area-inset-bottom));
445
+ min-height: 44px;
446
+ padding: 8px 12px max(8px, env(safe-area-inset-bottom));
447
447
  border-radius: 0;
448
448
  background: var(--dsw-alias-bg-layer-1);
449
- font-family: var(--dsw-font-family-body);
450
- font-size: var(--dsw-font-size-body-sm);
451
- line-height: var(--dsw-font-line-height-body-sm);
449
+ font: var(--dsw-font-xs-13);
452
450
  transition: background var(--ds-transition-duration-slow) var(--ds-ease-in-out);
453
451
  }
454
452
 
@@ -466,18 +464,21 @@
466
464
  .accountAvatar {
467
465
  position: relative;
468
466
  display: grid;
469
- width: 28px;
470
- height: 28px;
467
+ box-sizing: border-box;
468
+ width: 24px;
469
+ height: 24px;
471
470
  flex: none;
472
471
  place-items: center;
473
472
  border-radius: 50%;
474
473
  background: var(--dsw-alias-bg-layer-3);
475
474
  border: 1px solid var(--dsw-alias-border-l2);
476
475
  color: var(--dsw-alias-fg-accent);
476
+ font: var(--dsw-font-xxs-12);
477
477
  font-weight: 600;
478
478
  }
479
479
 
480
480
  .accountName {
481
+ min-width: 0;
481
482
  overflow: hidden;
482
483
  text-overflow: ellipsis;
483
484
  white-space: nowrap;
@@ -490,10 +491,10 @@
490
491
  left: 8px;
491
492
  display: grid;
492
493
  box-sizing: border-box;
493
- width: min(252px, calc(100% - 16px));
494
- padding: 10px 8px 8px;
494
+ width: min(224px, calc(100% - 16px));
495
+ padding: 8px 6px 6px;
495
496
  border: 1px solid var(--dsw-alias-border-l2);
496
- border-radius: 12px;
497
+ border-radius: 10px;
497
498
  background: var(--dsw-alias-bg-layer-2);
498
499
  z-index: 1;
499
500
  }
@@ -502,16 +503,15 @@
502
503
  display: flex;
503
504
  align-items: center;
504
505
  gap: 8px;
505
- min-height: 36px;
506
- padding: 0 8px 8px;
507
- font-family: var(--dsw-font-family-body);
508
- font-size: var(--dsw-font-size-body-sm);
509
- line-height: var(--dsw-font-line-height-body-sm);
506
+ min-width: 0;
507
+ min-height: 28px;
508
+ padding: 0 6px 6px;
509
+ font: var(--dsw-font-xs-13);
510
510
  }
511
511
 
512
512
  .accountMenuDivider {
513
513
  height: 1px;
514
- margin: 0 4px 6px;
514
+ margin: 0 4px 4px;
515
515
  background: var(--dsw-alias-border-l2);
516
516
  }
517
517
 
@@ -519,12 +519,12 @@
519
519
  display: flex;
520
520
  align-items: center;
521
521
  gap: 8px;
522
- min-height: 32px;
523
- padding: 0 10px;
522
+ min-height: 30px;
523
+ padding: 0 8px;
524
524
  border-radius: 7px;
525
525
  background: transparent;
526
526
  text-align: left;
527
- font: var(--dsw-font-s-14);
527
+ font: var(--dsw-font-xs-13);
528
528
  }
529
529
 
530
530
  .accountItem:hover {
@@ -546,6 +546,68 @@
546
546
  border-color: var(--dsw-alias-bg-layer-1);
547
547
  }
548
548
 
549
+ /* The native top layer is viewport-centered even though the account rail is positioned. */
550
+ .accountLogoutDialog {
551
+ box-sizing: border-box;
552
+ width: min(400px, calc(100vw - 32px));
553
+ max-width: calc(100vw - 32px);
554
+ max-height: calc(100dvh - 32px);
555
+ margin: auto;
556
+ padding: 24px;
557
+ overflow: auto;
558
+ border: 1px solid var(--dsw-alias-border-l2);
559
+ border-radius: 16px;
560
+ background: var(--dsw-alias-bg-layer-2);
561
+ color: var(--dsw-alias-fg-base);
562
+ font: var(--dsw-font-s-14);
563
+ }
564
+
565
+ .accountLogoutDialog::backdrop {
566
+ background: var(--dsw-alias-bg-mask-1);
567
+ }
568
+
569
+ .accountLogoutTitle {
570
+ margin: 0;
571
+ font: var(--dsw-font-base-strong-16);
572
+ }
573
+
574
+ .accountLogoutDescription {
575
+ margin: 12px 0 24px;
576
+ color: var(--dsw-alias-label-secondary);
577
+ }
578
+
579
+ .accountLogoutActions {
580
+ display: flex;
581
+ flex-wrap: wrap;
582
+ justify-content: flex-end;
583
+ gap: 8px;
584
+ }
585
+
586
+ .accountLogoutActions button {
587
+ min-height: 34px;
588
+ padding: 6px 14px;
589
+ border: 1px solid var(--dsw-alias-border-l2);
590
+ border-radius: 8px;
591
+ background: var(--dsw-alias-bg-layer-2);
592
+ color: var(--dsw-alias-fg-base);
593
+ font: var(--dsw-font-xs-13);
594
+ cursor: pointer;
595
+ }
596
+
597
+ .accountLogoutActions button:hover {
598
+ background: var(--dsw-alias-bg-hover);
599
+ }
600
+
601
+ .accountLogoutActions button:focus-visible {
602
+ outline: 2px solid var(--dsw-alias-border-accent);
603
+ outline-offset: 2px;
604
+ }
605
+
606
+ .accountLogoutActions .accountLogoutConfirm {
607
+ border-color: var(--dsw-alias-state-error-primary);
608
+ color: var(--dsw-alias-state-error-primary);
609
+ }
610
+
549
611
  /* The compatibility action cluster used when the host exposes no native
550
612
  right-sidebar affordance. It stays pinned to the viewport corner, sitting
551
613
  flush in the 34px tab strip (28px buttons at top: 3) whose right end it
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { ReactNode } from 'react'
11
+ import { clearSidebarHeader, SIDEBAR_HEADER_CSS, syncSidebarHeader } from './sidebar-header.ts'
11
12
 
12
13
  declare module '@deepseek-ai/dsh-client-ui-slots' {
13
14
  interface SlotMap {
@@ -30,6 +31,8 @@ const PROJECT_NAME = 'ChatCode CLI'
30
31
  const HERO_TITLE = '与世界,深一度对话'
31
32
  const LEGACY_HOST_BRAND_TEXT = /DSH\s*(?:Local\s*Build|本地构建)|DeepSeek\s*Harness|ChatCode\s*CLI/giu
32
33
  const LEGACY_HOST_BRAND_LABEL_TEXT = /^(?:DSH\s*(?:Local\s*Build|本地构建)|ChatCode\s*CLI)$/iu
34
+ const BRAND_SURFACE = '[class*="brandIdentity"],[class*="brandName"],[class*="headline"],.dut-sidebar-brand-name'
35
+ const USER_CONTENT = 'input,textarea,[contenteditable],[role="textbox"],pre,code,script,style'
33
36
  const HOST_BUILD_VERSION_TEXT = /^v?\d+\.\d+\.\d+(?:[-+][0-9a-z.-]+)*$/iu
34
37
  const HERO_HEADLINE = /探索未至之境/u
35
38
  const HERO_PREVIEW_TEXT = /^(?:预览版|Preview)$/iu
@@ -87,14 +90,11 @@ body:not([data-ds-dark-theme]) .dut-unicom-primary-action:active{transform:trans
87
90
  hovered, the host owns the complete interaction treatment again. */
88
91
  body[data-ds-dark-theme] .dut-unicom-primary-action:not(:hover){background:#9B0B09 !important}
89
92
  .dut-project-logo{display:block;object-fit:contain}
90
- .dut-left-logo{width:44px !important;height:44px !important}
91
- .dut-brand-title{font-size:18px !important;font-weight:600 !important}
92
- .dut-sidebar-logo{width:44px !important;height:44px !important}
93
- /* The alpha sidebar keeps its slotted identity at 24px and clips the brand
94
- button. Give only our expanded identity the full artwork height so the
95
- 44px knot is not cropped by that host-owned overflow boundary. */
96
- [class*="brandIdentity"]:has(.dut-sidebar-logo){height:44px !important}
97
- .dut-sidebar-brand-name{font-size:18px !important;line-height:22px !important;font-weight:600 !important;letter-spacing:0 !important;white-space:nowrap}
93
+ .dut-left-logo{width:24px !important;height:24px !important}
94
+ .dut-brand-title{font:var(--dsw-font-s-strong-14) !important}
95
+ .dut-sidebar-logo{width:24px !important;height:24px !important}
96
+ .dut-sidebar-brand-name{font:var(--dsw-font-s-strong-14) !important;letter-spacing:-.01em !important;white-space:nowrap}
97
+ ${SIDEBAR_HEADER_CSS}
98
98
  .dut-brand-version{display:none !important}
99
99
  .dut-hero-preview{display:none !important}
100
100
  .dut-hero-logo{width:60px !important;height:60px !important}
@@ -104,8 +104,12 @@ body[data-ds-dark-theme] .dut-unicom-primary-action:not(:hover){background:#9B0B
104
104
  `
105
105
 
106
106
  function hasPrimaryLabel(button: HTMLButtonElement): boolean {
107
+ // Plugin cards own their appearance. Inventory disclosure buttons include
108
+ // descriptions, where words like "install" do not identify a primary action.
109
+ if (button.closest('[data-chatcode-plugin-market], [data-plugin-module]')) return false
107
110
  const label = `${button.textContent ?? ''} ${button.getAttribute('aria-label') ?? ''} ${button.title}`.trim().toLowerCase()
108
- return /(?:^|\s)(new session|send|install)(?:\s|$)|新会话|发送|(?:^|\s)安装(?:\s|$)/u.test(label)
111
+ if (button.classList.contains('dut-sidebar-brand') || button.classList.contains('dut-sidebar-new-session')) return false
112
+ return /(?:^|\s)(send|install)(?:\s|$)|发送|(?:^|\s)安装(?:\s|$)/u.test(label)
109
113
  }
110
114
 
111
115
  /** Replace one host SVG while retaining its generated layout class names. */
@@ -193,19 +197,22 @@ function syncBranding(): void {
193
197
 
194
198
  // The sidebar label identifies the precise icon to its left. Process it
195
199
  // before the generic text pass so the sibling relationship is still intact.
196
- document.querySelectorAll<HTMLElement>('body *').forEach((element) => {
197
- if (element.childElementCount !== 0 || !LEGACY_HOST_BRAND_LABEL_TEXT.test(element.textContent?.trim() ?? '')) return
198
- if (element.textContent !== PROJECT_NAME) element.textContent = PROJECT_NAME
199
- element.classList.add('dut-brand-title')
200
- replaceLogoBesideBrandLabel(element)
201
- hideVersionBesideBrandLabel(element)
200
+ document.querySelectorAll<HTMLElement>(BRAND_SURFACE).forEach((surface) => {
201
+ surface.querySelectorAll<HTMLElement>('*').forEach((element) => {
202
+ if (element.closest(USER_CONTENT) !== null) return
203
+ if (element.childElementCount !== 0 || !LEGACY_HOST_BRAND_LABEL_TEXT.test(element.textContent?.trim() ?? '')) return
204
+ if (element.textContent !== PROJECT_NAME) element.textContent = PROJECT_NAME
205
+ element.classList.add('dut-brand-title')
206
+ replaceLogoBesideBrandLabel(element)
207
+ hideVersionBesideBrandLabel(element)
208
+ })
202
209
  })
203
210
 
204
211
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT)
205
212
  let node: Text | null
206
213
  while ((node = walker.nextNode() as Text | null) !== null) {
207
214
  const parent = node.parentElement
208
- if (parent?.closest('script,style') !== null) continue
215
+ if (parent === null || parent.closest(USER_CONTENT) !== null || parent.closest(BRAND_SURFACE) === null) continue
209
216
  if (LEGACY_HOST_BRAND_TEXT.test(node.data)) {
210
217
  LEGACY_HOST_BRAND_TEXT.lastIndex = 0
211
218
  node.data = node.data.replace(LEGACY_HOST_BRAND_TEXT, PROJECT_NAME)
@@ -233,6 +240,7 @@ function syncBranding(): void {
233
240
  if (svg instanceof SVGElement) replaceHostLogo(svg)
234
241
  })
235
242
 
243
+ syncSidebarHeader()
236
244
  document.querySelectorAll<HTMLButtonElement>('button').forEach((button) => {
237
245
  button.classList.toggle('dut-unicom-primary-action', hasPrimaryLabel(button))
238
246
  })
@@ -277,6 +285,7 @@ export function installBranding(): () => void {
277
285
  return () => {
278
286
  observer.disconnect()
279
287
  if (frame !== undefined) window.cancelAnimationFrame(frame)
288
+ clearSidebarHeader()
280
289
  favicon.remove()
281
290
  style.remove()
282
291
  }
@@ -36,6 +36,7 @@ import { WhaleIndicator, installWhaleStyles } from './whale.tsx'
36
36
  import { PreciseCacheHitEntry } from './cachehit.tsx'
37
37
  import { installTaskNotifier, previewAlerts, requestNotifyPermission } from './notifier.ts'
38
38
  import { HeroProjectLogo, installBranding, ProjectLogoIcon, SidebarProjectLogo, SidebarProjectName } from './branding.tsx'
39
+ import { buildFontCss } from './typography.ts'
39
40
 
40
41
  const NS = 'ui-tweaks'
41
42
  const SETTINGS_ROUTE = '/_dsh/ui-tweaks/settings'
@@ -43,12 +44,12 @@ const SETTINGS_ROUTE = '/_dsh/ui-tweaks/settings'
43
44
  /** Client context narrowed to the browser sessions face in this combined build. */
44
45
  type UITweaksClientContext = ClientContext & { sessions: ISessions }
45
46
 
46
- const DEFAULT_FONT_SIZE = 16
47
+ const DEFAULT_FONT_SIZE = 14
47
48
  const MIN_FONT_SIZE = 10
48
49
  const MAX_FONT_SIZE = 32
49
50
  /** Legacy: code font as a percentage of the message font size (81% = stock 13/16). */
50
51
  const DEFAULT_CODE_FONT_SCALE = 81
51
- /** Absolute code font size (px): 13 matches the host default at a 16px body. */
52
+ /** Default absolute code-block size, paired with 14px prose. */
52
53
  const DEFAULT_CODE_FONT_SIZE = 13
53
54
  const MIN_CODE_FONT_SIZE = 8
54
55
  const MAX_CODE_FONT_SIZE = 32
@@ -134,7 +135,7 @@ const en = {
134
135
  fontSize: 'Message font size',
135
136
  fontSizeHint: `Number between ${MIN_FONT_SIZE} and ${MAX_FONT_SIZE}; applies to message text, headings, tables and code.`,
136
137
  codeFontSize: 'Code font size',
137
- codeFontSizeHint: `Absolute code size in px (${MIN_CODE_FONT_SIZE}–${MAX_CODE_FONT_SIZE}); ${DEFAULT_CODE_FONT_SIZE}px is ChatCode CLI's default at a 16px body. Applies to code blocks; inline code follows proportionally.`,
138
+ codeFontSizeHint: `Absolute code size in px (${MIN_CODE_FONT_SIZE}–${MAX_CODE_FONT_SIZE}); ${DEFAULT_CODE_FONT_SIZE}px is ChatCode CLI's default at a 14px body. Applies to code blocks; inline code follows proportionally.`,
138
139
  lineHeight: 'Line spacing',
139
140
  lineHeightHint: `Base vertical spacing in px: gaps between message rows (Think ↔ tool cards) and blocks inside one reply, plus text line-height, paragraph and list margins, all scale from it; ${DEFAULT_LINE_HEIGHT} is ChatCode CLI's default.`,
140
141
  tableStyle: 'Table style',
@@ -334,7 +335,7 @@ const zh: Record<LocaleKey, string> = {
334
335
  fontSize: '消息字体大小',
335
336
  fontSizeHint: `取值 ${MIN_FONT_SIZE}–${MAX_FONT_SIZE},作用于消息正文、标题、表格与代码。`,
336
337
  codeFontSize: '代码字号',
337
- codeFontSizeHint: `代码绝对字号,取值 ${MIN_CODE_FONT_SIZE}–${MAX_CODE_FONT_SIZE}px;${DEFAULT_CODE_FONT_SIZE}px 为 ChatCode CLI 默认(正文 16 时)。作用于代码块,行内代码按比例跟随。`,
338
+ codeFontSizeHint: `代码绝对字号,取值 ${MIN_CODE_FONT_SIZE}–${MAX_CODE_FONT_SIZE}px;${DEFAULT_CODE_FONT_SIZE}px 为 ChatCode CLI 默认(正文 14 时)。作用于代码块,行内代码按比例跟随。`,
338
339
  lineHeight: '行高',
339
340
  lineHeightHint: `回复区垂直间距的基准值:消息之间(如 Think ↔ 工具卡片)、回复内块之间、正文行高、段落与列表边距都按它缩放;取值 ${MIN_LINE_HEIGHT}–${MAX_LINE_HEIGHT}px,${DEFAULT_LINE_HEIGHT} 为 ChatCode CLI 默认。`,
340
341
  tableStyle: '表格样式',
@@ -533,11 +534,13 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
533
534
 
534
535
  function resolveValue(value: TweaksValue | undefined): ResolvedTweaks {
535
536
  const fontSize = typeof value?.fontSize === 'number' ? value.fontSize : DEFAULT_FONT_SIZE
536
- // Effective code size: the absolute px input wins; otherwise derive px from
537
- // the legacy percentage at the resolved body size; otherwise stock.
537
+ const codeFontScale = value?.codeFontScale ?? DEFAULT_CODE_FONT_SCALE
538
+ // Explicit code sizing wins; the stock preference keeps readable 13px code.
538
539
  const codeFontSize = typeof value?.codeFontSize === 'number'
539
540
  ? Math.min(MAX_CODE_FONT_SIZE, Math.max(MIN_CODE_FONT_SIZE, value.codeFontSize))
540
- : Math.max(8, Math.round(fontSize * (13 / 16) * ((value?.codeFontScale ?? DEFAULT_CODE_FONT_SCALE) / DEFAULT_CODE_FONT_SCALE)))
541
+ : codeFontScale === DEFAULT_CODE_FONT_SCALE
542
+ ? DEFAULT_CODE_FONT_SIZE
543
+ : Math.max(8, Math.round(fontSize * (13 / 16) * (codeFontScale / DEFAULT_CODE_FONT_SCALE)))
541
544
  return {
542
545
  fontSize,
543
546
  codeFontSize,
@@ -559,50 +562,6 @@ function resolveValue(value: TweaksValue | undefined): ResolvedTweaks {
559
562
  }
560
563
  }
561
564
 
562
- /** Scale one theme px value proportionally to the chosen base size. */
563
- function rel(base: number, fontSize: number): number {
564
- return Math.max(8, Math.round((base / DEFAULT_FONT_SIZE) * fontSize))
565
- }
566
-
567
- /** Rebuild the markdown font tokens for the chosen base size, keeping the theme faces. */
568
- function buildFontCss(fontSize: number, lineHeight: number, codeFontSize: number): string {
569
- const cs = getComputedStyle(document.body)
570
- const fam = (name: string, fallback: string): string => {
571
- const value = cs.getPropertyValue(name).trim()
572
- return value.length > 0 ? value : fallback
573
- }
574
- const base = fam('--dsw-font-markdown-base-font-family', '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif')
575
- const code = fam('--dsw-font-markdown-code-font-family', '"SF Mono", Consolas, monospace')
576
- const codeBlock = fam('--dsw-font-markdown-code-block-font-family', '"SF Mono", Consolas, monospace')
577
-
578
- const parts: string[] = []
579
- // `baseLine` is the token's line-height at the 16px baseline; it scales with
580
- // both the chosen font size and the 行高 unit, so at the default (16) it
581
- // reproduces the stock rhythm exactly.
582
- const token = (shorthand: string, size: number, baseLine: number, family: string): void => {
583
- const line = Math.max(12, Math.round((baseLine / DEFAULT_FONT_SIZE) * fontSize * (lineHeight / DEFAULT_LINE_HEIGHT)))
584
- parts.push(`--${shorthand}:${size}px/${line}px ${family}`)
585
- parts.push(`--${shorthand}-font-size:${size}px`)
586
- parts.push(`--${shorthand}-line-height:${line}px`)
587
- }
588
- // Code fonts hang off the absolute code-block size (stock: 13px at a 16px
589
- // body), with inline code slightly larger and the small variant slightly
590
- // smaller, preserving DSH's hierarchy (14/13 and 12/13 of the block).
591
- const codePx = (blockRatio: number): number => Math.max(8, Math.round(codeFontSize * blockRatio))
592
- token('dsw-font-markdown-base', fontSize, 28, base)
593
- token('dsw-font-markdown-base-strong', fontSize, 28, base)
594
- token('dsw-font-markdown-base-italic', fontSize, 28, base)
595
- token('dsw-font-markdown-base-strong-italic', fontSize, 28, base)
596
- token('dsw-font-markdown-h1', rel(24, fontSize), 34, base)
597
- token('dsw-font-markdown-h2', rel(22, fontSize), 32, base)
598
- token('dsw-font-markdown-h3', rel(20, fontSize), 30, base)
599
- token('dsw-font-markdown-h4', fontSize, 28, base)
600
- token('dsw-font-markdown-code', codePx(14 / 13), 22, code)
601
- token('dsw-font-markdown-code-block', codePx(1), 22, codeBlock)
602
- token('dsw-font-markdown-code-block-small', codePx(12 / 13), 18, codeBlock)
603
- return `body{${parts.join(';')}}`
604
- }
605
-
606
565
  /**
607
566
  * Claude Desktop-ish markdown table look: light-gray rounded cell cards with
608
567
  * small gaps, no borders. Cells share the theme's inline-code background and
@@ -610,7 +569,7 @@ function buildFontCss(fontSize: number, lineHeight: number, codeFontSize: number
610
569
  * renderer, so headers and cells always match.
611
570
  */
612
571
  const CLAUDE_TABLE_CSS = `
613
- div[data-slot="conversation.chat.node"] table{
572
+ div[data-slot="conversation.chat.node"] [class*="_markdown"]:not([data-markdown-variant="compact"]) table{
614
573
  border-collapse:separate !important;
615
574
  border-spacing:3px !important;
616
575
  width:100% !important;
@@ -621,7 +580,7 @@ div[data-slot="conversation.chat.node"] table{
621
580
  border:none !important;
622
581
  font-size:var(--dsw-font-markdown-base-font-size) !important;
623
582
  }
624
- div[data-slot="conversation.chat.node"] table thead th{
583
+ div[data-slot="conversation.chat.node"] [class*="_markdown"]:not([data-markdown-variant="compact"]) table thead th{
625
584
  background:var(--dsw-alias-markdown-inline-code) !important;
626
585
  color:inherit !important;
627
586
  font-weight:400 !important;
@@ -630,7 +589,7 @@ div[data-slot="conversation.chat.node"] table thead th{
630
589
  border:none !important;
631
590
  border-radius:6px !important;
632
591
  }
633
- div[data-slot="conversation.chat.node"] table tbody td{
592
+ div[data-slot="conversation.chat.node"] [class*="_markdown"]:not([data-markdown-variant="compact"]) table tbody td{
634
593
  background:var(--dsw-alias-markdown-inline-code) !important;
635
594
  color:inherit !important;
636
595
  font-size:inherit !important;
@@ -639,8 +598,8 @@ div[data-slot="conversation.chat.node"] table tbody td{
639
598
  border:none !important;
640
599
  border-radius:6px !important;
641
600
  }
642
- div[data-slot="conversation.chat.node"] table code,
643
- div[data-slot="conversation.chat.node"] table pre{
601
+ div[data-slot="conversation.chat.node"] [class*="_markdown"]:not([data-markdown-variant="compact"]) table code,
602
+ div[data-slot="conversation.chat.node"] [class*="_markdown"]:not([data-markdown-variant="compact"]) table pre{
644
603
  background:transparent !important;
645
604
  border:none !important;
646
605
  box-shadow:none !important;
@@ -650,52 +609,32 @@ div[data-slot="conversation.chat.node"] table pre{
650
609
  function buildRuntimeCss(value: ResolvedTweaks): string {
651
610
  const rules: string[] = []
652
611
  rules.push(buildFontCss(value.fontSize, value.lineHeight, value.codeFontSize))
653
- // User-sent messages use their own fixed font-size (not the markdown tokens);
654
- // route them through the same base so they follow the fontSize setting too.
612
+ // Sent messages share the reply metrics, including explicit UI Tweaks sizes.
655
613
  // `steering` messages are sent while the agent is busy (busyEnter: steer);
656
614
  // `[data-pending-steering]` is their in-flight bubble before it becomes durable.
657
- rules.push(`[data-chat-flow-kind="user"] [class^="_text_"],[data-chat-flow-kind="steering"] [class^="_text_"],[data-pending-steering] [class^="_text_"]{font-size:var(--dsw-font-markdown-base-font-size) !important}`)
658
- // Composer input follows the same base size. The visible text is rendered by
659
- // the hidden-textarea + backdrop/mirror pattern: the textarea is transparent,
660
- // the `data-input-backdrop` paints the text you see, and `data-input-mirror`
661
- // drives auto-grow. All three (plus the placeholder) must share the size.
662
- // Only font-size is touched: line-height is part of the auto-grow/caret
663
- // metrics, so overriding it misplaces the caret.
664
- rules.push(`[data-composer-card="true"] textarea,[data-composer-card="true"] [data-input-backdrop],[data-composer-card="true"] [data-input-mirror]{font-size:var(--dsw-font-markdown-base-font-size) !important}`)
665
- rules.push(`[data-composer-card="true"] textarea::placeholder{font-size:var(--dsw-font-markdown-base-font-size) !important}`)
666
- // Markdown table cells are pinned by DSH (15px); let every table style follow
667
- // the fontSize setting as well.
668
- rules.push(`div[data-slot="conversation.chat.node"] table th,div[data-slot="conversation.chat.node"] table td{font-size:var(--dsw-font-markdown-base-font-size) !important}`)
669
- // Inline code is pinned by DSH to 0.875em of the surrounding text (it ignores
670
- // the code token); scale that em by how far the chosen code size sits from
671
- // the stock ratio (a 13px block at this body size).
672
- const stockCodeBlock = value.fontSize * (13 / 16)
615
+ rules.push(`[data-chat-flow-kind="user"] [class*="_bubble"],[data-chat-flow-kind="steering"] [class*="_bubble"],[data-pending-steering] [class*="_bubble"]{font-size:var(--dsw-font-markdown-base-font-size) !important;line-height:var(--dsw-font-markdown-base-line-height) !important}`)
616
+ // Current contenteditable composers and older textarea composers inherit
617
+ // one size/leading pair, including placeholders and measurement layers.
618
+ rules.push(`[data-composer-card]{font:var(--dsw-font-markdown-base) !important}`)
619
+ // Tables use the same body size and leading in both table styles.
620
+ rules.push(`div[data-slot="conversation.chat.node"] [class*="_markdown"]:not([data-markdown-variant="compact"]) table th,div[data-slot="conversation.chat.node"] [class*="_markdown"]:not([data-markdown-variant="compact"]) table td{font-size:var(--dsw-font-markdown-base-font-size) !important;line-height:var(--dsw-font-markdown-base-line-height) !important}`)
621
+ // Inline code normally uses 0.92em. Explicit code sizing follows the
622
+ // same ratio relative to the 14px body / 13px code baseline.
623
+ const stockCodeBlock = value.fontSize * (13 / 14)
673
624
  if (Math.abs(value.codeFontSize - stockCodeBlock) > 0.5) {
674
- const em = (0.875 * (value.codeFontSize / stockCodeBlock)).toFixed(3)
675
- rules.push(`div[data-slot="conversation.chat.node"] div[class*="_markdown_"] :not(pre)>code{font-size:${em}em !important}`)
625
+ const em = (0.92 * (value.codeFontSize / stockCodeBlock)).toFixed(3)
626
+ rules.push(`div[data-slot="conversation.chat.node"] div[class*="_markdown"]:not([data-markdown-variant="compact"]) :not(pre)>code{font-size:${em}em !important}`)
676
627
  }
677
- // 行高: the base vertical rhythm of the reply area. When it differs from
678
- // the stock 16px, every vertical spacing scales from it proportionally:
679
- // ① gaps between message rows (Think ↔ tool cards like pwsh, user ↔
680
- // assistant) — the chat flow column;
681
- // ② gaps between content blocks inside one assistant reply (Think ↔ text) —
682
- // the Assistant Markdown block container (`Sxvs8a_body`, CSS-module
683
- // hashed, stable within the pinned rc.8 conversation package);
684
- // ③ the markdown typography rhythm: paragraphs (`p` has a stock `margin:
685
- // 16px 0` that must be overridden on both axes, not just bottom, or it
686
- // can never tighten below 16), list margins, list-item gaps (stock 6px),
687
- // paragraphs inside list items (stock 8px), code blocks, headings and
688
- // rules — all scaled from the 行高 unit.
689
- // Text line-height itself scales in buildFontCss above.
628
+ // Custom spacing scales prose without expanding compact thinking.
690
629
  if (value.lineHeight !== DEFAULT_LINE_HEIGHT) {
691
630
  const n = value.lineHeight
692
- const md = `div[data-slot="conversation.chat.node"] div[class*="_markdown_"]`
693
- const liGap = Math.max(2, Math.round((6 * n) / DEFAULT_LINE_HEIGHT))
631
+ const md = `div[data-slot="conversation.chat.node"] div[class*="_markdown"]:not([data-markdown-variant="compact"])`
632
+ const liGap = Math.max(2, Math.round((4 * n) / DEFAULT_LINE_HEIGHT))
694
633
  const liPGap = Math.max(2, Math.round((8 * n) / DEFAULT_LINE_HEIGHT))
695
- rules.push(`div[data-slot="conversation.view"] .Md3f7G_column{gap:${n}px !important}`)
696
- rules.push(`div[data-slot="conversation.chat.node"] .Sxvs8a_body{gap:${n}px !important}`)
697
- rules.push(`${md} p{margin:${n}px 0 !important}`)
698
- rules.push(`${md} ul,${md} ol{margin:${n}px 0 !important}`)
634
+ rules.push(`div[data-slot="conversation.view"] [data-chat-flow]{gap:${n}px !important}`)
635
+ rules.push(`div[data-slot="conversation.chat.node"] [data-assistant-body]{--dsh-assistant-block-gap:${n}px}`)
636
+ rules.push(`${md} p{margin:${Math.round(0.75 * n)}px 0 !important}`)
637
+ rules.push(`${md} ul,${md} ol{margin:${Math.round(0.75 * n)}px 0 !important}`)
699
638
  rules.push(`${md} pre{margin:${n}px 0 !important}`)
700
639
  // Code blocks: the pre's inner vertical padding is stock 16px (shiki
701
640
  // default); scale it so the block's own height follows 行高 too. Only
@@ -711,13 +650,11 @@ function buildRuntimeCss(value: ResolvedTweaks): string {
711
650
  }
712
651
  rules.push(`${md} li:not(:first-child){margin-top:${liGap}px !important}`)
713
652
  rules.push(`${md} li>p{margin:${liPGap}px 0 !important}`)
714
- rules.push(`${md} h1,${md} h2,${md} h3{margin:${2 * n}px 0 ${n}px !important}`)
653
+ rules.push(`${md} h1,${md} h2,${md} h3{margin:${Math.round(1.5 * n)}px 0 ${Math.round(0.75 * n)}px !important}`)
715
654
  rules.push(`${md} h4,${md} h5,${md} h6{margin:${n}px 0 !important}`)
716
655
  rules.push(`${md} hr{margin:${2 * n}px 0 !important}`)
717
656
  rules.push(`${md} blockquote{margin:${n}px 0 0 !important}`)
718
- // Keep DSH's flush edges: the first/last block of a reply has zero outer
719
- // margin (stock `>*:first-child` / `>*:last-child`), so a message starts
720
- // and ends tight and the spacing to the next node stays the column gap.
657
+ // Preserve the first and last block's flush edges.
721
658
  rules.push(`${md} > :first-child{margin-top:0 !important}`)
722
659
  rules.push(`${md} > :last-child{margin-bottom:0 !important}`)
723
660
  }
@@ -0,0 +1,73 @@
1
+ /** Compact sidebar chrome; the host retains every button, shortcut and drag surface. */
2
+ export const SIDEBAR_HEADER_CSS = `
3
+ .dut-sidebar-header{height:44px !important;padding:0 4px !important;margin-bottom:8px !important;gap:8px !important}
4
+ .dut-sidebar-brand{min-width:0 !important;background:transparent !important;box-shadow:none !important}
5
+ .dut-sidebar-identity{height:24px !important;gap:8px !important}
6
+ .dut-sidebar-wordmark{font:var(--dsw-font-s-strong-14) !important;letter-spacing:-.01em !important;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
7
+ .dut-sidebar-toggle{width:28px !important;height:28px !important;border-radius:8px !important;color:var(--dsw-alias-label-secondary) !important}
8
+ .dut-sidebar-toggle:hover{background:var(--dsw-alias-interactive-bg-hover) !important}
9
+ /* Reuse the original ChatCode primary-action red, scoped to this marked CTA. */
10
+ .dut-sidebar-new-session{height:40px !important;justify-content:flex-start !important;gap:8px !important;margin:0 0 10px !important;padding:8px 10px !important;border:1px solid #9B0B09 !important;border-radius:10px !important;background:linear-gradient(135deg,#9B0B09,#b71916) !important;color:#fff !important;font:var(--dsw-font-s-strong-14) !important;font-weight:600 !important;box-shadow:none !important;transform:none !important}
11
+ .dut-sidebar-new-session:hover{background:linear-gradient(135deg,#b71916,#cf302c) !important}
12
+ .dut-sidebar-new-session:active{background:#9B0B09 !important}
13
+ .dut-sidebar-new-session:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
14
+ .dut-sidebar-new-session > span:first-child{flex:1;min-width:0;overflow:hidden;mask-image:none !important}
15
+ .dut-sidebar-new-session > span:first-child > span{width:auto !important;justify-content:flex-start !important;gap:8px !important;min-width:0}
16
+ .dut-sidebar-new-session > span:first-child > span > svg{width:16px;height:16px;flex:none;color:inherit}
17
+ .dut-sidebar-new-session > span:first-child > span > span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
18
+ .dut-sidebar-new-session > span + span{opacity:1 !important;flex:none;color:color-mix(in srgb,currentColor 75%,transparent);font:var(--dsw-font-xxs-12)}
19
+ /* Temporary navigation-only hide. Remove this block to restore Plugins;
20
+ registrations, settings, installed plugins and other panel entries stay intact. */
21
+ nav:is([aria-label="全局面板"],[aria-label="Global panels"]) > button:is([aria-label="插件"],[aria-label="Plugins"]),
22
+ nav:is([aria-label="全局面板"],[aria-label="Global panels"]):has(> button:is([aria-label="插件"],[aria-label="Plugins"]):only-child){display:none !important}
23
+ `
24
+
25
+ const HEADER_CLASSES = [
26
+ 'dut-sidebar-header', 'dut-sidebar-brand', 'dut-sidebar-identity',
27
+ 'dut-sidebar-wordmark', 'dut-sidebar-toggle', 'dut-sidebar-new-session',
28
+ ] as const
29
+ type HeaderClass = typeof HEADER_CLASSES[number]
30
+
31
+ /** Mark only the expanded brand row and its adjacent native New Session button. */
32
+ export function syncSidebarHeader(): void {
33
+ const desired = new Map<HTMLElement, Set<HeaderClass>>()
34
+ const mark = (element: Element | null | undefined, className: HeaderClass): void => {
35
+ if (!(element instanceof HTMLElement)) return
36
+ const classes = desired.get(element) ?? new Set<HeaderClass>()
37
+ classes.add(className)
38
+ desired.set(element, classes)
39
+ }
40
+ for (const name of document.querySelectorAll<HTMLElement>('.dut-sidebar-brand-name,.dut-brand-title')) {
41
+ const row = name.closest<HTMLElement>('[data-window-drag]')
42
+ const logo = row?.querySelector('.dut-sidebar-logo,.dut-left-logo')
43
+ if (!row || !logo) continue
44
+ const brand = [...row.children].find(child => child.contains(name) && child.contains(logo))
45
+ if (!brand) continue
46
+ mark(row, 'dut-sidebar-header')
47
+ mark(brand, 'dut-sidebar-brand')
48
+ mark(logo.closest('[class*="brandIdentity"]'), 'dut-sidebar-identity')
49
+ mark(name, 'dut-sidebar-wordmark')
50
+ for (const button of row.querySelectorAll('button')) {
51
+ if (!brand.contains(button) && button !== brand) mark(button, 'dut-sidebar-toggle')
52
+ }
53
+ // The host renders this native action directly after the drag row. Do
54
+ // not infer a new action from arbitrary text in another panel or dialog.
55
+ const next = row.nextElementSibling
56
+ if (next instanceof HTMLButtonElement) mark(next, 'dut-sidebar-new-session')
57
+ }
58
+ // A collapsed rail no longer has a wordmark. Remove expanded-only styling
59
+ // without changing the host's rail geometry or its logo/expand interaction.
60
+ for (const className of HEADER_CLASSES) {
61
+ for (const element of document.querySelectorAll<HTMLElement>(`.${className}`)) {
62
+ if (!desired.get(element)?.has(className)) element.classList.remove(className)
63
+ }
64
+ }
65
+ for (const [element, classes] of desired) element.classList.add(...classes)
66
+ }
67
+
68
+ /** Restore native header styling when the branding effect is disposed. */
69
+ export function clearSidebarHeader(): void {
70
+ for (const className of HEADER_CLASSES) {
71
+ for (const element of document.querySelectorAll(`.${className}`)) element.classList.remove(className)
72
+ }
73
+ }
@@ -0,0 +1,38 @@
1
+ /** Font overrides for the conversation, preserving the live theme families. */
2
+
3
+ /**
4
+ * Apply the UI Tweaks size and spacing preferences to the host's prose tokens.
5
+ * @param fontSize - Selected body size in pixels.
6
+ * @param lineHeight - Vertical-spacing unit; 16 retains the normal rhythm.
7
+ * @param codeFontSize - Selected code-block size in pixels.
8
+ * @returns Theme-token overrides for a runtime stylesheet.
9
+ */
10
+ export function buildFontCss(fontSize: number, lineHeight: number, codeFontSize: number): string {
11
+ const parts: string[] = []
12
+ const base = 'var(--dsw-font-markdown-base-font-family, var(--dsw-font-family))'
13
+ const code = 'var(--dsw-font-markdown-code-block-font-family, var(--ds-font-family-code))'
14
+ const token = (name: string, size: number, leading: number, family: string, weight = 400, style = 'normal'): void => {
15
+ const line = Math.max(12, Math.round(leading * (lineHeight / 16)))
16
+ parts.push(`--${name}:${style} ${weight} ${size}px/${line}px ${family}`)
17
+ parts.push(`--${name}-font-size:${size}px`)
18
+ parts.push(`--${name}-line-height:${line}px`)
19
+ parts.push(`--${name}-font-weight:${weight}`)
20
+ parts.push(`--${name}-font-style:${style}`)
21
+ }
22
+ const bodyLine = fontSize + 10
23
+ token('dsw-font-markdown-base', fontSize, bodyLine, base)
24
+ token('dsw-font-markdown-base-strong', fontSize, bodyLine, base, 600)
25
+ token('dsw-font-markdown-base-italic', fontSize, bodyLine, base, 400, 'italic')
26
+ token('dsw-font-markdown-base-strong-italic', fontSize, bodyLine, base, 600, 'italic')
27
+ token('dsw-font-markdown-h1', fontSize + 4, bodyLine + 4, base, 600)
28
+ token('dsw-font-markdown-h2', fontSize + 2, bodyLine + 2, base, 600)
29
+ token('dsw-font-markdown-h3', fontSize + 1, bodyLine + 1, base, 600)
30
+ token('dsw-font-markdown-h4', fontSize, bodyLine, base, 600)
31
+ token('dsw-font-markdown-table', fontSize, bodyLine, base)
32
+ token('dsw-font-markdown-table-head', fontSize, bodyLine, base, 500)
33
+ token('dsw-font-markdown-code', codeFontSize, codeFontSize + 7,
34
+ 'var(--dsw-font-markdown-code-font-family, var(--ds-font-family-code))')
35
+ token('dsw-font-markdown-code-block', codeFontSize, codeFontSize + 7, code)
36
+ token('dsw-font-markdown-code-block-small', Math.max(8, codeFontSize - 2), codeFontSize + 3, code)
37
+ return `body{${parts.join(';')}}`
38
+ }
package/src/index.ts CHANGED
@@ -54,6 +54,7 @@ import { CodingPlanCatalogue } from './codingplan.ts'
54
54
  import { checkOneUpdate, CHATCODE_UPDATE_PACKAGES, performAllUpdates } from './chatcode-update.ts'
55
55
  import { readJsonBody, requireString, SidebarError, writeError, writeJson, writeOk } from './wire.ts'
56
56
  import { apply as applyUiTweaksHost } from './ui-tweaks/index.ts'
57
+ import { buildPluginMarketApi } from './plugin-market-api.ts'
57
58
 
58
59
  export { Config }
59
60
  export type { SidebarConfig, ResolvedSidebarConfig }
@@ -491,6 +492,7 @@ function buildApi(
491
492
  const { sessionId, cwd } = await cwdOf(payload)
492
493
  return { sessionId, cwd, root: rootLabel(cwd), parent: parentOf(cwd) ?? null }
493
494
  },
495
+ ...buildPluginMarketApi(ctx, cwdOf),
494
496
  'fs.tree': async (payload) => {
495
497
  const { cwd } = await cwdOf(payload)
496
498
  const record = payload as { path?: unknown }