@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.
- package/lib/client-editor.js +262 -262
- package/lib/client-mermaid.js +259 -259
- package/lib/client-registry.js +467 -408
- package/lib/client-terminal.js +265 -265
- package/lib/client.js +464 -405
- package/lib/types/client/chatcode-login-route.d.ts +4 -0
- package/lib/types/client/current-session.d.ts +11 -0
- package/lib/types/client/session-pins.d.ts +2 -0
- package/lib/types/client/settings-nav-icon.d.ts +6 -1
- package/lib/types/client/ui-tweaks/branding.d.ts +1 -1
- package/lib/types/context-types.d.ts +26 -6
- package/package.json +1 -1
- package/src/client/AccountMenu.tsx +10 -4
- package/src/client/ChatcodeGate.tsx +2 -2
- package/src/client/Sidebar.tsx +3 -1
- package/src/client/chatcode-login-route.ts +11 -0
- package/src/client/current-session.ts +22 -0
- package/src/client/index.tsx +27 -2
- package/src/client/intercept.tsx +2 -1
- package/src/client/layout.css +42 -0
- package/src/client/locales-ja.ts +1 -1
- package/src/client/locales.ts +2 -2
- package/src/client/session-pins.tsx +10 -3
- package/src/client/settings-nav-icon.ts +10 -3
- package/src/client/ui-tweaks/branding.tsx +17 -11
- package/src/context-types.ts +26 -6
|
@@ -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/*
|
|
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
|
|
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.
|
|
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
|
|
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(
|
|
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
|
-
{
|
|
155
|
-
<LogoutIcon />{t('
|
|
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
|
-
|
|
32
|
-
window.location.replace(`/sidebar/login?return=${encodeURIComponent(back)}`)
|
|
32
|
+
redirectToChatcodeLogin()
|
|
33
33
|
}, [chatcode])
|
|
34
34
|
return null
|
|
35
35
|
}
|
package/src/client/Sidebar.tsx
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|
package/src/client/index.tsx
CHANGED
|
@@ -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 {
|
|
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
|
package/src/client/intercept.tsx
CHANGED
|
@@ -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
|
|
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
|
})
|
package/src/client/layout.css
CHANGED
|
@@ -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. */
|
package/src/client/locales-ja.ts
CHANGED
|
@@ -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 を再起動してください。',
|
package/src/client/locales.ts
CHANGED
|
@@ -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: '
|
|
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
|
|
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 ===
|
|
294
|
-
onClick={() => { ctx
|
|
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
|
|
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 =
|
|
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
|
-
/*
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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')
|