@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
package/lib/index.js CHANGED
@@ -26,7 +26,7 @@ import { YAMLMap, YAMLSeq, parse, parseDocument, stringify } from "yaml";
26
26
  const UI_TWEAKS_SETTINGS_NAMESPACE = "better-sidebar";
27
27
  /** Configuration schema with documented defaults. */
28
28
  const Config$1 = z$1.object({
29
- fontSize: z$1.number().min(10).max(32).default(16),
29
+ fontSize: z$1.number().min(10).max(32).default(14),
30
30
  codeFontScale: z$1.number().min(50).max(150).default(81),
31
31
  codeFontSize: z$1.number().min(8).max(32),
32
32
  lineHeight: z$1.number().min(0).max(64).default(16),
@@ -865,13 +865,124 @@ function registerBundleRoute(ctx, fence) {
865
865
  });
866
866
  }
867
867
  //#endregion
868
+ //#region src/login-page-script.ts
869
+ /** Standalone sign-in recovery; each document starts once and always keeps a usable reopen action. */
870
+ const CHATCODE_LOGIN_PAGE_SCRIPT = String.raw`
871
+ (() => {
872
+ const message = document.getElementById('message');
873
+ const button = document.getElementById('login');
874
+ const fallback = document.getElementById('login-fallback');
875
+ const link = document.getElementById('login-link');
876
+ const current = new URL(location.href);
877
+ const returnTo = (() => {
878
+ const value = current.searchParams.get('return');
879
+ if (!value || !value.startsWith('/') || value.startsWith('//')) return '/';
880
+ const target = new URL(value, location.origin);
881
+ return target.origin === location.origin ? target.pathname + target.search + target.hash : '/';
882
+ })();
883
+ let busy = false, checking = false, attempted = false, recheck = false, attempt, popup;
884
+ const call = async name => {
885
+ const response = await fetch('/sidebar/api/' + name, {
886
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}',
887
+ });
888
+ const data = await response.json();
889
+ if (!response.ok || !data.ok) throw new Error('操作失败');
890
+ return data.value;
891
+ };
892
+ const showMessage = (text, error = false) => {
893
+ message.textContent = text;
894
+ message.classList.toggle('error', error);
895
+ };
896
+ async function login() {
897
+ if (busy) return;
898
+ attempted = true;
899
+ busy = true;
900
+ button.disabled = true;
901
+ button.textContent = '正在打开登录页…';
902
+ let tab = null;
903
+ try { tab = window.open('', '_blank'); if (tab) tab.opener = null; } catch {}
904
+ try {
905
+ if (!attempt) {
906
+ const started = await call('chatcode.start');
907
+ const url = new URL(started.url);
908
+ if (!['http:', 'https:'].includes(url.protocol) || typeof started.sessionId !== 'string') throw new Error('登录地址无效');
909
+ attempt = { sessionId: started.sessionId, url: url.href };
910
+ }
911
+ link.href = attempt.url;
912
+ link.textContent = attempt.url;
913
+ fallback.hidden = false;
914
+ if (tab && !tab.closed) {
915
+ try { tab.location.href = attempt.url; popup = tab; } catch { try { tab.close(); } catch {} }
916
+ }
917
+ showMessage('请在登录页完成登录;若网页已关闭,可随时重新打开。');
918
+ } catch {
919
+ try { if (tab) tab.close(); } catch {}
920
+ showMessage('登录页打开失败,请重试。', true);
921
+ } finally {
922
+ busy = false;
923
+ button.disabled = false;
924
+ button.textContent = attempt ? '重新打开登录页' : '去登录';
925
+ }
926
+ }
927
+ async function status() {
928
+ if (checking || busy) return;
929
+ checking = true;
930
+ const observedAttempt = attempt, observedStarted = attempted;
931
+ try {
932
+ const state = await call('chatcode.status');
933
+ if (busy || attempt !== observedAttempt || attempted !== observedStarted) return;
934
+ if (state.available && state.validation === 'valid' && !state.expired) {
935
+ try { if (popup) popup.close(); window.focus(); } catch {}
936
+ location.replace(returnTo);
937
+ return;
938
+ }
939
+ if (!state.available) {
940
+ recheck = true;
941
+ button.textContent = '重新检测';
942
+ showMessage('登录服务暂不可用,请稍后重试。', true);
943
+ return;
944
+ }
945
+ if (state.configured && state.validation === 'unavailable') {
946
+ recheck = true;
947
+ button.textContent = '重新检测';
948
+ showMessage('暂时无法验证已有账号,请检查网络后重新检测。', true);
949
+ return;
950
+ }
951
+ recheck = false;
952
+ button.textContent = attempt ? '重新打开登录页' : '去登录';
953
+ if (attempt && (state.login?.sessionId !== attempt.sessionId || state.login.state !== 'pending')) {
954
+ attempt = undefined;
955
+ fallback.hidden = true;
956
+ link.removeAttribute('href');
957
+ link.textContent = '';
958
+ button.textContent = '去登录';
959
+ showMessage('登录请求已失效,请重新登录。');
960
+ }
961
+ if (!attempted) await login();
962
+ } catch {
963
+ recheck = true;
964
+ button.textContent = '重新检测';
965
+ showMessage('无法读取登录状态,请稍后重试。', true);
966
+ } finally {
967
+ checking = false;
968
+ if (!busy) button.disabled = false;
969
+ }
970
+ }
971
+ button.addEventListener('click', () => { void (recheck ? status() : login()); });
972
+ void status();
973
+ setInterval(() => { void status(); }, 2000);
974
+ })();
975
+ `;
976
+ //#endregion
868
977
  //#region src/login-route.ts
869
978
  const assetPath = join(dirname(fileURLToPath(import.meta.url)), "..", "assets", "logo.svg");
870
- function page(logo) {
979
+ /** Render the standalone login gate with one automatic attempt and persistent manual recovery. */
980
+ function renderChatcodeLoginPage(logo) {
871
981
  return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>请登录 ChatCode</title><style>
872
982
  *{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh;display:grid;place-items:center;font-family:Inter,"Microsoft YaHei",system-ui,sans-serif;color:#172b4d;background:radial-gradient(circle at 18% 18%,#e5edff 0,transparent 30rem),linear-gradient(135deg,#edf2fb,#dfe8f7)}.card{width:min(490px,calc(100vw - 40px));padding:34px 44px 36px;border:1px solid #dce4ef;border-radius:18px;background:#fff;box-shadow:0 20px 48px rgba(36,62,103,.18),0 2px 8px rgba(36,62,103,.06)}.brand{display:flex;align-items:center;justify-content:center;gap:12px;padding-bottom:27px;border-bottom:1px solid #edf0f5}.brand img{width:34px;height:34px}.brand strong{font-size:22px;letter-spacing:-.02em}.content{padding-top:40px;text-align:center}.content h1{margin:0;font-size:28px;line-height:1.3;font-weight:650}.content p{min-height:24px;margin:12px 0 30px;color:#71809a;font-size:14px;line-height:1.7}.action{width:100%;min-height:46px;border:0;border-radius:8px;background:#9B0B09;color:#fff;font:600 15px/1 Inter,"Microsoft YaHei",sans-serif;cursor:pointer;box-shadow:0 8px 18px rgba(155,11,9,.25);transition:transform .18s ease,box-shadow .18s ease,background .18s ease}.action:hover:not(:disabled){transform:translateY(-2px);background:#b20d0a;box-shadow:0 12px 24px rgba(155,11,9,.38)}.action:focus-visible{outline:3px solid #f0b7b5;outline-offset:3px}.action:disabled{cursor:default;opacity:.7}.error{color:#b42318}@media(max-width:480px){.card{padding:30px 26px}.content{padding-top:32px}}
873
- </style></head><body><main class="card"><div class="brand"><img src="data:image/svg+xml;base64,${logo}" alt=""><strong>ChatCode</strong></div><section class="content"><h1>请登录 ChatCode</h1><p id="message">请登录后继续使用。</p><button id="login" class="action" type="button">去登录</button></section></main><script>
874
- const message=document.getElementById('message'),button=document.getElementById('login');const current=new URL(location.href);const returnTo=(()=>{const v=current.searchParams.get('return');return v&&v.startsWith('/')&&!v.startsWith('//')?v:'/'} )();const call=async(name,body={})=>{const r=await fetch('/sidebar/api/'+name,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)});const d=await r.json();if(!r.ok||!d.ok)throw new Error(d.error?.message||'操作失败');return d.value};let waiting=false;async function status(){try{const s=await call('chatcode.status');if(s.available&&s.validation==='valid'&&!s.expired){location.replace(returnTo);return}if(waiting)message.textContent='正在等待浏览器完成登录…'}catch{message.textContent='无法读取登录状态,请稍后重试。';message.classList.add('error')}}async function login(){const tab=window.open('','_blank');button.disabled=true;button.textContent='正在打开登录页…';message.classList.remove('error');try{const s=await call('chatcode.start');waiting=true;if(tab&&!tab.closed){tab.opener=null;tab.location.href=s.url;message.textContent='请在新打开的浏览器窗口完成登录…'}else{message.innerHTML='浏览器拦截了登录窗口,请 <a href="'+s.url+'" target="_blank" rel="noopener noreferrer">打开登录页</a> 后完成登录。'}}catch{try{tab&&tab.close()}catch{}button.disabled=false;button.textContent='去登录';message.textContent='登录页打开失败,请重试。';message.classList.add('error')}}button.addEventListener('click',login);status();setInterval(status,2000);
983
+ .login-fallback{margin:18px 0 0;color:#71809a;font-size:12px;line-height:1.7;overflow-wrap:anywhere}.login-fallback a{display:block;margin-top:4px;color:#315da8;text-underline-offset:3px}
984
+ </style></head><body><main class="card"><div class="brand"><img src="data:image/svg+xml;base64,${logo}" alt=""><strong>ChatCode</strong></div><section class="content"><h1>请登录 ChatCode</h1><p id="message" aria-live="polite">正在检查登录状态…</p><button id="login" class="action" type="button" disabled>正在检查登录状态…</button><div id="login-fallback" class="login-fallback" hidden>若登录网页已关闭或未弹出,可再次访问:<a id="login-link" target="_blank" rel="noopener noreferrer"></a></div></section></main><script>
985
+ ${CHATCODE_LOGIN_PAGE_SCRIPT}
875
986
  <\/script></body></html>`;
876
987
  }
877
988
  function registerLoginRoute(ctx, fence) {
@@ -896,7 +1007,7 @@ function registerLoginRoute(ctx, fence) {
896
1007
  "cache-control": "no-store",
897
1008
  "content-security-policy": "default-src 'self'; img-src 'self' data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'"
898
1009
  });
899
- res.end(req.method === "HEAD" ? void 0 : page(logo));
1010
+ res.end(req.method === "HEAD" ? void 0 : renderChatcodeLoginPage(logo));
900
1011
  } catch {
901
1012
  res.writeHead(500);
902
1013
  res.end("login page unavailable");
@@ -5882,6 +5993,51 @@ function apply$1(ctx) {
5882
5993
  ctx.logger.info("[ChatCode CLI UI Tweaks] settings namespace registered and Web routes mounted");
5883
5994
  }
5884
5995
  //#endregion
5996
+ //#region src/plugin-market-api.ts
5997
+ function buildPluginMarketApi(ctx, cwdOf) {
5998
+ const service = () => {
5999
+ const value = ctx.get("chatcodePluginMarket", false);
6000
+ if (!value) throw new SidebarError("chatcode-unavailable", "插件市场服务未挂载,请更新并启用 ChatCode config 插件。", 503);
6001
+ return value;
6002
+ };
6003
+ const scopeOf = (payload) => {
6004
+ const scope = requireString(payload, "scope");
6005
+ if (scope !== "project" && scope !== "user") throw new SidebarError("bad-request", "scope must be project or user");
6006
+ return scope;
6007
+ };
6008
+ const contextOf = async (payload) => {
6009
+ const sessionId = payload?.sessionId;
6010
+ if (sessionId === void 0 || sessionId === "") return {
6011
+ sessionId: "",
6012
+ cwd: process.cwd()
6013
+ };
6014
+ requireString(payload, "sessionId");
6015
+ return {
6016
+ sessionId,
6017
+ cwd: (await cwdOf(payload)).cwd
6018
+ };
6019
+ };
6020
+ return {
6021
+ "plugins.context": contextOf,
6022
+ "plugins.marketplaces": () => service().marketplaces(),
6023
+ "plugins.catalog": async (payload) => {
6024
+ const market = service();
6025
+ const id = requireString(payload, "marketplaceId");
6026
+ if (payload.includeMetadata !== true) return market.plugins(id);
6027
+ return market.catalog ? market.catalog(id) : {
6028
+ plugins: await market.plugins(id),
6029
+ warning: ""
6030
+ };
6031
+ },
6032
+ "plugins.installed": async (payload) => service().installed((await contextOf(payload)).cwd),
6033
+ "plugins.install": async (payload) => service().install((await contextOf(payload)).cwd, requireString(payload, "pluginId"), scopeOf(payload)),
6034
+ "plugins.uninstall": async (payload) => {
6035
+ await service().uninstall((await contextOf(payload)).cwd, requireString(payload, "pluginId"), scopeOf(payload));
6036
+ return { ok: true };
6037
+ }
6038
+ };
6039
+ }
6040
+ //#endregion
5885
6041
  //#region src/index.ts
5886
6042
  /**
5887
6043
  * dsh-better-sidebar host half: the /sidebar JSON API (explorer listing, file
@@ -6197,6 +6353,7 @@ function buildApi(ctx, ptyManager, agentPtyRegistry, resolved, terminalShell, ge
6197
6353
  parent: parentOf(cwd) ?? null
6198
6354
  };
6199
6355
  },
6356
+ ...buildPluginMarketApi(ctx, cwdOf),
6200
6357
  "fs.tree": async (payload) => {
6201
6358
  const { cwd } = await cwdOf(payload);
6202
6359
  return listDirectory(payload.path === void 0 ? cwd : await ensureWorkspacePath(cwd, requireString(payload, "path"), fenceEnabledOf(getSettings)), resolved.listLimit);
@@ -0,0 +1,5 @@
1
+ /** Native modal supplies an inert background and default Cancel focus until a choice is made. */
2
+ export declare function LogoutConfirmDialog({ onCancel, onConfirm }: {
3
+ onCancel: () => void;
4
+ onConfirm: () => void;
5
+ }): JSX.Element;
@@ -0,0 +1,7 @@
1
+ import type { Context } from '../context-types.ts';
2
+ import type { PluginMarketCatalog } from './plugin-market-catalog.ts';
3
+ export declare function PluginMarketSettingsSection({ ctx, catalog }: {
4
+ ctx: Context;
5
+ catalog: PluginMarketCatalog;
6
+ }): JSX.Element;
7
+ export declare function registerPluginMarketSettings(ctx: Context, catalog: PluginMarketCatalog): void;
@@ -0,0 +1,7 @@
1
+ import { type SessionScope } from './api.ts';
2
+ import type { PluginMarketCatalog } from './plugin-market-catalog.ts';
3
+ export declare function PluginMarketView({ scope, visible, catalog }: {
4
+ scope: SessionScope;
5
+ visible: boolean;
6
+ catalog: PluginMarketCatalog;
7
+ }): JSX.Element;
@@ -1,4 +1,5 @@
1
1
  import type { BrowserProbeResult } from './browser.ts';
2
+ import type { MarketplaceCatalog, MarketplacePlugin, InstalledMarketplacePlugin, PluginMarketScope } from '../plugin-market-shared.ts';
2
3
  /** One wire failure. */
3
4
  export declare class SidebarApiError extends Error {
4
5
  readonly code: string;
@@ -184,6 +185,20 @@ export interface SessionScope {
184
185
  }
185
186
  /** The sidebar API surface (session scope threaded through every call). */
186
187
  export declare const api: {
188
+ pluginMarketContext: (scope: SessionScope, signal?: AbortSignal) => Promise<{
189
+ sessionId: string;
190
+ cwd: string;
191
+ }>;
192
+ pluginMarketplaces: (signal?: AbortSignal) => Promise<MarketplaceCatalog>;
193
+ pluginCatalog: (marketplaceId: string, signal?: AbortSignal) => Promise<MarketplacePlugin[] | {
194
+ plugins: MarketplacePlugin[];
195
+ warning: string;
196
+ }>;
197
+ pluginsInstalled: (scope: SessionScope, signal?: AbortSignal) => Promise<InstalledMarketplacePlugin[]>;
198
+ pluginInstall: (scope: SessionScope, pluginId: string, installScope: PluginMarketScope) => Promise<InstalledMarketplacePlugin>;
199
+ pluginUninstall: (scope: SessionScope, pluginId: string, installScope: PluginMarketScope) => Promise<{
200
+ ok: true;
201
+ }>;
187
202
  chatcodeStatus: (signal?: AbortSignal) => Promise<ChatCodeAuthState>;
188
203
  chatcodeStart: () => Promise<{
189
204
  sessionId: string;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The 7 built-in tab descriptors: the plugin registers its own pages
3
- * (editor / git / terminal / browser / diff) through
2
+ * The 6 built-in tab descriptors: the plugin registers its own pages
3
+ * (editor / git / terminal / browser / diff / plugin-market) through
4
4
  * the same {@link BetterSidebarService} external plugins use — eating its
5
5
  * own dogfood. The terminal descriptor owns its quota (`TERMINAL_LIMIT`)
6
6
  * and mints `terminal:<uuid>` ids through `createTab`; the browser mints
@@ -8,6 +8,7 @@
8
8
  * (the old standalone explorer merged into it).
9
9
  */
10
10
  import type { Context } from '../../context-types.ts';
11
+ import { type PluginMarketCatalog } from '../plugin-market-catalog.ts';
11
12
  import type { TabDescriptor } from '../service.ts';
12
13
  /** How many UI-owned terminals may be open at once (agent-owned ones are uncapped). */
13
14
  export declare const TERMINAL_LIMIT = 3;
@@ -15,6 +16,8 @@ export declare const TERMINAL_LIMIT = 3;
15
16
  export interface BuiltinTabOptions {
16
17
  /** Returns the display title for newly opened terminal tabs. */
17
18
  terminalTitle?: () => string;
19
+ /** Shared app-lifetime catalog; production supplies the activation's store. */
20
+ pluginMarketCatalog?: PluginMarketCatalog;
18
21
  }
19
- /** The 5 built-in tab descriptors. */
22
+ /** The 6 built-in tab descriptors. */
20
23
  export declare function builtinTabs(ctx: Context, options?: BuiltinTabOptions): readonly TabDescriptor[];
@@ -22,8 +22,31 @@
22
22
  */
23
23
  /** The zh dictionary (also registered into the ChatCode CLI locale registry under {@link LOCALE_NS}). */
24
24
  export declare const zh: {
25
+ pluginMarketTitle: string;
26
+ pluginMarketProjectDirectory: string;
27
+ pluginMarketInstalledTab: string;
28
+ pluginMarketRefresh: string;
29
+ pluginMarketLoading: string;
30
+ pluginMarketRefreshing: string;
31
+ pluginMarketEmpty: string;
32
+ pluginMarketBack: string;
33
+ pluginMarketInstall: string;
34
+ pluginMarketUninstall: string;
35
+ pluginMarketProject: string;
36
+ pluginMarketUser: string;
37
+ pluginMarketInstalled: string;
38
+ pluginMarketUninstalled: string;
39
+ pluginMarketSubtitle: string;
40
+ pluginMarketSearch: string;
41
+ pluginMarketDefault: string;
42
+ pluginMarketBrowse: string;
43
+ pluginMarketNoDescription: string;
44
+ pluginMarketInstalling: string;
45
+ pluginMarketUninstalling: string;
25
46
  accountMenuSettings: string;
26
47
  accountMenuLogout: string;
48
+ accountMenuLogoutConfirmTitle: string;
49
+ accountMenuLogoutConfirmDescription: string;
27
50
  accountMenuLabel: string;
28
51
  updateMenu: string;
29
52
  updateNav: string;
@@ -0,0 +1,25 @@
1
+ import type { MarketplaceCatalog, MarketplacePlugin } from '../plugin-market-shared.ts';
2
+ interface Entry<T> {
3
+ readonly status: 'loading' | 'ready' | 'error';
4
+ readonly data?: T;
5
+ readonly error?: string;
6
+ readonly warning?: string;
7
+ }
8
+ interface Snapshot {
9
+ readonly markets?: Entry<MarketplaceCatalog>;
10
+ readonly plugins: Readonly<Record<string, Entry<MarketplacePlugin[]>>>;
11
+ readonly refresh?: {
12
+ readonly status: 'loading' | 'ready' | 'error';
13
+ readonly error?: string;
14
+ };
15
+ }
16
+ export interface PluginMarketCatalog {
17
+ subscribe(listener: () => void): () => void;
18
+ getSnapshot(): Snapshot;
19
+ start(): void;
20
+ ensure(marketplaceId: string): void;
21
+ refresh(marketplaceId?: string): void;
22
+ dispose(): void;
23
+ }
24
+ export declare function createPluginMarketCatalog(): PluginMarketCatalog;
25
+ export {};
@@ -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/* 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";
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:24px !important;height:24px !important}\n.dut-brand-title{font:var(--dsw-font-s-strong-14) !important}\n.dut-sidebar-logo{width:24px !important;height:24px !important}\n.dut-sidebar-brand-name{font:var(--dsw-font-s-strong-14) !important;letter-spacing:-.01em !important;white-space:nowrap}\n\n.dut-sidebar-header{height:44px !important;padding:0 4px !important;margin-bottom:8px !important;gap:8px !important}\n.dut-sidebar-brand{min-width:0 !important;background:transparent !important;box-shadow:none !important}\n.dut-sidebar-identity{height:24px !important;gap:8px !important}\n.dut-sidebar-wordmark{font:var(--dsw-font-s-strong-14) !important;letter-spacing:-.01em !important;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.dut-sidebar-toggle{width:28px !important;height:28px !important;border-radius:8px !important;color:var(--dsw-alias-label-secondary) !important}\n.dut-sidebar-toggle:hover{background:var(--dsw-alias-interactive-bg-hover) !important}\n/* Reuse the original ChatCode primary-action red, scoped to this marked CTA. */\n.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}\n.dut-sidebar-new-session:hover{background:linear-gradient(135deg,#b71916,#cf302c) !important}\n.dut-sidebar-new-session:active{background:#9B0B09 !important}\n.dut-sidebar-new-session:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}\n.dut-sidebar-new-session > span:first-child{flex:1;min-width:0;overflow:hidden;mask-image:none !important}\n.dut-sidebar-new-session > span:first-child > span{width:auto !important;justify-content:flex-start !important;gap:8px !important;min-width:0}\n.dut-sidebar-new-session > span:first-child > span > svg{width:16px;height:16px;flex:none;color:inherit}\n.dut-sidebar-new-session > span:first-child > span > span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.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)}\n/* Temporary navigation-only hide. Remove this block to restore Plugins;\n registrations, settings, installed plugins and other panel entries stay intact. */\nnav:is([aria-label=\"\u5168\u5C40\u9762\u677F\"],[aria-label=\"Global panels\"]) > button:is([aria-label=\"\u63D2\u4EF6\"],[aria-label=\"Plugins\"]),\nnav:is([aria-label=\"\u5168\u5C40\u9762\u677F\"],[aria-label=\"Global panels\"]):has(> button:is([aria-label=\"\u63D2\u4EF6\"],[aria-label=\"Plugins\"]):only-child){display:none !important}\n\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;
@@ -58,7 +58,7 @@ declare const en: {
58
58
  readonly fontSize: "Message font size";
59
59
  readonly fontSizeHint: "Number between 10 and 32; applies to message text, headings, tables and code.";
60
60
  readonly codeFontSize: "Code font size";
61
- readonly codeFontSizeHint: "Absolute code size in px (8–32); 13px is ChatCode CLI's default at a 16px body. Applies to code blocks; inline code follows proportionally.";
61
+ readonly codeFontSizeHint: "Absolute code size in px (8–32); 13px is ChatCode CLI's default at a 14px body. Applies to code blocks; inline code follows proportionally.";
62
62
  readonly lineHeight: "Line spacing";
63
63
  readonly 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; 16 is ChatCode CLI's default.";
64
64
  readonly tableStyle: "Table style";
@@ -0,0 +1,6 @@
1
+ /** Compact sidebar chrome; the host retains every button, shortcut and drag surface. */
2
+ export declare const SIDEBAR_HEADER_CSS = "\n.dut-sidebar-header{height:44px !important;padding:0 4px !important;margin-bottom:8px !important;gap:8px !important}\n.dut-sidebar-brand{min-width:0 !important;background:transparent !important;box-shadow:none !important}\n.dut-sidebar-identity{height:24px !important;gap:8px !important}\n.dut-sidebar-wordmark{font:var(--dsw-font-s-strong-14) !important;letter-spacing:-.01em !important;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.dut-sidebar-toggle{width:28px !important;height:28px !important;border-radius:8px !important;color:var(--dsw-alias-label-secondary) !important}\n.dut-sidebar-toggle:hover{background:var(--dsw-alias-interactive-bg-hover) !important}\n/* Reuse the original ChatCode primary-action red, scoped to this marked CTA. */\n.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}\n.dut-sidebar-new-session:hover{background:linear-gradient(135deg,#b71916,#cf302c) !important}\n.dut-sidebar-new-session:active{background:#9B0B09 !important}\n.dut-sidebar-new-session:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}\n.dut-sidebar-new-session > span:first-child{flex:1;min-width:0;overflow:hidden;mask-image:none !important}\n.dut-sidebar-new-session > span:first-child > span{width:auto !important;justify-content:flex-start !important;gap:8px !important;min-width:0}\n.dut-sidebar-new-session > span:first-child > span > svg{width:16px;height:16px;flex:none;color:inherit}\n.dut-sidebar-new-session > span:first-child > span > span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.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)}\n/* Temporary navigation-only hide. Remove this block to restore Plugins;\n registrations, settings, installed plugins and other panel entries stay intact. */\nnav:is([aria-label=\"\u5168\u5C40\u9762\u677F\"],[aria-label=\"Global panels\"]) > button:is([aria-label=\"\u63D2\u4EF6\"],[aria-label=\"Plugins\"]),\nnav:is([aria-label=\"\u5168\u5C40\u9762\u677F\"],[aria-label=\"Global panels\"]):has(> button:is([aria-label=\"\u63D2\u4EF6\"],[aria-label=\"Plugins\"]):only-child){display:none !important}\n";
3
+ /** Mark only the expanded brand row and its adjacent native New Session button. */
4
+ export declare function syncSidebarHeader(): void;
5
+ /** Restore native header styling when the branding effect is disposed. */
6
+ export declare function clearSidebarHeader(): void;
@@ -0,0 +1,9 @@
1
+ /** Font overrides for the conversation, preserving the live theme families. */
2
+ /**
3
+ * Apply the UI Tweaks size and spacing preferences to the host's prose tokens.
4
+ * @param fontSize - Selected body size in pixels.
5
+ * @param lineHeight - Vertical-spacing unit; 16 retains the normal rhythm.
6
+ * @param codeFontSize - Selected code-block size in pixels.
7
+ * @returns Theme-token overrides for a runtime stylesheet.
8
+ */
9
+ export declare function buildFontCss(fontSize: number, lineHeight: number, codeFontSize: number): string;
@@ -0,0 +1,2 @@
1
+ /** Standalone sign-in recovery; each document starts once and always keeps a usable reopen action. */
2
+ export declare const CHATCODE_LOGIN_PAGE_SCRIPT: string;
@@ -1,2 +1,4 @@
1
1
  import type { Context, SidebarHttpRequest } from './context-types.ts';
2
+ /** Render the standalone login gate with one automatic attempt and persistent manual recovery. */
3
+ export declare function renderChatcodeLoginPage(logo: string): string;
2
4
  export declare function registerLoginRoute(ctx: Context, fence: (req: SidebarHttpRequest) => boolean): () => void;
@@ -0,0 +1,5 @@
1
+ /** Shared config plugin routes inside the sidebar's existing trust fence. */
2
+ import type { Context } from './context-types.ts';
3
+ export declare function buildPluginMarketApi(ctx: Pick<Context, 'get'>, cwdOf: (payload: unknown) => Promise<{
4
+ cwd: string;
5
+ }>): Record<string, (payload: unknown) => Promise<unknown>>;
@@ -0,0 +1,37 @@
1
+ /** Credential-free structural contract of the config plugin's host service. */
2
+ export type PluginMarketScope = 'project' | 'user';
3
+ export interface PluginMarketplace {
4
+ id: string;
5
+ name: string;
6
+ description: string;
7
+ official: boolean;
8
+ }
9
+ export interface MarketplacePlugin {
10
+ id: string;
11
+ marketplaceId: string;
12
+ name: string;
13
+ description: string;
14
+ }
15
+ export interface InstalledMarketplacePlugin extends MarketplacePlugin {
16
+ scope: PluginMarketScope;
17
+ projectPath: string;
18
+ installPath: string;
19
+ version: string;
20
+ installedAt: string;
21
+ }
22
+ export interface MarketplaceCatalog {
23
+ marketplaces: PluginMarketplace[];
24
+ warning: string;
25
+ }
26
+ export interface PluginCatalog {
27
+ plugins: MarketplacePlugin[];
28
+ warning: string;
29
+ }
30
+ export interface PluginMarketService {
31
+ marketplaces(): Promise<MarketplaceCatalog>;
32
+ plugins(marketplaceId: string): Promise<MarketplacePlugin[]>;
33
+ catalog?(marketplaceId: string): Promise<PluginCatalog>;
34
+ installed(cwd: string): Promise<InstalledMarketplacePlugin[]>;
35
+ install(cwd: string, pluginId: string, scope: PluginMarketScope): Promise<InstalledMarketplacePlugin>;
36
+ uninstall(cwd: string, pluginId: string, scope: PluginMarketScope): Promise<void>;
37
+ }
@@ -22,10 +22,9 @@ export interface UITweaksConfig {
22
22
  */
23
23
  codeFontScale?: number;
24
24
  /**
25
- * Absolute code font size in px (8–32). 13 matches the stock DSH code block
26
- * at a 16px body. Drives the code block directly; inline code and the small
27
- * code font follow proportionally. When unset, falls back to the legacy
28
- * `codeFontScale` percentage, then to the stock default.
25
+ * Absolute code font size in px (8–32), default 13. Drives code blocks;
26
+ * inline code and small code follow proportionally. When unset, uses a non-default legacy
27
+ * `codeFontScale` percentage, otherwise 13px.
29
28
  */
30
29
  codeFontSize?: number;
31
30
  /**
@@ -106,7 +105,7 @@ export interface UITweaksConfig {
106
105
  }
107
106
  export declare const MIN_FONT_SIZE = 10;
108
107
  export declare const MAX_FONT_SIZE = 32;
109
- export declare const DEFAULT_FONT_SIZE = 16;
108
+ export declare const DEFAULT_FONT_SIZE = 14;
110
109
  /** 16px matches the stock DSH vertical rhythm (message/block gaps, etc.). */
111
110
  export declare const MIN_LINE_HEIGHT = 0;
112
111
  export declare const MAX_LINE_HEIGHT = 64;
@@ -115,7 +114,7 @@ export declare const DEFAULT_LINE_HEIGHT = 16;
115
114
  export declare const MIN_CODE_FONT_SCALE = 50;
116
115
  export declare const MAX_CODE_FONT_SCALE = 150;
117
116
  export declare const DEFAULT_CODE_FONT_SCALE = 81;
118
- /** Absolute code font size (px): 13 is the stock DSH code block at a 16px body. */
117
+ /** Default absolute code-block size, paired with 14px prose. */
119
118
  export declare const MIN_CODE_FONT_SIZE = 8;
120
119
  export declare const MAX_CODE_FONT_SIZE = 32;
121
120
  export declare const DEFAULT_CODE_FONT_SIZE = 13;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatcode/cco-better-sidebar",
3
- "version": "0.18.0-rc.3",
3
+ "version": "0.18.0-rc.5",
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": {
@@ -11,6 +11,7 @@ import { useEffect, useRef, useState } from 'react'
11
11
  import { api } from './api.ts'
12
12
  import { useChatcodeAccount } from './useChatcodeAccount.ts'
13
13
  import { redirectToChatcodeLogin } from './chatcode-login-route.ts'
14
+ import { LogoutConfirmDialog } from './LogoutConfirmDialog.tsx'
14
15
  import { t } from './locales.ts'
15
16
  import css from './sidebar.module.css'
16
17
 
@@ -75,6 +76,7 @@ function logout(): void {
75
76
  export function AccountMenu(): JSX.Element {
76
77
  const rootRef = useRef<HTMLDivElement>(null)
77
78
  const [open, setOpen] = useState(false)
79
+ const [logoutTarget, setLogoutTarget] = useState<'chatcode' | 'browser'>()
78
80
  const [userName, setUserName] = useState('User')
79
81
  const { chatcode, busy, error, loginUrl, startLogin, logout: logoutChatcode } = useChatcodeAccount()
80
82
 
@@ -157,11 +159,11 @@ export function AccountMenu(): JSX.Element {
157
159
  {canStartChatcodeLogin && <button type="button" role="menuitem" className={css.accountItem} disabled={busy} onClick={startLogin}>
158
160
  <LogoutIcon />{busy ? t('accountMenuChatcodeStarting') : t('accountMenuChatcodeLogin')}
159
161
  </button>}
160
- {hasChatcodeAccount && <button type="button" role="menuitem" className={css.accountItem} disabled={busy} onClick={logoutChatcode}>
162
+ {hasChatcodeAccount && <button type="button" role="menuitem" className={css.accountItem} disabled={busy} onClick={() => { setLogoutTarget('chatcode') }}>
161
163
  <LogoutIcon />{t('accountMenuLogout')}
162
164
  </button>}
163
165
  </>}
164
- {chatcode?.available === false && <button type="button" role="menuitem" className={css.accountItem} onClick={logout}>
166
+ {chatcode?.available === false && <button type="button" role="menuitem" className={css.accountItem} onClick={() => { setLogoutTarget('browser') }}>
165
167
  <LogoutIcon />{t('accountMenuLogout')}
166
168
  </button>}
167
169
  </div>
@@ -178,6 +180,14 @@ export function AccountMenu(): JSX.Element {
178
180
  <span className={css.accountName}>{chatcodeName}</span>
179
181
  <span className={css.accountPresence} style={{ opacity: signedIn ? 1 : 0.25 }} aria-hidden="true" />
180
182
  </button>
183
+ {logoutTarget !== undefined && <LogoutConfirmDialog
184
+ onCancel={() => { setLogoutTarget(undefined) }}
185
+ onConfirm={() => {
186
+ setLogoutTarget(undefined)
187
+ if (logoutTarget === 'chatcode') logoutChatcode()
188
+ else logout()
189
+ }}
190
+ />}
181
191
  </div>
182
192
  )
183
193
  }
@@ -0,0 +1,39 @@
1
+ import { useEffect, useId, useRef } from 'react'
2
+ import { t } from './locales.ts'
3
+ import css from './sidebar.module.css'
4
+
5
+ /** Native modal supplies an inert background and default Cancel focus until a choice is made. */
6
+ export function LogoutConfirmDialog({ onCancel, onConfirm }: {
7
+ onCancel: () => void
8
+ onConfirm: () => void
9
+ }): JSX.Element {
10
+ const ref = useRef<HTMLDialogElement>(null)
11
+ const titleId = useId()
12
+ const descriptionId = useId()
13
+
14
+ useEffect(() => {
15
+ const dialog = ref.current
16
+ dialog?.showModal()
17
+ return () => { if (dialog?.open) dialog.close() }
18
+ }, [])
19
+
20
+ return (
21
+ <dialog
22
+ ref={ref}
23
+ className={css.accountLogoutDialog}
24
+ role="alertdialog"
25
+ aria-modal="true"
26
+ aria-labelledby={titleId}
27
+ aria-describedby={descriptionId}
28
+ onCancel={event => { event.preventDefault(); onCancel() }}
29
+ onKeyDown={event => { event.stopPropagation() }}
30
+ >
31
+ <h2 id={titleId} className={css.accountLogoutTitle}>{t('accountMenuLogoutConfirmTitle')}</h2>
32
+ <p id={descriptionId} className={css.accountLogoutDescription}>{t('accountMenuLogoutConfirmDescription')}</p>
33
+ <div className={css.accountLogoutActions}>
34
+ <button type="button" autoFocus onClick={onCancel}>{t('cancel')}</button>
35
+ <button type="button" className={css.accountLogoutConfirm} onClick={onConfirm}>{t('accountMenuLogout')}</button>
36
+ </div>
37
+ </dialog>
38
+ )
39
+ }