@chatcode/cco-better-sidebar 0.18.0-rc.4 → 0.18.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/README_EN.md +15 -1
- package/lib/client-editor.js +338 -266
- package/lib/client-mermaid.js +328 -260
- package/lib/client-registry.js +1049 -542
- package/lib/client-terminal.js +337 -265
- package/lib/client.js +1048 -541
- package/lib/index.js +162 -11
- package/lib/types/client/LogoutConfirmDialog.d.ts +5 -0
- package/lib/types/client/PluginProjectPicker.d.ts +13 -0
- package/lib/types/client/api.d.ts +7 -5
- package/lib/types/client/locales.d.ts +14 -0
- package/lib/types/client/ui-tweaks/branding.d.ts +1 -1
- package/lib/types/client/ui-tweaks/sidebar-header.d.ts +6 -0
- package/lib/types/login-page-script.d.ts +2 -0
- package/lib/types/login-route.d.ts +2 -0
- package/lib/types/plugin-market-shared.d.ts +22 -0
- package/package.json +1 -1
- package/src/client/AccountMenu.tsx +12 -2
- package/src/client/LogoutConfirmDialog.tsx +39 -0
- package/src/client/PluginMarketView.module.css +14 -0
- package/src/client/PluginMarketView.tsx +86 -24
- package/src/client/PluginProjectPicker.tsx +73 -0
- package/src/client/api.ts +7 -5
- package/src/client/locales-ja.ts +15 -1
- package/src/client/locales.ts +66 -3
- package/src/client/sidebar.module.css +81 -19
- package/src/client/ui-tweaks/branding.tsx +10 -9
- package/src/client/ui-tweaks/sidebar-header.ts +73 -0
- package/src/login-page-script.ts +107 -0
- package/src/login-route.ts +7 -4
- package/src/plugin-market-api.ts +55 -9
- package/src/plugin-market-shared.ts +9 -0
package/lib/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
874
|
-
|
|
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 :
|
|
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");
|
|
@@ -5894,20 +6005,50 @@ function buildPluginMarketApi(ctx, cwdOf) {
|
|
|
5894
6005
|
if (scope !== "project" && scope !== "user") throw new SidebarError("bad-request", "scope must be project or user");
|
|
5895
6006
|
return scope;
|
|
5896
6007
|
};
|
|
6008
|
+
const selectedProject = async (payload) => {
|
|
6009
|
+
if (typeof payload?.projectPath !== "string") throw new SidebarError("bad-request", "请先选择目标项目目录。");
|
|
6010
|
+
const path = requireString(payload, "projectPath");
|
|
6011
|
+
const qualified = process.platform !== "win32" || /^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/][^\\/]+)/.test(path);
|
|
6012
|
+
if (!isAbsolute(path) || !qualified || path.includes("\0")) throw new SidebarError("bad-request", "请选择有效的项目目录。");
|
|
6013
|
+
const canonical = await realpath(path);
|
|
6014
|
+
if (!(await stat(canonical)).isDirectory()) throw new SidebarError("bad-request", "请选择项目文件夹。");
|
|
6015
|
+
return canonical;
|
|
6016
|
+
};
|
|
6017
|
+
const picker = () => {
|
|
6018
|
+
const value = ctx.get("directoryPicker", false);
|
|
6019
|
+
if (!value) throw new SidebarError("method-error", "目录选择器未挂载,请启用宿主目录选择插件。", 503);
|
|
6020
|
+
return value.capability();
|
|
6021
|
+
};
|
|
5897
6022
|
const contextOf = async (payload) => {
|
|
5898
6023
|
const sessionId = payload?.sessionId;
|
|
5899
|
-
if (sessionId === void 0 || sessionId === "") return {
|
|
5900
|
-
sessionId: "",
|
|
5901
|
-
cwd: process.cwd()
|
|
5902
|
-
};
|
|
6024
|
+
if (sessionId === void 0 || sessionId === "") return { sessionId: "" };
|
|
5903
6025
|
requireString(payload, "sessionId");
|
|
5904
6026
|
return {
|
|
5905
6027
|
sessionId,
|
|
5906
6028
|
cwd: (await cwdOf(payload)).cwd
|
|
5907
6029
|
};
|
|
5908
6030
|
};
|
|
6031
|
+
const projectOf = async (payload) => {
|
|
6032
|
+
if (payload?.projectPath !== void 0) return selectedProject(payload);
|
|
6033
|
+
return (await contextOf(payload)).cwd;
|
|
6034
|
+
};
|
|
5909
6035
|
return {
|
|
5910
6036
|
"plugins.context": contextOf,
|
|
6037
|
+
"plugins.project.pick": async () => {
|
|
6038
|
+
const capability = picker();
|
|
6039
|
+
if (capability.kind === "browse") return { kind: "browse" };
|
|
6040
|
+
const path = await capability.pick(AbortSignal.timeout(12e4));
|
|
6041
|
+
return {
|
|
6042
|
+
kind: "native",
|
|
6043
|
+
path: path === null ? null : await selectedProject({ projectPath: path })
|
|
6044
|
+
};
|
|
6045
|
+
},
|
|
6046
|
+
"plugins.project.directories": async (payload) => {
|
|
6047
|
+
const capability = picker();
|
|
6048
|
+
if (capability.kind !== "browse") throw new SidebarError("method-error", "当前宿主使用系统文件夹选择窗口。");
|
|
6049
|
+
const path = payload?.path;
|
|
6050
|
+
return capability.list(path === void 0 ? void 0 : requireString(payload, "path"), AbortSignal.timeout(15e3));
|
|
6051
|
+
},
|
|
5911
6052
|
"plugins.marketplaces": () => service().marketplaces(),
|
|
5912
6053
|
"plugins.catalog": async (payload) => {
|
|
5913
6054
|
const market = service();
|
|
@@ -5918,10 +6059,20 @@ function buildPluginMarketApi(ctx, cwdOf) {
|
|
|
5918
6059
|
warning: ""
|
|
5919
6060
|
};
|
|
5920
6061
|
},
|
|
5921
|
-
"plugins.installed": async (payload) =>
|
|
5922
|
-
|
|
6062
|
+
"plugins.installed": async (payload) => {
|
|
6063
|
+
const project = await projectOf(payload);
|
|
6064
|
+
const records = await service().installed(project ?? process.cwd());
|
|
6065
|
+
return project ? records : records.filter((record) => record.scope === "user");
|
|
6066
|
+
},
|
|
6067
|
+
"plugins.install": async (payload) => {
|
|
6068
|
+
const scope = scopeOf(payload);
|
|
6069
|
+
const project = scope === "project" ? await selectedProject(payload) : await projectOf(payload);
|
|
6070
|
+
return service().install(project ?? process.cwd(), requireString(payload, "pluginId"), scope);
|
|
6071
|
+
},
|
|
5923
6072
|
"plugins.uninstall": async (payload) => {
|
|
5924
|
-
|
|
6073
|
+
const scope = scopeOf(payload);
|
|
6074
|
+
const project = scope === "project" ? await selectedProject(payload) : await projectOf(payload);
|
|
6075
|
+
await service().uninstall(project ?? process.cwd(), requireString(payload, "pluginId"), scope);
|
|
5925
6076
|
return { ok: true };
|
|
5926
6077
|
}
|
|
5927
6078
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PluginProjectPick } from '../plugin-market-shared.ts';
|
|
2
|
+
export interface PluginProjectRequest {
|
|
3
|
+
result: Promise<PluginProjectPick>;
|
|
4
|
+
controller: AbortController;
|
|
5
|
+
finish: (path: string | null) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function PluginProjectPicker({ request, initialPath, onPicked, onCancel, onError }: {
|
|
8
|
+
request: PluginProjectRequest;
|
|
9
|
+
initialPath?: string;
|
|
10
|
+
onPicked: (path: string) => void;
|
|
11
|
+
onCancel: () => void;
|
|
12
|
+
onError: (message: string) => void;
|
|
13
|
+
}): JSX.Element | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BrowserProbeResult } from './browser.ts';
|
|
2
|
-
import type { MarketplaceCatalog, MarketplacePlugin, InstalledMarketplacePlugin, PluginMarketScope } from '../plugin-market-shared.ts';
|
|
2
|
+
import type { MarketplaceCatalog, MarketplacePlugin, InstalledMarketplacePlugin, PluginMarketScope, PluginProjectPick, PluginProjectDirectories } from '../plugin-market-shared.ts';
|
|
3
3
|
/** One wire failure. */
|
|
4
4
|
export declare class SidebarApiError extends Error {
|
|
5
5
|
readonly code: string;
|
|
@@ -187,16 +187,18 @@ export interface SessionScope {
|
|
|
187
187
|
export declare const api: {
|
|
188
188
|
pluginMarketContext: (scope: SessionScope, signal?: AbortSignal) => Promise<{
|
|
189
189
|
sessionId: string;
|
|
190
|
-
cwd
|
|
190
|
+
cwd?: string;
|
|
191
191
|
}>;
|
|
192
|
+
pluginProjectPick: (signal?: AbortSignal) => Promise<PluginProjectPick>;
|
|
193
|
+
pluginProjectDirectories: (path?: string, signal?: AbortSignal) => Promise<PluginProjectDirectories>;
|
|
192
194
|
pluginMarketplaces: (signal?: AbortSignal) => Promise<MarketplaceCatalog>;
|
|
193
195
|
pluginCatalog: (marketplaceId: string, signal?: AbortSignal) => Promise<MarketplacePlugin[] | {
|
|
194
196
|
plugins: MarketplacePlugin[];
|
|
195
197
|
warning: string;
|
|
196
198
|
}>;
|
|
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<{
|
|
199
|
+
pluginsInstalled: (scope: SessionScope, signal?: AbortSignal, projectPath?: string) => Promise<InstalledMarketplacePlugin[]>;
|
|
200
|
+
pluginInstall: (scope: SessionScope, pluginId: string, installScope: PluginMarketScope, projectPath?: string) => Promise<InstalledMarketplacePlugin>;
|
|
201
|
+
pluginUninstall: (scope: SessionScope, pluginId: string, installScope: PluginMarketScope, projectPath?: string) => Promise<{
|
|
200
202
|
ok: true;
|
|
201
203
|
}>;
|
|
202
204
|
chatcodeStatus: (signal?: AbortSignal) => Promise<ChatCodeAuthState>;
|
|
@@ -37,14 +37,28 @@ export declare const zh: {
|
|
|
37
37
|
pluginMarketInstalled: string;
|
|
38
38
|
pluginMarketUninstalled: string;
|
|
39
39
|
pluginMarketSubtitle: string;
|
|
40
|
+
pluginMarketChooseProject: string;
|
|
41
|
+
pluginMarketProjectHint: string;
|
|
42
|
+
pluginMarketChoosingDirectory: string;
|
|
43
|
+
pluginMarketInstalledAt: string;
|
|
44
|
+
pluginMarketInstallPath: string;
|
|
45
|
+
pluginMarketDirectoryPath: string;
|
|
46
|
+
pluginMarketOpenDirectory: string;
|
|
47
|
+
pluginMarketSelectDirectory: string;
|
|
48
|
+
pluginMarketHomeDirectory: string;
|
|
49
|
+
pluginMarketDirectoryTruncated: string;
|
|
40
50
|
pluginMarketSearch: string;
|
|
41
51
|
pluginMarketDefault: string;
|
|
42
52
|
pluginMarketBrowse: string;
|
|
43
53
|
pluginMarketNoDescription: string;
|
|
44
54
|
pluginMarketInstalling: string;
|
|
45
55
|
pluginMarketUninstalling: string;
|
|
56
|
+
pluginMarketInstallProgress: string;
|
|
57
|
+
pluginMarketUninstallProgress: string;
|
|
46
58
|
accountMenuSettings: string;
|
|
47
59
|
accountMenuLogout: string;
|
|
60
|
+
accountMenuLogoutConfirmTitle: string;
|
|
61
|
+
accountMenuLogoutConfirmDescription: string;
|
|
48
62
|
accountMenuLabel: string;
|
|
49
63
|
updateMenu: string;
|
|
50
64
|
updateNav: string;
|
|
@@ -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:
|
|
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;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -27,6 +27,28 @@ export interface PluginCatalog {
|
|
|
27
27
|
plugins: MarketplacePlugin[];
|
|
28
28
|
warning: string;
|
|
29
29
|
}
|
|
30
|
+
/** Host-owned paths for the remote Web directory browser. */
|
|
31
|
+
export interface PluginProjectDirectories {
|
|
32
|
+
path: string;
|
|
33
|
+
home: string;
|
|
34
|
+
crumbs: Array<{
|
|
35
|
+
name: string;
|
|
36
|
+
path: string;
|
|
37
|
+
hidden: boolean;
|
|
38
|
+
}>;
|
|
39
|
+
entries: Array<{
|
|
40
|
+
name: string;
|
|
41
|
+
path: string;
|
|
42
|
+
hidden: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
truncated: boolean;
|
|
45
|
+
}
|
|
46
|
+
export type PluginProjectPick = {
|
|
47
|
+
kind: 'native';
|
|
48
|
+
path: string | null;
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'browse';
|
|
51
|
+
};
|
|
30
52
|
export interface PluginMarketService {
|
|
31
53
|
marketplaces(): Promise<MarketplaceCatalog>;
|
|
32
54
|
plugins(marketplaceId: string): Promise<MarketplacePlugin[]>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatcode/cco-better-sidebar",
|
|
3
|
-
"version": "0.18.0-rc.
|
|
3
|
+
"version": "0.18.0-rc.6",
|
|
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={
|
|
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={
|
|
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
|
+
}
|
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
.title { font-size: 16px; line-height: 24px; font-weight: 600; margin: 0; }
|
|
5
5
|
.subtitle, .project, .meta, .state { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); margin: 2px 0; }
|
|
6
6
|
.project { overflow-wrap: anywhere; }
|
|
7
|
+
.projectRow { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
|
8
|
+
.projectRow > p { flex: 1; min-width: min(100%, 200px); }
|
|
9
|
+
.projectRow > button { flex-shrink: 0; }
|
|
10
|
+
.installPath { margin: 4px 0; overflow-wrap: anywhere; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
|
|
11
|
+
.projectPicker { width: min(620px, calc(100vw - 32px)); }
|
|
12
|
+
.directoryPath { display: flex; align-items: center; gap: 8px; }
|
|
13
|
+
.directoryPath > span { flex: 1; min-width: 0; }
|
|
14
|
+
.directoryCrumbs { display: flex; flex-wrap: wrap; gap: 4px; margin: 12px 0; }
|
|
15
|
+
.directoryList { max-height: 320px; overflow: auto; min-height: 100px; }
|
|
16
|
+
.directoryList > button { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px; border: 0; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; text-align: left; cursor: pointer; overflow-wrap: anywhere; }
|
|
17
|
+
.directoryList > button:hover, .directoryList > button:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
|
|
7
18
|
.toolbar { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px; margin: 16px 0 12px; }
|
|
8
19
|
.tabs { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
|
|
9
20
|
.tabs button { white-space: nowrap; }
|
|
@@ -11,6 +22,9 @@
|
|
|
11
22
|
.breadcrumb { display: flex; align-items: center; gap: 8px; margin: 4px 0 12px; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; }
|
|
12
23
|
.breadcrumb button { flex-shrink: 0; white-space: nowrap; }
|
|
13
24
|
.breadcrumb > span:last-child { min-width: 0; }
|
|
25
|
+
.operation { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; gap: 10px; padding: 16px; margin: 12px 0; border: 1px solid var(--dsw-alias-state-business-primary); border-radius: 10px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-state-business-primary); font-weight: 500; }
|
|
26
|
+
.operation > span:first-child { flex-shrink: 0; }
|
|
27
|
+
.operation > span:last-child { min-width: 0; overflow-wrap: anywhere; }
|
|
14
28
|
.warning { padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; margin: 10px 0; font-size: 12px; color: var(--dsw-alias-label-secondary); }
|
|
15
29
|
.error, .notice { font-size: 12px; padding: 8px 10px; border-radius: 8px; margin: 10px 0; overflow-wrap: anywhere; }
|
|
16
30
|
.error { color: var(--dsw-alias-state-danger-primary); background: var(--dsw-alias-state-danger-bg); }
|