@chatcode/cco-better-sidebar 0.18.0-alpha.1 → 0.18.0-alpha.3

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.
@@ -0,0 +1,4 @@
1
+ /** Build the local ChatCode login gate URL while preserving the current page. */
2
+ export declare function chatcodeLoginRoute(location: Pick<Location, 'pathname' | 'search' | 'hash'>): string;
3
+ /** Enter the existing local login flow after account state cannot be read. */
4
+ export declare function redirectToChatcodeLogin(location?: Location): void;
@@ -0,0 +1,11 @@
1
+ import type { Context } from '../context-types.ts';
2
+ /**
3
+ * Read the session selected by the host's scoped-session adapter.
4
+ *
5
+ * Newer hosts deliberately removed `sessions.list.current`: selection is UI
6
+ * scope state, not catalog state. The adapter is authoritative on both old
7
+ * and new hosts and tracks main-view retain/release transitions.
8
+ */
9
+ export declare function currentSessionId(ctx: Context): string | undefined;
10
+ /** React subscription to the same authoritative current-session source. */
11
+ export declare function useCurrentSessionId(ctx: Context): string | undefined;
@@ -4,6 +4,8 @@ import type { SidebarStore } from './state.ts';
4
4
  export declare function pinnedSessionIdsOf(value: {
5
5
  pluginSettings: Record<string, Record<string, unknown>>;
6
6
  }): string[];
7
+ /** Open through the Host's navigation policy, not the pure Session catalog. */
8
+ export declare function openPinnedSession(ctx: Context, sessionId: string): void;
7
9
  /**
8
10
  * Mount the separate pinned group and append a fourth item to DSH's native
9
11
  * three-item row menu. The group is inserted beside (not inside) the host
@@ -15,6 +15,11 @@ export declare const USAGE_NAV_MARKER = "data-dsh-better-sidebar-usage-nav";
15
15
  export declare const AI_RATIO_NAV_MARKER = "data-dsh-better-sidebar-ai-ratio-nav";
16
16
  /** Marker of the "更新" (update) settings nav row. */
17
17
  export declare const UPDATE_NAV_MARKER = "data-dsh-better-sidebar-update-nav";
18
+ /** Fallback markers for host/plugin settings rows whose icons mount late. */
19
+ export declare const GENERAL_NAV_MARKER = "data-dsh-better-sidebar-general-nav";
20
+ export declare const PLUGIN_MARKET_NAV_MARKER = "data-dsh-better-sidebar-plugin-market-nav";
21
+ export declare const UI_TWEAKS_NAV_MARKER = "data-dsh-better-sidebar-ui-tweaks-nav";
22
+ export declare const SKILL_MARKET_NAV_MARKER = "data-dsh-better-sidebar-skill-market-nav";
18
23
  /**
19
24
  * Keep the marker on the settings-nav button whose visible text is this
20
25
  * plugin's current localized section label.
@@ -24,4 +29,4 @@ export declare const UPDATE_NAV_MARKER = "data-dsh-better-sidebar-update-nav";
24
29
  * other's markers).
25
30
  * @returns disposer that disconnects observation and removes owned markers.
26
31
  */
27
- export declare function registerSettingsNavIcon(label: () => string, marker?: string): () => void;
32
+ export declare function registerSettingsNavIcon(label: () => string | readonly string[], marker?: string): () => void;
@@ -40,6 +40,6 @@ export declare function SidebarProjectLogo({ size }: {
40
40
  }): ReactNode;
41
41
  /** Expanded-sidebar wordmark supplied through the host's dedicated slot. */
42
42
  export declare function SidebarProjectName(): ReactNode;
43
- export declare const BRANDING_CSS = "\n/* Keep the light-theme type hierarchy in ChatCode's restrained red family,\n while leaving every surface and interaction state (including workspace /\n sidebar hover and selected fills, menus, the composer and shadows) owned\n by the host theme. */\nbody:not([data-ds-dark-theme]){\n --dsw-alias-label-primary:#301111;\n --dsw-alias-label-secondary:#754745;\n --dsw-alias-label-tertiary:#a77471;\n}\n/* Saturated brand red remains exclusive to explicitly marked primary actions. */\nbody:not([data-ds-dark-theme]) .dut-unicom-primary-action{background:linear-gradient(135deg,#9B0B09,#b71916) !important;border-color:#9B0B09 !important;color:#fff !important;box-shadow:0 7px 18px rgba(155,11,9,.24) !important}\nbody:not([data-ds-dark-theme]) .dut-unicom-primary-action:hover{background:linear-gradient(135deg,#b71916,#cf302c) !important;border-color:#b71916 !important;color:#fff !important;box-shadow:0 10px 24px rgba(155,11,9,.30) !important}\nbody:not([data-ds-dark-theme]) .dut-unicom-primary-action:active{transform:translateY(1px);box-shadow:0 4px 11px rgba(155,11,9,.22) !important}\n/* In dark mode, alter only the resting background of primary buttons. Once\n hovered, the host owns the complete interaction treatment again. */\nbody[data-ds-dark-theme] .dut-unicom-primary-action:not(:hover){background:#9B0B09 !important}\n.dut-project-logo{display:block;object-fit:contain}\n.dut-left-logo{width:44px !important;height:44px !important}\n.dut-brand-title{font-size:18px !important;font-weight:600 !important}\n.dut-sidebar-logo{width:44px !important;height:44px !important}\n/* The alpha sidebar keeps its slotted identity at 24px and clips the brand\n button. Give only our expanded identity the full artwork height so the\n 44px knot is not cropped by that host-owned overflow boundary. */\n[class*=\"brandIdentity\"]:has(.dut-sidebar-logo){height:44px !important}\n.dut-sidebar-brand-name{font-size:18px !important;line-height:22px !important;font-weight:600 !important;letter-spacing:0 !important;white-space:nowrap}\n.dut-brand-version{display:none !important}\n.dut-hero-preview{display:none !important}\n.dut-hero-logo{width:60px !important;height:60px !important}\n[class*=\"headline\"]:has(.dut-hero-logo){grid-template-columns:60px auto !important;font-size:26px !important;line-height:34px !important}\n.dut-settings-logo{width:19px;height:19px;object-fit:contain}\n";
43
+ export declare const BRANDING_CSS = "\n/* ChatCode CLI branding is deliberately limited to primary actions. Every\n other surface and interaction state (including workspace/sidebar rows,\n menus, the composer, text and shadows) remains owned by the host theme. */\nbody:not([data-ds-dark-theme]) .dut-unicom-primary-action{background:linear-gradient(135deg,#9B0B09,#b71916) !important;border-color:#9B0B09 !important;color:#fff !important;box-shadow:0 7px 18px rgba(155,11,9,.24) !important}\nbody:not([data-ds-dark-theme]) .dut-unicom-primary-action:hover{background:linear-gradient(135deg,#b71916,#cf302c) !important;border-color:#b71916 !important;color:#fff !important;box-shadow:0 10px 24px rgba(155,11,9,.30) !important}\nbody:not([data-ds-dark-theme]) .dut-unicom-primary-action:active{transform:translateY(1px);box-shadow:0 4px 11px rgba(155,11,9,.22) !important}\n/* In dark mode, alter only the resting background of primary buttons. Once\n hovered, the host owns the complete interaction treatment again. */\nbody[data-ds-dark-theme] .dut-unicom-primary-action:not(:hover){background:#9B0B09 !important}\n.dut-project-logo{display:block;object-fit:contain}\n.dut-left-logo{width:44px !important;height:44px !important}\n.dut-brand-title{font-size:18px !important;font-weight:600 !important}\n.dut-sidebar-logo{width:44px !important;height:44px !important}\n/* The alpha sidebar keeps its slotted identity at 24px and clips the brand\n button. Give only our expanded identity the full artwork height so the\n 44px knot is not cropped by that host-owned overflow boundary. */\n[class*=\"brandIdentity\"]:has(.dut-sidebar-logo){height:44px !important}\n.dut-sidebar-brand-name{font-size:18px !important;line-height:22px !important;font-weight:600 !important;letter-spacing:0 !important;white-space:nowrap}\n.dut-brand-version{display:none !important}\n.dut-hero-preview{display:none !important}\n.dut-hero-logo{width:60px !important;height:60px !important}\n.dut-hero-title{font-size:30px !important;line-height:40px !important;font-weight:600 !important;letter-spacing:.06em !important;white-space:nowrap}\n[class*=\"headline\"]:has(.dut-hero-logo){grid-template-columns:60px auto !important;font-size:30px !important;line-height:40px !important}\n.dut-settings-logo{width:19px;height:19px;object-fit:contain}\n";
44
44
  /** Install the brand theme and retain it across host navigation/rerenders. */
45
45
  export declare function installBranding(): () => void;
@@ -289,7 +289,8 @@ export interface SidebarSessionPersistenceService {
289
289
  }
290
290
  /** The client session list snapshot the sidebar subscribes to. */
291
291
  export interface SidebarSessionList {
292
- current: string | undefined;
292
+ /** @deprecated Selection moved to `uiSession.adapter.current`; old hosts may still expose this field. */
293
+ current?: string;
293
294
  byId: Record<string, SidebarSessionSummary>;
294
295
  /** Direct durable catalogs keyed by their selected parent address. */
295
296
  subagentsByParent?: Readonly<Record<string, SidebarSubagentCatalog>>;
@@ -306,11 +307,6 @@ export interface SidebarSessionsService {
306
307
  getSnapshot(): SidebarSessionList;
307
308
  subscribe(fn: () => void): () => void;
308
309
  };
309
- /**
310
- * Select a listed session as current (mirror of the runtime ISessions.open)
311
- * — used to jump back to the main agent from the topology root node.
312
- */
313
- open?(id: string): void;
314
310
  /**
315
311
  * Fork a session from a completed-turn prefix of the source and resolve
316
312
  * the child session id (mirror of the runtime ISessions.fork — throws on
@@ -356,6 +352,17 @@ export interface SidebarSessionsService {
356
352
  */
357
353
  refreshSubagents?(parentSessionId: string): Promise<void>;
358
354
  }
355
+ /** The host's authoritative current Session scope binding. */
356
+ export interface SidebarUiSessionService {
357
+ adapter: {
358
+ current: {
359
+ getSnapshot(): {
360
+ readonly key: string | undefined;
361
+ };
362
+ subscribe(fn: () => void): () => void;
363
+ };
364
+ };
365
+ }
359
366
  /**
360
367
  * The client locale service face (mirror of @deepseek-ai/dsh-client-locale's
361
368
  * LocaleRuntime — only the slices the sidebar touches). The sidebar follows
@@ -401,6 +408,15 @@ export interface SidebarWorkspacesService {
401
408
  /** Open a filesystem path with the Host operating system's default application. */
402
409
  openPath(path: string): Promise<void>;
403
410
  }
411
+ /**
412
+ * The client Workspace navigation face. Session selection is a UI navigation
413
+ * operation in current DSH builds: it retains the target for the main view,
414
+ * updates the scoped Session adapter, and reveals the conversation panel.
415
+ */
416
+ export interface SidebarUiWorkspaceService {
417
+ /** Select a Session and show its Conversation, including persisted history. */
418
+ openSession(sessionId: string): void;
419
+ }
404
420
  /**
405
421
  * The invariant service face (mirror of @deepseek-ai/dsh-invariants'
406
422
  * InvariantRegistry). The upstream augmentation does not reach this Context
@@ -475,12 +491,16 @@ export interface SidebarContextShape {
475
491
  webServer: SidebarWebServer;
476
492
  /** The session store (host `.get`) and the client list feed (`.list`) faces. */
477
493
  sessions: SidebarSessionStore & SidebarSessionsService;
494
+ /** Current UI Session scope; selection no longer belongs to the Session list catalog. */
495
+ uiSession: SidebarUiSessionService;
478
496
  /** The web runtime trust list (bind-derived). */
479
497
  webRuntime: SidebarWebRuntime;
480
498
  /** The client slot registry (register/inject). */
481
499
  slots: SidebarSlotsService;
482
500
  /** The client workspaces service face (file-open funnel). */
483
501
  workspaces: SidebarWorkspacesService;
502
+ /** The client Workspace navigation service (session-open funnel). */
503
+ uiWorkspace: SidebarUiWorkspaceService;
484
504
  /** The settings service face (prefs persistence + namespace reads). */
485
505
  settings: SidebarSettingsService;
486
506
  /** The invariant registry face. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatcode/cco-better-sidebar",
3
- "version": "0.18.0-alpha.1",
3
+ "version": "0.18.0-alpha.3",
4
4
  "description": "ChatCode CLI Web plugin: a VS Code-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes a service for other plugins to register sidebar tabs and file viewers.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -10,6 +10,7 @@
10
10
  import { useEffect, useRef, useState } from 'react'
11
11
  import { api } from './api.ts'
12
12
  import { useChatcodeAccount } from './useChatcodeAccount.ts'
13
+ import { redirectToChatcodeLogin } from './chatcode-login-route.ts'
13
14
  import { t } from './locales.ts'
14
15
  import css from './sidebar.module.css'
15
16
 
@@ -93,6 +94,10 @@ export function AccountMenu(): JSX.Element {
93
94
  return () => { document.removeEventListener('pointerdown', onPointerDown) }
94
95
  }, [])
95
96
 
97
+ useEffect(() => {
98
+ if (error === 'status') redirectToChatcodeLogin()
99
+ }, [error])
100
+
96
101
  // The host has no slot for replacing this one closed-shell control. Keep
97
102
  // the original fully recoverable: its inline style is restored on disposal
98
103
  // and it remains the target invoked by our Settings menu item.
@@ -127,7 +132,8 @@ export function AccountMenu(): JSX.Element {
127
132
  const signedIn = chatcode?.validation === 'valid' && chatcode.expired !== true
128
133
  // A saved credential remains usable for the next check when the company
129
134
  // network is unavailable. Do not turn that transient state into a login CTA.
130
- const canStartChatcodeLogin = chatcode?.available === true && !signedIn
135
+ const hasChatcodeAccount = chatcode?.available === true && chatcode.configured === true
136
+ const canStartChatcodeLogin = chatcode?.available === true && !hasChatcodeAccount
131
137
  && !(chatcode.configured === true && chatcode.validation === 'unavailable')
132
138
 
133
139
  return (
@@ -144,15 +150,15 @@ export function AccountMenu(): JSX.Element {
144
150
  <SettingsIcon />
145
151
  {t('accountMenuSettings')}
146
152
  </button>
147
- {error && <div role="alert" className={css.accountItem}>{t(error === 'status' ? 'accountMenuChatcodeStatusError' : 'accountMenuChatcodeError')}</div>}
153
+ {error === 'operation' && <div role="alert" className={css.accountItem}>{t('accountMenuChatcodeError')}</div>}
148
154
  {(chatcode?.login?.state === 'failed' || chatcode?.login?.state === 'timed-out') && <div role="alert" className={css.accountItem}>{t('accountMenuChatcodeRetry')}</div>}
149
155
  {loginUrl && <a data-dsh-external="true" className={css.accountItem} href={loginUrl} target="_blank" rel="noopener noreferrer" onClick={event => { event.stopPropagation() }}>{t('accountMenuChatcodeOpen')}</a>}
150
156
  {chatcode?.available === true && <>
151
157
  {canStartChatcodeLogin && <button type="button" role="menuitem" className={css.accountItem} disabled={busy} onClick={startLogin}>
152
158
  <LogoutIcon />{busy ? t('accountMenuChatcodeStarting') : t('accountMenuChatcodeLogin')}
153
159
  </button>}
154
- {chatcode.configured && <button type="button" role="menuitem" className={css.accountItem} disabled={busy} onClick={logoutChatcode}>
155
- <LogoutIcon />{t('accountMenuChatcodeLogout')}
160
+ {hasChatcodeAccount && <button type="button" role="menuitem" className={css.accountItem} disabled={busy} onClick={logoutChatcode}>
161
+ <LogoutIcon />{t('accountMenuLogout')}
156
162
  </button>}
157
163
  </>}
158
164
  {chatcode?.available === false && <button type="button" role="menuitem" className={css.accountItem} onClick={logout}>
@@ -2,6 +2,7 @@
2
2
  import { useEffect, useRef } from 'react'
3
3
  import type { ChatCodeAuthState } from './api.ts'
4
4
  import { useChatcodeAccount } from './useChatcodeAccount.ts'
5
+ import { redirectToChatcodeLogin } from './chatcode-login-route.ts'
5
6
 
6
7
  type GateDecision = 'allow' | 'login' | 'wait'
7
8
 
@@ -28,8 +29,7 @@ export function ChatcodeGate(): JSX.Element | null {
28
29
  // this visit has passed that check, a later company-network outage does
29
30
  // not interrupt ongoing work.
30
31
  if (chatcodeGateDecision(chatcode, verifiedThisVisit.current) !== 'login') return
31
- const back = `${window.location.pathname}${window.location.search}${window.location.hash}`
32
- window.location.replace(`/sidebar/login?return=${encodeURIComponent(back)}`)
32
+ redirectToChatcodeLogin()
33
33
  }, [chatcode])
34
34
  return null
35
35
  }
@@ -64,6 +64,7 @@ import { OrphanedTab } from './OrphanedTab.tsx'
64
64
  import { RenderBoundary } from './RenderBoundary.tsx'
65
65
  import { tabContentCompare, type TabContentMemoKey } from './tab-content-memo.ts'
66
66
  import { t } from './locales.ts'
67
+ import { useCurrentSessionId } from './current-session.ts'
67
68
  import { api, type SessionScope } from './api.ts'
68
69
  import { AccountMenu } from './AccountMenu.tsx'
69
70
  import css from './sidebar.module.css'
@@ -322,7 +323,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
322
323
  useMemo(() => (callback: () => void) => ctx.sessions.list.subscribe(callback), [ctx]),
323
324
  useCallback(() => ctx.sessions.list.getSnapshot(), [ctx]),
324
325
  )
325
- const current = sessionList.current
326
+ const current = useCurrentSessionId(ctx)
326
327
 
327
328
  // Per-session sidebar state.
328
329
  const snapshot = useSyncExternalStore(
@@ -1385,6 +1386,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
1385
1386
  // focus is the only way the existing Tooltip can explain what is missing.
1386
1387
  return (
1387
1388
  <div data-dsh-panel-host {...osFileDragShield}>
1389
+ <AccountMenu />
1388
1390
  {nativeToolbarHost !== null ? createPortal(
1389
1391
  <div className={clsx(css.toggleCluster, css.toggleClusterNative)} data-dsh-toggle-cluster data-dsh-native-toolbar-cluster>
1390
1392
  {!narrow && (
@@ -0,0 +1,11 @@
1
+ /** Build the local ChatCode login gate URL while preserving the current page. */
2
+ export function chatcodeLoginRoute(location: Pick<Location, 'pathname' | 'search' | 'hash'>): string {
3
+ const back = `${location.pathname}${location.search}${location.hash}`
4
+ return `/sidebar/login?return=${encodeURIComponent(back)}`
5
+ }
6
+
7
+ /** Enter the existing local login flow after account state cannot be read. */
8
+ export function redirectToChatcodeLogin(location: Location = window.location): void {
9
+ if (location.pathname === '/sidebar/login') return
10
+ location.replace(chatcodeLoginRoute(location))
11
+ }
@@ -0,0 +1,22 @@
1
+ import { useCallback, useSyncExternalStore } from 'react'
2
+ import type { Context } from '../context-types.ts'
3
+
4
+ /**
5
+ * Read the session selected by the host's scoped-session adapter.
6
+ *
7
+ * Newer hosts deliberately removed `sessions.list.current`: selection is UI
8
+ * scope state, not catalog state. The adapter is authoritative on both old
9
+ * and new hosts and tracks main-view retain/release transitions.
10
+ */
11
+ export function currentSessionId(ctx: Context): string | undefined {
12
+ return ctx.uiSession.adapter.current.getSnapshot().key
13
+ }
14
+
15
+ /** React subscription to the same authoritative current-session source. */
16
+ export function useCurrentSessionId(ctx: Context): string | undefined {
17
+ const source = ctx.uiSession.adapter.current
18
+ return useSyncExternalStore(
19
+ useCallback(listener => source.subscribe(listener), [source]),
20
+ useCallback(() => source.getSnapshot().key, [source]),
21
+ )
22
+ }
@@ -22,7 +22,16 @@ import { RenderBoundary } from './RenderBoundary.tsx'
22
22
  import { registerOpenPathInterception, registerTurnTailInterception } from './intercept.tsx'
23
23
  import { registerLinkInterception } from './link-intercept.ts'
24
24
  import { registerImeGuard } from './ime-guard.ts'
25
- import { AI_RATIO_NAV_MARKER, registerSettingsNavIcon, UPDATE_NAV_MARKER, USAGE_NAV_MARKER } from './settings-nav-icon.ts'
25
+ import {
26
+ AI_RATIO_NAV_MARKER,
27
+ GENERAL_NAV_MARKER,
28
+ PLUGIN_MARKET_NAV_MARKER,
29
+ registerSettingsNavIcon,
30
+ SKILL_MARKET_NAV_MARKER,
31
+ UI_TWEAKS_NAV_MARKER,
32
+ UPDATE_NAV_MARKER,
33
+ USAGE_NAV_MARKER,
34
+ } from './settings-nav-icon.ts'
26
35
  import { installSessionPins } from './session-pins.tsx'
27
36
  import { registerChatPresentationCompatibility } from './chat-presentation.ts'
28
37
  import { loadExternalDisable, loadPrefs } from './prefs.ts'
@@ -43,7 +52,7 @@ import { apply as applyUiTweaksClient } from './ui-tweaks/index.tsx'
43
52
  * locale service backs the sidebar's copy — see locales.ts). `modules`
44
53
  * (rc.8+) is the client module system the chunk loader resolves its
45
54
  * externals through — Cordis guards service access without inject. */
46
- export const inject = ['slots', 'sessions', 'workspaces', 'locale', 'modules', 'commandUi', 'conversation']
55
+ export const inject = ['slots', 'sessions', 'uiSession', 'uiWorkspace', 'workspaces', 'locale', 'modules', 'commandUi', 'conversation']
47
56
 
48
57
  /**
49
58
  * Error boundary over the sidebar tree (root scope): a render error in the
@@ -455,6 +464,22 @@ export function apply(ctx: Context): void {
455
464
  () => registerSettingsNavIcon(() => t('updateNav'), UPDATE_NAV_MARKER),
456
465
  'dsh-better-sidebar: update settings navigation icon',
457
466
  )
467
+ ctx.effect(
468
+ () => registerSettingsNavIcon(() => ['通用设置', 'General'], GENERAL_NAV_MARKER),
469
+ 'dsh-better-sidebar: general settings navigation icon fallback',
470
+ )
471
+ ctx.effect(
472
+ () => registerSettingsNavIcon(() => ['插件市场', 'Plugin marketplace', 'Plugin Market'], PLUGIN_MARKET_NAV_MARKER),
473
+ 'dsh-better-sidebar: plugin market navigation icon fallback',
474
+ )
475
+ ctx.effect(
476
+ () => registerSettingsNavIcon(() => ['界面调整', 'UI Tweaks'], UI_TWEAKS_NAV_MARKER),
477
+ 'dsh-better-sidebar: UI tweaks navigation icon fallback',
478
+ )
479
+ ctx.effect(
480
+ () => registerSettingsNavIcon(() => ['技能市场', 'Skill marketplace', 'Skill Market'], SKILL_MARKET_NAV_MARKER),
481
+ 'dsh-better-sidebar: skill market navigation icon fallback',
482
+ )
458
483
 
459
484
  // The "Side card" settings section: appears in the DSH Settings shell
460
485
  // once the shell's declaration is on the ledger (slots.inject waits for
@@ -11,6 +11,7 @@ import { firstLeaf, revealPaths, togglePanel, type SidebarStore } from './state.
11
11
  import { t } from './locales.ts'
12
12
  import { resolveSidebarPath, selectProducedFiles } from './produced-files.ts'
13
13
  import { wrapOpenPath } from './openpath-intercept.ts'
14
+ import { currentSessionId } from './current-session.ts'
14
15
  import css from './sidebar.module.css'
15
16
 
16
17
  /** Open a file in the sidebar's editor (used by the intercepted row and the explorer). */
@@ -190,7 +191,7 @@ export function registerOpenPathInterception(ctx: Context, store: SidebarStore):
190
191
  takeoverEnabled: () => !store.getSuspended()
191
192
  && store.getPrefs().interceptOpenPath !== false
192
193
  && store.getPrefs().tabsEnabled['editor'] !== false,
193
- currentSessionId: () => ctx.sessions.list.getSnapshot().current,
194
+ currentSessionId: () => currentSessionId(ctx),
194
195
  openInSidebar: (path, sessionId) => { openSidebarFile(ctx, store, sessionId, path) },
195
196
  revealInExplorer: (_path, sessionId) => { revealInExplorer(ctx, store, sessionId, lastProduced) },
196
197
  })
@@ -94,6 +94,48 @@ body[data-dsh-sidebar-dragging] #root :has(> [data-slot="conversation"]) {
94
94
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 7v10'/%3E%3Cpath d='M6 5v14'/%3E%3Crect width='12' height='18' x='10' y='3' rx='2'/%3E%3C/svg%3E") center / contain no-repeat;
95
95
  }
96
96
 
97
+ /* Some host/plugin settings rows receive their SVG only after the global
98
+ plugin group is expanded. Paint stable fallbacks immediately so the nav
99
+ never opens with blank icon slots; a late host SVG is hidden to avoid a
100
+ duplicate. */
101
+ [data-dsh-better-sidebar-general-nav] > svg:first-child,
102
+ [data-dsh-better-sidebar-plugin-market-nav] > svg:first-child,
103
+ [data-dsh-better-sidebar-ui-tweaks-nav] > svg:first-child,
104
+ [data-dsh-better-sidebar-skill-market-nav] > svg:first-child {
105
+ display: none;
106
+ }
107
+
108
+ [data-dsh-better-sidebar-general-nav]::before,
109
+ [data-dsh-better-sidebar-plugin-market-nav]::before,
110
+ [data-dsh-better-sidebar-ui-tweaks-nav]::before,
111
+ [data-dsh-better-sidebar-skill-market-nav]::before {
112
+ content: '';
113
+ flex: none;
114
+ width: 16px;
115
+ height: 16px;
116
+ background: currentColor;
117
+ }
118
+
119
+ [data-dsh-better-sidebar-general-nav]::before {
120
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3Cpath d='M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9 7 7M17 17l2.1 2.1M19.1 4.9 17 7M7 17l-2.1 2.1'/%3E%3C/svg%3E") center / contain no-repeat;
121
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3Cpath d='M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9 7 7M17 17l2.1 2.1M19.1 4.9 17 7M7 17l-2.1 2.1'/%3E%3C/svg%3E") center / contain no-repeat;
122
+ }
123
+
124
+ [data-dsh-better-sidebar-plugin-market-nav]::before {
125
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='7' height='7' rx='1'/%3E%3Crect x='14' y='3' width='7' height='7' rx='1'/%3E%3Crect x='3' y='14' width='7' height='7' rx='1'/%3E%3Crect x='14' y='14' width='7' height='7' rx='1'/%3E%3C/svg%3E") center / contain no-repeat;
126
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='7' height='7' rx='1'/%3E%3Crect x='14' y='3' width='7' height='7' rx='1'/%3E%3Crect x='3' y='14' width='7' height='7' rx='1'/%3E%3Crect x='14' y='14' width='7' height='7' rx='1'/%3E%3C/svg%3E") center / contain no-repeat;
127
+ }
128
+
129
+ [data-dsh-better-sidebar-ui-tweaks-nav]::before {
130
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6'/%3E%3C/svg%3E") center / contain no-repeat;
131
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6'/%3E%3C/svg%3E") center / contain no-repeat;
132
+ }
133
+
134
+ [data-dsh-better-sidebar-skill-market-nav]::before {
135
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m12 3-1.7 4.3L6 9l4.3 1.7L12 15l1.7-4.3L18 9l-4.3-1.7Z'/%3E%3Cpath d='m5 15-.8 2.2L2 18l2.2.8L5 21l.8-2.2L8 18l-2.2-.8Z'/%3E%3Cpath d='m19 14-1 2.5-2.5 1L18 18.5l1 2.5 1-2.5 2.5-1-2.5-1Z'/%3E%3C/svg%3E") center / contain no-repeat;
136
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m12 3-1.7 4.3L6 9l4.3 1.7L12 15l1.7-4.3L18 9l-4.3-1.7Z'/%3E%3Cpath d='m5 15-.8 2.2L2 18l2.2.8L5 21l.8-2.2L8 18l-2.2-.8Z'/%3E%3Cpath d='m19 14-1 2.5-2.5 1L18 18.5l1 2.5 1-2.5 2.5-1-2.5-1Z'/%3E%3C/svg%3E") center / contain no-repeat;
137
+ }
138
+
97
139
  /* The "Token 用量" heatmap section's nav row: same marker scheme as above,
98
140
  painting a calendar glyph (Lucide calendar-days) — a month of days is the
99
141
  section's whole subject. */
@@ -24,7 +24,7 @@ export const ja: Record<string, string> = {
24
24
  updateSettingsDesc: 'ChatCode の CLI 版を npm の最新版へ更新します。',
25
25
  updateChecking: 'バージョン更新を確認しています…',
26
26
  updatePerforming: '最新バージョンへ更新しています…',
27
- updateUpToDate: 'すべて最新です。',
27
+ updateUpToDate: 'すでに最新です。',
28
28
  updateNow: '今すぐ更新',
29
29
  updateConfirm: 'これらのパッケージを最新版へ更新します。反映には再起動が必要です。',
30
30
  updateRestartHint: '更新が完了しました。ChatCode CLI を再起動してください。',
@@ -31,7 +31,7 @@ export const zh = {
31
31
  updateSettingsDesc: '将 ChatCode CLI 端升级到 npm 最新版。',
32
32
  updateChecking: '正在检查版本更新…',
33
33
  updatePerforming: '正在升级到最新版本…',
34
- updateUpToDate: '全部已是最新版本。',
34
+ updateUpToDate: '已是最新版本。',
35
35
  updateNow: '立即更新',
36
36
  updateConfirm: '将把这些包升级到最新版本,完成后需要重启才能生效。',
37
37
  updateRestartHint: '更新成功,请重启 ChatCode CLI 以使用新版本。',
@@ -502,7 +502,7 @@ export const en: Record<keyof typeof zh, string> = {
502
502
  updateSettingsDesc: 'Upgrade the ChatCode CLI to the latest npm version.',
503
503
  updateChecking: 'Checking for version updates…',
504
504
  updatePerforming: 'Upgrading to the latest versions…',
505
- updateUpToDate: 'Everything is already up to date.',
505
+ updateUpToDate: 'Already up to date.',
506
506
  updateNow: 'Update now',
507
507
  updateConfirm: 'These packages will be upgraded to the latest versions. A restart is required to apply them.',
508
508
  updateRestartHint: 'Update complete. Restart ChatCode CLI to use the new version.',
@@ -16,6 +16,7 @@ import { parsePrefs } from './prefs.ts'
16
16
  import { t } from './locales.ts'
17
17
  import { IconPinOutline16 } from './icons.tsx'
18
18
  import css from './sidebar.module.css'
19
+ import { useCurrentSessionId } from './current-session.ts'
19
20
 
20
21
  const PIN_SETTINGS_KEY = 'session-pins'
21
22
 
@@ -35,6 +36,11 @@ function pinSettings(ids: readonly string[]): Record<string, unknown> {
35
36
  return { ids: [...ids] }
36
37
  }
37
38
 
39
+ /** Open through the Host's navigation policy, not the pure Session catalog. */
40
+ export function openPinnedSession(ctx: Context, sessionId: string): void {
41
+ ctx.uiWorkspace.openSession(sessionId)
42
+ }
43
+
38
44
  /** A small current-color pin icon for the menu row injected into DSH's menu. */
39
45
  function pinIconMarkup(): string {
40
46
  return '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M9.5 1.5 14.5 6.5 12.5 8.5 10 6 5.5 10.5 6 12 4.5 13.5 2.5 11.5 4 10 5.5 10.5 10 6 7.5 8.5 6.5Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>'
@@ -221,6 +227,7 @@ function PinnedSessions({ ctx, store, onChange }: {
221
227
  onChange: (ids: readonly string[]) => void
222
228
  }) {
223
229
  const list = useSyncExternalStore(ctx.sessions.list.subscribe, ctx.sessions.list.getSnapshot, ctx.sessions.list.getSnapshot)
230
+ const currentSessionId = useCurrentSessionId(ctx)
224
231
  const prefs = useSyncExternalStore(store.subscribe.bind(store), store.getSnapshot.bind(store), store.getSnapshot.bind(store)).prefs
225
232
  const ids = pinnedSessionIdsOf(prefs)
226
233
  const pinKey = ids.join('\0')
@@ -256,7 +263,7 @@ function PinnedSessions({ ctx, store, onChange }: {
256
263
  const forkSession = (sessionId: string): void => {
257
264
  closeMenu()
258
265
  void ctx.sessions.fork?.({ sessionId, increaseTitle: true }).then((childId) => {
259
- ctx.sessions.open?.(childId)
266
+ openPinnedSession(ctx, childId)
260
267
  }).catch((error: unknown) => {
261
268
  console.warn('[dsh-better-sidebar] fork pinned session failed:', error)
262
269
  })
@@ -290,8 +297,8 @@ function PinnedSessions({ ctx, store, onChange }: {
290
297
  <button
291
298
  type="button"
292
299
  className={css.pinnedSessionOpen}
293
- aria-current={session.id === list.current ? 'page' : undefined}
294
- onClick={() => { ctx.sessions.open?.(session.id) }}
300
+ aria-current={session.id === currentSessionId ? 'page' : undefined}
301
+ onClick={() => { openPinnedSession(ctx, session.id) }}
295
302
  >
296
303
  <span className={css.pinnedSessionDot} data-running={session.running || undefined} aria-hidden="true" />
297
304
  <span className={css.pinnedSessionTitle}>{session.displayTitle}</span>
@@ -19,6 +19,12 @@ export const AI_RATIO_NAV_MARKER = 'data-dsh-better-sidebar-ai-ratio-nav'
19
19
  /** Marker of the "更新" (update) settings nav row. */
20
20
  export const UPDATE_NAV_MARKER = 'data-dsh-better-sidebar-update-nav'
21
21
 
22
+ /** Fallback markers for host/plugin settings rows whose icons mount late. */
23
+ export const GENERAL_NAV_MARKER = 'data-dsh-better-sidebar-general-nav'
24
+ export const PLUGIN_MARKET_NAV_MARKER = 'data-dsh-better-sidebar-plugin-market-nav'
25
+ export const UI_TWEAKS_NAV_MARKER = 'data-dsh-better-sidebar-ui-tweaks-nav'
26
+ export const SKILL_MARKET_NAV_MARKER = 'data-dsh-better-sidebar-skill-market-nav'
27
+
22
28
  /**
23
29
  * Keep the marker on the settings-nav button whose visible text is this
24
30
  * plugin's current localized section label.
@@ -28,15 +34,16 @@ export const UPDATE_NAV_MARKER = 'data-dsh-better-sidebar-update-nav'
28
34
  * other's markers).
29
35
  * @returns disposer that disconnects observation and removes owned markers.
30
36
  */
31
- export function registerSettingsNavIcon(label: () => string, marker: string = SETTINGS_NAV_MARKER): () => void {
37
+ export function registerSettingsNavIcon(label: () => string | readonly string[], marker: string = SETTINGS_NAV_MARKER): () => void {
32
38
  let disposed = false
33
39
 
34
40
  const sync = (): void => {
35
41
  if (disposed) return
36
- const currentLabel = label().trim()
42
+ const resolved = label()
43
+ const currentLabels = new Set((typeof resolved === 'string' ? [resolved] : resolved).map(value => value.trim()).filter(Boolean))
37
44
  const buttons = document.querySelectorAll<HTMLButtonElement>('[role="dialog"] nav button')
38
45
  for (const button of buttons) {
39
- const matches = currentLabel.length > 0 && button.textContent?.trim() === currentLabel
46
+ const matches = currentLabels.has(button.textContent?.trim() ?? '')
40
47
  if (matches) button.setAttribute(marker, '')
41
48
  else button.removeAttribute(marker)
42
49
  }
@@ -27,6 +27,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
27
27
  /** Browser URL for the project-owned `assets/logo.svg` file. */
28
28
  export const PROJECT_LOGO_URL = '/_dsh/ui-tweaks/assets/logo.svg'
29
29
  const PROJECT_NAME = 'ChatCode CLI'
30
+ const HERO_TITLE = '与世界,深一度对话'
30
31
  const LEGACY_HOST_BRAND_TEXT = /DSH\s*(?:Local\s*Build|本地构建)|ChatCode\s*CLI/giu
31
32
  const HOST_BRAND_LABEL_TEXT = /^(?:DSH\s*(?:Local\s*Build|本地构建)|ChatCode\s*CLI)$/iu
32
33
  const HOST_BUILD_VERSION_TEXT = /^v?\d+\.\d+\.\d+(?:[-+][0-9a-z.-]+)*$/iu
@@ -57,16 +58,9 @@ export function SidebarProjectName(): ReactNode {
57
58
  }
58
59
 
59
60
  export const BRANDING_CSS = `
60
- /* Keep the light-theme type hierarchy in ChatCode's restrained red family,
61
- while leaving every surface and interaction state (including workspace /
62
- sidebar hover and selected fills, menus, the composer and shadows) owned
63
- by the host theme. */
64
- body:not([data-ds-dark-theme]){
65
- --dsw-alias-label-primary:#301111;
66
- --dsw-alias-label-secondary:#754745;
67
- --dsw-alias-label-tertiary:#a77471;
68
- }
69
- /* Saturated brand red remains exclusive to explicitly marked primary actions. */
61
+ /* ChatCode CLI branding is deliberately limited to primary actions. Every
62
+ other surface and interaction state (including workspace/sidebar rows,
63
+ menus, the composer, text and shadows) remains owned by the host theme. */
70
64
  body:not([data-ds-dark-theme]) .dut-unicom-primary-action{background:linear-gradient(135deg,#9B0B09,#b71916) !important;border-color:#9B0B09 !important;color:#fff !important;box-shadow:0 7px 18px rgba(155,11,9,.24) !important}
71
65
  body:not([data-ds-dark-theme]) .dut-unicom-primary-action:hover{background:linear-gradient(135deg,#b71916,#cf302c) !important;border-color:#b71916 !important;color:#fff !important;box-shadow:0 10px 24px rgba(155,11,9,.30) !important}
72
66
  body:not([data-ds-dark-theme]) .dut-unicom-primary-action:active{transform:translateY(1px);box-shadow:0 4px 11px rgba(155,11,9,.22) !important}
@@ -85,7 +79,8 @@ body[data-ds-dark-theme] .dut-unicom-primary-action:not(:hover){background:#9B0B
85
79
  .dut-brand-version{display:none !important}
86
80
  .dut-hero-preview{display:none !important}
87
81
  .dut-hero-logo{width:60px !important;height:60px !important}
88
- [class*="headline"]:has(.dut-hero-logo){grid-template-columns:60px auto !important;font-size:26px !important;line-height:34px !important}
82
+ .dut-hero-title{font-size:30px !important;line-height:40px !important;font-weight:600 !important;letter-spacing:.06em !important;white-space:nowrap}
83
+ [class*="headline"]:has(.dut-hero-logo){grid-template-columns:60px auto !important;font-size:30px !important;line-height:40px !important}
89
84
  .dut-settings-logo{width:19px;height:19px;object-fit:contain}
90
85
  `
91
86
 
@@ -176,6 +171,17 @@ function syncBranding(): void {
176
171
  if (HERO_HEADLINE.test(node.data)) node.data = node.data.replace(HERO_HEADLINE, PROJECT_NAME)
177
172
  }
178
173
 
174
+ // Keep the product name in the sidebar, but use a more expressive headline
175
+ // above the new-session composer.
176
+ document.querySelectorAll<HTMLElement>('.dut-hero-logo').forEach((logo) => {
177
+ const headline = logo.closest<HTMLElement>('[class*="headline"]')
178
+ const title = Array.from(headline?.querySelectorAll<HTMLElement>('*') ?? [])
179
+ .find(element => element.childElementCount === 0 && element.textContent?.trim() === PROJECT_NAME)
180
+ if (title === undefined) return
181
+ title.textContent = HERO_TITLE
182
+ title.classList.add('dut-hero-title')
183
+ })
184
+
179
185
  // Older host builds expose a brand-mark wrapper but no text-bearing sibling.
180
186
  document.querySelectorAll<HTMLElement>('[class*="brandMark"]').forEach((host) => {
181
187
  const svg = host.matches('svg') ? host : host.querySelector('svg')