@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.
- package/README.md +19 -1
- package/README_EN.md +19 -1
- package/lib/client-editor.js +326 -258
- package/lib/client-mermaid.js +312 -259
- package/lib/client-registry.js +1392 -605
- package/lib/client-terminal.js +328 -260
- package/lib/client.js +1352 -565
- package/lib/index.js +162 -5
- package/lib/types/client/LogoutConfirmDialog.d.ts +5 -0
- package/lib/types/client/PluginMarketSettingsSection.d.ts +7 -0
- package/lib/types/client/PluginMarketView.d.ts +7 -0
- package/lib/types/client/api.d.ts +15 -0
- package/lib/types/client/builtins/tabs.d.ts +6 -3
- package/lib/types/client/locales.d.ts +23 -0
- package/lib/types/client/plugin-market-catalog.d.ts +25 -0
- package/lib/types/client/ui-tweaks/branding.d.ts +1 -1
- package/lib/types/client/ui-tweaks/index.d.ts +1 -1
- package/lib/types/client/ui-tweaks/sidebar-header.d.ts +6 -0
- package/lib/types/client/ui-tweaks/typography.d.ts +9 -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-api.d.ts +5 -0
- package/lib/types/plugin-market-shared.d.ts +37 -0
- package/lib/types/ui-tweaks/config.d.ts +5 -6
- package/package.json +1 -1
- package/src/client/AccountMenu.tsx +12 -2
- package/src/client/LogoutConfirmDialog.tsx +39 -0
- package/src/client/PluginMarketSettingsSection.tsx +55 -0
- package/src/client/PluginMarketView.module.css +35 -0
- package/src/client/PluginMarketView.tsx +154 -0
- package/src/client/api.ts +7 -0
- package/src/client/builtins/tabs.tsx +16 -3
- package/src/client/index.tsx +10 -1
- package/src/client/locales-ja.ts +23 -0
- package/src/client/locales.ts +48 -0
- package/src/client/plugin-market-catalog.ts +90 -0
- package/src/client/sidebar.module.css +81 -19
- package/src/client/ui-tweaks/branding.tsx +25 -16
- package/src/client/ui-tweaks/index.tsx +36 -99
- package/src/client/ui-tweaks/sidebar-header.ts +73 -0
- package/src/client/ui-tweaks/typography.ts +38 -0
- package/src/index.ts +2 -0
- package/src/login-page-script.ts +107 -0
- package/src/login-route.ts +7 -4
- package/src/plugin-market-api.ts +42 -0
- package/src/plugin-market-shared.ts +21 -0
- package/src/ui-tweaks/config.ts +10 -10
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Standalone sign-in recovery; each document starts once and always keeps a usable reopen action. */
|
|
2
|
+
export const CHATCODE_LOGIN_PAGE_SCRIPT = String.raw`
|
|
3
|
+
(() => {
|
|
4
|
+
const message = document.getElementById('message');
|
|
5
|
+
const button = document.getElementById('login');
|
|
6
|
+
const fallback = document.getElementById('login-fallback');
|
|
7
|
+
const link = document.getElementById('login-link');
|
|
8
|
+
const current = new URL(location.href);
|
|
9
|
+
const returnTo = (() => {
|
|
10
|
+
const value = current.searchParams.get('return');
|
|
11
|
+
if (!value || !value.startsWith('/') || value.startsWith('//')) return '/';
|
|
12
|
+
const target = new URL(value, location.origin);
|
|
13
|
+
return target.origin === location.origin ? target.pathname + target.search + target.hash : '/';
|
|
14
|
+
})();
|
|
15
|
+
let busy = false, checking = false, attempted = false, recheck = false, attempt, popup;
|
|
16
|
+
const call = async name => {
|
|
17
|
+
const response = await fetch('/sidebar/api/' + name, {
|
|
18
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}',
|
|
19
|
+
});
|
|
20
|
+
const data = await response.json();
|
|
21
|
+
if (!response.ok || !data.ok) throw new Error('操作失败');
|
|
22
|
+
return data.value;
|
|
23
|
+
};
|
|
24
|
+
const showMessage = (text, error = false) => {
|
|
25
|
+
message.textContent = text;
|
|
26
|
+
message.classList.toggle('error', error);
|
|
27
|
+
};
|
|
28
|
+
async function login() {
|
|
29
|
+
if (busy) return;
|
|
30
|
+
attempted = true;
|
|
31
|
+
busy = true;
|
|
32
|
+
button.disabled = true;
|
|
33
|
+
button.textContent = '正在打开登录页…';
|
|
34
|
+
let tab = null;
|
|
35
|
+
try { tab = window.open('', '_blank'); if (tab) tab.opener = null; } catch {}
|
|
36
|
+
try {
|
|
37
|
+
if (!attempt) {
|
|
38
|
+
const started = await call('chatcode.start');
|
|
39
|
+
const url = new URL(started.url);
|
|
40
|
+
if (!['http:', 'https:'].includes(url.protocol) || typeof started.sessionId !== 'string') throw new Error('登录地址无效');
|
|
41
|
+
attempt = { sessionId: started.sessionId, url: url.href };
|
|
42
|
+
}
|
|
43
|
+
link.href = attempt.url;
|
|
44
|
+
link.textContent = attempt.url;
|
|
45
|
+
fallback.hidden = false;
|
|
46
|
+
if (tab && !tab.closed) {
|
|
47
|
+
try { tab.location.href = attempt.url; popup = tab; } catch { try { tab.close(); } catch {} }
|
|
48
|
+
}
|
|
49
|
+
showMessage('请在登录页完成登录;若网页已关闭,可随时重新打开。');
|
|
50
|
+
} catch {
|
|
51
|
+
try { if (tab) tab.close(); } catch {}
|
|
52
|
+
showMessage('登录页打开失败,请重试。', true);
|
|
53
|
+
} finally {
|
|
54
|
+
busy = false;
|
|
55
|
+
button.disabled = false;
|
|
56
|
+
button.textContent = attempt ? '重新打开登录页' : '去登录';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function status() {
|
|
60
|
+
if (checking || busy) return;
|
|
61
|
+
checking = true;
|
|
62
|
+
const observedAttempt = attempt, observedStarted = attempted;
|
|
63
|
+
try {
|
|
64
|
+
const state = await call('chatcode.status');
|
|
65
|
+
if (busy || attempt !== observedAttempt || attempted !== observedStarted) return;
|
|
66
|
+
if (state.available && state.validation === 'valid' && !state.expired) {
|
|
67
|
+
try { if (popup) popup.close(); window.focus(); } catch {}
|
|
68
|
+
location.replace(returnTo);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!state.available) {
|
|
72
|
+
recheck = true;
|
|
73
|
+
button.textContent = '重新检测';
|
|
74
|
+
showMessage('登录服务暂不可用,请稍后重试。', true);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (state.configured && state.validation === 'unavailable') {
|
|
78
|
+
recheck = true;
|
|
79
|
+
button.textContent = '重新检测';
|
|
80
|
+
showMessage('暂时无法验证已有账号,请检查网络后重新检测。', true);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
recheck = false;
|
|
84
|
+
button.textContent = attempt ? '重新打开登录页' : '去登录';
|
|
85
|
+
if (attempt && (state.login?.sessionId !== attempt.sessionId || state.login.state !== 'pending')) {
|
|
86
|
+
attempt = undefined;
|
|
87
|
+
fallback.hidden = true;
|
|
88
|
+
link.removeAttribute('href');
|
|
89
|
+
link.textContent = '';
|
|
90
|
+
button.textContent = '去登录';
|
|
91
|
+
showMessage('登录请求已失效,请重新登录。');
|
|
92
|
+
}
|
|
93
|
+
if (!attempted) await login();
|
|
94
|
+
} catch {
|
|
95
|
+
recheck = true;
|
|
96
|
+
button.textContent = '重新检测';
|
|
97
|
+
showMessage('无法读取登录状态,请稍后重试。', true);
|
|
98
|
+
} finally {
|
|
99
|
+
checking = false;
|
|
100
|
+
if (!busy) button.disabled = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
button.addEventListener('click', () => { void (recheck ? status() : login()); });
|
|
104
|
+
void status();
|
|
105
|
+
setInterval(() => { void status(); }, 2000);
|
|
106
|
+
})();
|
|
107
|
+
`
|
package/src/login-route.ts
CHANGED
|
@@ -2,14 +2,17 @@ import { readFile } from 'node:fs/promises'
|
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import type { Context, SidebarHttpRequest, SidebarHttpResponse } from './context-types.ts'
|
|
5
|
+
import { CHATCODE_LOGIN_PAGE_SCRIPT } from './login-page-script.ts'
|
|
5
6
|
|
|
6
7
|
const assetPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'assets', 'logo.svg')
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
/** Render the standalone login gate with one automatic attempt and persistent manual recovery. */
|
|
10
|
+
export function renderChatcodeLoginPage(logo: string): string {
|
|
9
11
|
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>
|
|
10
12
|
*{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}}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
.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}
|
|
14
|
+
</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>
|
|
15
|
+
${CHATCODE_LOGIN_PAGE_SCRIPT}
|
|
13
16
|
</script></body></html>`
|
|
14
17
|
}
|
|
15
18
|
|
|
@@ -20,7 +23,7 @@ export function registerLoginRoute(ctx: Context, fence: (req: SidebarHttpRequest
|
|
|
20
23
|
try {
|
|
21
24
|
const logo = (await readFile(assetPath)).toString('base64')
|
|
22
25
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'content-security-policy': "default-src 'self'; img-src 'self' data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'" })
|
|
23
|
-
res.end(req.method === 'HEAD' ? undefined :
|
|
26
|
+
res.end(req.method === 'HEAD' ? undefined : renderChatcodeLoginPage(logo))
|
|
24
27
|
} catch { res.writeHead(500); res.end('login page unavailable') }
|
|
25
28
|
} })
|
|
26
29
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Shared config plugin routes inside the sidebar's existing trust fence. */
|
|
2
|
+
import type { Context } from './context-types.ts'
|
|
3
|
+
import type { PluginMarketService, PluginMarketScope } from './plugin-market-shared.ts'
|
|
4
|
+
import { requireString, SidebarError } from './wire.ts'
|
|
5
|
+
|
|
6
|
+
export function buildPluginMarketApi(ctx: Pick<Context, 'get'>, cwdOf: (payload: unknown) => Promise<{ cwd: string }>): Record<string, (payload: unknown) => Promise<unknown>> {
|
|
7
|
+
const service = (): PluginMarketService => {
|
|
8
|
+
const value = ctx.get('chatcodePluginMarket', false) as PluginMarketService | undefined
|
|
9
|
+
if (!value) throw new SidebarError('chatcode-unavailable', '插件市场服务未挂载,请更新并启用 ChatCode config 插件。', 503)
|
|
10
|
+
return value
|
|
11
|
+
}
|
|
12
|
+
const scopeOf = (payload: unknown): PluginMarketScope => {
|
|
13
|
+
const scope = requireString(payload, 'scope')
|
|
14
|
+
if (scope !== 'project' && scope !== 'user') throw new SidebarError('bad-request', 'scope must be project or user')
|
|
15
|
+
return scope
|
|
16
|
+
}
|
|
17
|
+
// Settings can be opened before a conversation exists. In that case the
|
|
18
|
+
// host launch directory is the project; never take a sessionless cwd from
|
|
19
|
+
// the browser. Conversations still use the existing authoritative resolver.
|
|
20
|
+
const contextOf = async (payload: unknown): Promise<{ sessionId: string; cwd: string }> => {
|
|
21
|
+
const sessionId = (payload as { sessionId?: unknown } | null)?.sessionId
|
|
22
|
+
if (sessionId === undefined || sessionId === '') return { sessionId: '', cwd: process.cwd() }
|
|
23
|
+
requireString(payload, 'sessionId')
|
|
24
|
+
return { sessionId: sessionId as string, cwd: (await cwdOf(payload)).cwd }
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
'plugins.context': contextOf,
|
|
28
|
+
'plugins.marketplaces': () => service().marketplaces(),
|
|
29
|
+
'plugins.catalog': async payload => {
|
|
30
|
+
const market = service()
|
|
31
|
+
const id = requireString(payload, 'marketplaceId')
|
|
32
|
+
if ((payload as { includeMetadata?: unknown }).includeMetadata !== true) return market.plugins(id)
|
|
33
|
+
return market.catalog ? market.catalog(id) : { plugins: await market.plugins(id), warning: '' }
|
|
34
|
+
},
|
|
35
|
+
'plugins.installed': async payload => service().installed((await contextOf(payload)).cwd),
|
|
36
|
+
'plugins.install': async payload => service().install((await contextOf(payload)).cwd, requireString(payload, 'pluginId'), scopeOf(payload)),
|
|
37
|
+
'plugins.uninstall': async payload => {
|
|
38
|
+
await service().uninstall((await contextOf(payload)).cwd, requireString(payload, 'pluginId'), scopeOf(payload))
|
|
39
|
+
return { ok: true }
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Credential-free structural contract of the config plugin's host service. */
|
|
2
|
+
export type PluginMarketScope = 'project' | 'user'
|
|
3
|
+
export interface PluginMarketplace { id: string; name: string; description: string; official: boolean }
|
|
4
|
+
export interface MarketplacePlugin { id: string; marketplaceId: string; name: string; description: string }
|
|
5
|
+
export interface InstalledMarketplacePlugin extends MarketplacePlugin {
|
|
6
|
+
scope: PluginMarketScope
|
|
7
|
+
projectPath: string
|
|
8
|
+
installPath: string
|
|
9
|
+
version: string
|
|
10
|
+
installedAt: string
|
|
11
|
+
}
|
|
12
|
+
export interface MarketplaceCatalog { marketplaces: PluginMarketplace[]; warning: string }
|
|
13
|
+
export interface PluginCatalog { plugins: MarketplacePlugin[]; warning: string }
|
|
14
|
+
export interface PluginMarketService {
|
|
15
|
+
marketplaces(): Promise<MarketplaceCatalog>
|
|
16
|
+
plugins(marketplaceId: string): Promise<MarketplacePlugin[]>
|
|
17
|
+
catalog?(marketplaceId: string): Promise<PluginCatalog>
|
|
18
|
+
installed(cwd: string): Promise<InstalledMarketplacePlugin[]>
|
|
19
|
+
install(cwd: string, pluginId: string, scope: PluginMarketScope): Promise<InstalledMarketplacePlugin>
|
|
20
|
+
uninstall(cwd: string, pluginId: string, scope: PluginMarketScope): Promise<void>
|
|
21
|
+
}
|
package/src/ui-tweaks/config.ts
CHANGED
|
@@ -29,10 +29,9 @@ export interface UITweaksConfig {
|
|
|
29
29
|
*/
|
|
30
30
|
codeFontScale?: number
|
|
31
31
|
/**
|
|
32
|
-
* Absolute code font size in px (8–32)
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* `codeFontScale` percentage, then to the stock default.
|
|
32
|
+
* Absolute code font size in px (8–32), default 13. Drives code blocks;
|
|
33
|
+
* inline code and small code follow proportionally. When unset, uses a non-default legacy
|
|
34
|
+
* `codeFontScale` percentage, otherwise 13px.
|
|
36
35
|
*/
|
|
37
36
|
codeFontSize?: number
|
|
38
37
|
/**
|
|
@@ -114,7 +113,7 @@ export interface UITweaksConfig {
|
|
|
114
113
|
|
|
115
114
|
export const MIN_FONT_SIZE = 10
|
|
116
115
|
export const MAX_FONT_SIZE = 32
|
|
117
|
-
export const DEFAULT_FONT_SIZE =
|
|
116
|
+
export const DEFAULT_FONT_SIZE = 14
|
|
118
117
|
|
|
119
118
|
/** 16px matches the stock DSH vertical rhythm (message/block gaps, etc.). */
|
|
120
119
|
export const MIN_LINE_HEIGHT = 0
|
|
@@ -126,7 +125,7 @@ export const MIN_CODE_FONT_SCALE = 50
|
|
|
126
125
|
export const MAX_CODE_FONT_SCALE = 150
|
|
127
126
|
export const DEFAULT_CODE_FONT_SCALE = 81
|
|
128
127
|
|
|
129
|
-
/**
|
|
128
|
+
/** Default absolute code-block size, paired with 14px prose. */
|
|
130
129
|
export const MIN_CODE_FONT_SIZE = 8
|
|
131
130
|
export const MAX_CODE_FONT_SIZE = 32
|
|
132
131
|
export const DEFAULT_CODE_FONT_SIZE = 13
|
|
@@ -164,7 +163,7 @@ export const DEFAULT_NOTIFY_SOUND = false
|
|
|
164
163
|
|
|
165
164
|
/** Configuration schema with documented defaults. */
|
|
166
165
|
export const Config: Schema<UITweaksConfig> = z.object({
|
|
167
|
-
fontSize: z.number().min(10).max(32).default(
|
|
166
|
+
fontSize: z.number().min(10).max(32).default(DEFAULT_FONT_SIZE),
|
|
168
167
|
codeFontScale: z.number().min(MIN_CODE_FONT_SCALE).max(MAX_CODE_FONT_SCALE).default(DEFAULT_CODE_FONT_SCALE),
|
|
169
168
|
codeFontSize: z.number().min(MIN_CODE_FONT_SIZE).max(MAX_CODE_FONT_SIZE),
|
|
170
169
|
lineHeight: z.number().min(MIN_LINE_HEIGHT).max(MAX_LINE_HEIGHT).default(DEFAULT_LINE_HEIGHT),
|
|
@@ -229,11 +228,12 @@ export interface ResolvedUITweaksConfig {
|
|
|
229
228
|
export function resolveConfig(config: UITweaksConfig = {}): ResolvedUITweaksConfig {
|
|
230
229
|
const fontSize = config.fontSize ?? DEFAULT_FONT_SIZE
|
|
231
230
|
const codeFontScale = config.codeFontScale ?? DEFAULT_CODE_FONT_SCALE
|
|
232
|
-
//
|
|
233
|
-
// the legacy percentage at the resolved body size; otherwise stock.
|
|
231
|
+
// Explicit code sizing wins; the stock preference keeps readable 13px code.
|
|
234
232
|
const codeFontSize = typeof config.codeFontSize === 'number'
|
|
235
233
|
? Math.min(MAX_CODE_FONT_SIZE, Math.max(MIN_CODE_FONT_SIZE, config.codeFontSize))
|
|
236
|
-
:
|
|
234
|
+
: codeFontScale === DEFAULT_CODE_FONT_SCALE
|
|
235
|
+
? DEFAULT_CODE_FONT_SIZE
|
|
236
|
+
: Math.max(8, Math.round(fontSize * (13 / 16) * (codeFontScale / DEFAULT_CODE_FONT_SCALE)))
|
|
237
237
|
const lineHeight = config.lineHeight ?? DEFAULT_LINE_HEIGHT
|
|
238
238
|
const tableStyle = config.tableStyle ?? 'default'
|
|
239
239
|
const gitBarEnabled = config.gitBarEnabled ?? DEFAULT_GITBAR_ENABLED
|