@chatcode/cco-better-sidebar 0.18.0-rc.3 → 0.18.0-rc.4
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 +5 -1
- package/README_EN.md +5 -1
- package/lib/client-editor.js +315 -258
- package/lib/client-mermaid.js +298 -256
- package/lib/client-registry.js +1097 -460
- package/lib/client-terminal.js +318 -261
- package/lib/client.js +1110 -473
- package/lib/index.js +47 -1
- 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 +21 -0
- package/lib/types/client/plugin-market-catalog.d.ts +25 -0
- package/lib/types/client/ui-tweaks/index.d.ts +1 -1
- package/lib/types/client/ui-tweaks/typography.d.ts +9 -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/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 +21 -0
- package/src/client/locales.ts +42 -0
- package/src/client/plugin-market-catalog.ts +90 -0
- package/src/client/ui-tweaks/branding.tsx +15 -7
- package/src/client/ui-tweaks/index.tsx +36 -99
- package/src/client/ui-tweaks/typography.ts +38 -0
- package/src/index.ts +2 -0
- package/src/plugin-market-api.ts +42 -0
- package/src/plugin-market-shared.ts +21 -0
- 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(
|
|
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),
|
|
@@ -5882,6 +5882,51 @@ function apply$1(ctx) {
|
|
|
5882
5882
|
ctx.logger.info("[ChatCode CLI UI Tweaks] settings namespace registered and Web routes mounted");
|
|
5883
5883
|
}
|
|
5884
5884
|
//#endregion
|
|
5885
|
+
//#region src/plugin-market-api.ts
|
|
5886
|
+
function buildPluginMarketApi(ctx, cwdOf) {
|
|
5887
|
+
const service = () => {
|
|
5888
|
+
const value = ctx.get("chatcodePluginMarket", false);
|
|
5889
|
+
if (!value) throw new SidebarError("chatcode-unavailable", "插件市场服务未挂载,请更新并启用 ChatCode config 插件。", 503);
|
|
5890
|
+
return value;
|
|
5891
|
+
};
|
|
5892
|
+
const scopeOf = (payload) => {
|
|
5893
|
+
const scope = requireString(payload, "scope");
|
|
5894
|
+
if (scope !== "project" && scope !== "user") throw new SidebarError("bad-request", "scope must be project or user");
|
|
5895
|
+
return scope;
|
|
5896
|
+
};
|
|
5897
|
+
const contextOf = async (payload) => {
|
|
5898
|
+
const sessionId = payload?.sessionId;
|
|
5899
|
+
if (sessionId === void 0 || sessionId === "") return {
|
|
5900
|
+
sessionId: "",
|
|
5901
|
+
cwd: process.cwd()
|
|
5902
|
+
};
|
|
5903
|
+
requireString(payload, "sessionId");
|
|
5904
|
+
return {
|
|
5905
|
+
sessionId,
|
|
5906
|
+
cwd: (await cwdOf(payload)).cwd
|
|
5907
|
+
};
|
|
5908
|
+
};
|
|
5909
|
+
return {
|
|
5910
|
+
"plugins.context": contextOf,
|
|
5911
|
+
"plugins.marketplaces": () => service().marketplaces(),
|
|
5912
|
+
"plugins.catalog": async (payload) => {
|
|
5913
|
+
const market = service();
|
|
5914
|
+
const id = requireString(payload, "marketplaceId");
|
|
5915
|
+
if (payload.includeMetadata !== true) return market.plugins(id);
|
|
5916
|
+
return market.catalog ? market.catalog(id) : {
|
|
5917
|
+
plugins: await market.plugins(id),
|
|
5918
|
+
warning: ""
|
|
5919
|
+
};
|
|
5920
|
+
},
|
|
5921
|
+
"plugins.installed": async (payload) => service().installed((await contextOf(payload)).cwd),
|
|
5922
|
+
"plugins.install": async (payload) => service().install((await contextOf(payload)).cwd, requireString(payload, "pluginId"), scopeOf(payload)),
|
|
5923
|
+
"plugins.uninstall": async (payload) => {
|
|
5924
|
+
await service().uninstall((await contextOf(payload)).cwd, requireString(payload, "pluginId"), scopeOf(payload));
|
|
5925
|
+
return { ok: true };
|
|
5926
|
+
}
|
|
5927
|
+
};
|
|
5928
|
+
}
|
|
5929
|
+
//#endregion
|
|
5885
5930
|
//#region src/index.ts
|
|
5886
5931
|
/**
|
|
5887
5932
|
* dsh-better-sidebar host half: the /sidebar JSON API (explorer listing, file
|
|
@@ -6197,6 +6242,7 @@ function buildApi(ctx, ptyManager, agentPtyRegistry, resolved, terminalShell, ge
|
|
|
6197
6242
|
parent: parentOf(cwd) ?? null
|
|
6198
6243
|
};
|
|
6199
6244
|
},
|
|
6245
|
+
...buildPluginMarketApi(ctx, cwdOf),
|
|
6200
6246
|
"fs.tree": async (payload) => {
|
|
6201
6247
|
const { cwd } = await cwdOf(payload);
|
|
6202
6248
|
return listDirectory(payload.path === void 0 ? cwd : await ensureWorkspacePath(cwd, requireString(payload, "path"), fenceEnabledOf(getSettings)), resolved.listLimit);
|
|
@@ -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
|
|
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
|
|
22
|
+
/** The 6 built-in tab descriptors. */
|
|
20
23
|
export declare function builtinTabs(ctx: Context, options?: BuiltinTabOptions): readonly TabDescriptor[];
|
|
@@ -22,6 +22,27 @@
|
|
|
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;
|
|
27
48
|
accountMenuLabel: 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 {};
|
|
@@ -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
|
|
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,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,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)
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
+
"version": "0.18.0-rc.4",
|
|
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": {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Replace the product's market settings seat through the public slot API. */
|
|
2
|
+
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react'
|
|
3
|
+
import type { Context } from '../context-types.ts'
|
|
4
|
+
import { api, type SessionScope } from './api.ts'
|
|
5
|
+
import { useCurrentSessionId } from './current-session.ts'
|
|
6
|
+
import { PluginMarketView } from './PluginMarketView.tsx'
|
|
7
|
+
import { RenderBoundary } from './RenderBoundary.tsx'
|
|
8
|
+
import { t } from './locales.ts'
|
|
9
|
+
import type { PluginMarketCatalog } from './plugin-market-catalog.ts'
|
|
10
|
+
|
|
11
|
+
export function PluginMarketSettingsSection({ ctx, catalog }: { ctx: Context; catalog: PluginMarketCatalog }): JSX.Element {
|
|
12
|
+
const sessionId = useCurrentSessionId(ctx) ?? ''
|
|
13
|
+
const sessions = useSyncExternalStore(
|
|
14
|
+
useCallback(listener => ctx.sessions.list.subscribe(listener), [ctx]),
|
|
15
|
+
useCallback(() => ctx.sessions.list.getSnapshot(), [ctx]),
|
|
16
|
+
)
|
|
17
|
+
useSyncExternalStore(
|
|
18
|
+
useCallback(listener => ctx.locale.subscribe(listener), [ctx]),
|
|
19
|
+
useCallback(() => ctx.locale.getSnapshot(), [ctx]),
|
|
20
|
+
)
|
|
21
|
+
const cwd = sessions.byId[sessionId]?.cwd
|
|
22
|
+
const key = `${sessionId}\0${cwd ?? ''}`
|
|
23
|
+
const [context, setContext] = useState<{ key: string; scope: SessionScope }>()
|
|
24
|
+
const [error, setError] = useState('')
|
|
25
|
+
const [revision, setRevision] = useState(0)
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const controller = new AbortController()
|
|
28
|
+
let active = true
|
|
29
|
+
setError('')
|
|
30
|
+
void api.pluginMarketContext({ sessionId, cwd }, controller.signal)
|
|
31
|
+
.then(scope => { if (active) setContext({ key, scope }) })
|
|
32
|
+
.catch(cause => { if (active) setError(cause instanceof Error ? cause.message : String(cause)) })
|
|
33
|
+
return () => { active = false; controller.abort() }
|
|
34
|
+
}, [sessionId, cwd, revision])
|
|
35
|
+
|
|
36
|
+
return <div data-chatcode-plugin-market-settings>
|
|
37
|
+
{context?.key === key ? <PluginMarketView key={key} scope={context.scope} catalog={catalog} visible />
|
|
38
|
+
: error ? <><p role="status">{error}</p><button type="button" onClick={() => setRevision(value => value + 1)}>{t('pluginMarketRefresh')}</button></>
|
|
39
|
+
: <p role="status">{t('pluginMarketLoading')}</p>}
|
|
40
|
+
</div>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function registerPluginMarketSettings(ctx: Context, catalog: PluginMarketCatalog): void {
|
|
44
|
+
// Same seat as dsh-market, lower priority wins independent of activation
|
|
45
|
+
// order. The settings shell projects elected entries, giving one nav row.
|
|
46
|
+
// The old market component never mounts; the skill-market seat is untouched.
|
|
47
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
48
|
+
name: 'settings.section',
|
|
49
|
+
id: 'market',
|
|
50
|
+
order: 40,
|
|
51
|
+
priority: -10,
|
|
52
|
+
label: () => t('pluginMarketTitle'),
|
|
53
|
+
inject: () => ({ ctx, catalog }),
|
|
54
|
+
}, ({ ctx, catalog }: { ctx: Context; catalog: PluginMarketCatalog }) => <RenderBoundary><PluginMarketSettingsSection ctx={ctx} catalog={catalog} /></RenderBoundary>))
|
|
55
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.root { box-sizing: border-box; height: 100%; overflow: auto; padding: 4px; color: var(--dsw-alias-label-primary); font: var(--dsw-font-s-14); }
|
|
2
|
+
.head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 8px; }
|
|
3
|
+
.head > span { box-sizing: border-box; min-width: 220px; max-width: 380px; flex: 1; }
|
|
4
|
+
.title { font-size: 16px; line-height: 24px; font-weight: 600; margin: 0; }
|
|
5
|
+
.subtitle, .project, .meta, .state { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); margin: 2px 0; }
|
|
6
|
+
.project { overflow-wrap: anywhere; }
|
|
7
|
+
.toolbar { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px; margin: 16px 0 12px; }
|
|
8
|
+
.tabs { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
|
|
9
|
+
.tabs button { white-space: nowrap; }
|
|
10
|
+
.tabs button[aria-pressed='true'] { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); font-weight: 600; }
|
|
11
|
+
.breadcrumb { display: flex; align-items: center; gap: 8px; margin: 4px 0 12px; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; }
|
|
12
|
+
.breadcrumb button { flex-shrink: 0; white-space: nowrap; }
|
|
13
|
+
.breadcrumb > span:last-child { min-width: 0; }
|
|
14
|
+
.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
|
+
.error, .notice { font-size: 12px; padding: 8px 10px; border-radius: 8px; margin: 10px 0; overflow-wrap: anywhere; }
|
|
16
|
+
.error { color: var(--dsw-alias-state-danger-primary); background: var(--dsw-alias-state-danger-bg); }
|
|
17
|
+
.notice { color: var(--dsw-alias-state-success-primary); background: var(--dsw-alias-state-success-bg); }
|
|
18
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); gap: 10px; margin-top: 12px; }
|
|
19
|
+
.card { display: flex; flex-direction: column; min-width: 0; min-height: 150px; padding: 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; background: var(--dsw-alias-bg-layer-1); }
|
|
20
|
+
.cardHead { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
21
|
+
.card h3 { font-size: 14px; line-height: 20px; font-weight: 600; margin: 0; overflow-wrap: anywhere; }
|
|
22
|
+
.description { font-size: 12px; line-height: 18px; margin: 8px 0; flex: 1; color: var(--dsw-alias-label-secondary); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }
|
|
23
|
+
.meta { min-height: 18px; overflow-wrap: anywhere; }
|
|
24
|
+
.actions { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
|
|
25
|
+
.projectInstall.projectInstall { background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); }
|
|
26
|
+
.projectInstall.projectInstall:hover:not(:disabled) { background-color: var(--dsw-alias-button-primary-fill) !important; color: var(--dsw-alias-label-primary-foreground) !important; }
|
|
27
|
+
.marketCard { text-align: left; color: inherit; font: inherit; cursor: pointer; width: 100%; }
|
|
28
|
+
.marketCard:hover:not(:disabled) { border-color: var(--dsw-alias-border-l1); background: var(--dsw-alias-interactive-bg-hover); }
|
|
29
|
+
.marketCard:disabled { cursor: not-allowed; opacity: 0.5; }
|
|
30
|
+
.spinning { display: inline-flex; animation: marketRefresh 1s linear infinite; }
|
|
31
|
+
@keyframes marketRefresh { to { transform: rotate(360deg); } }
|
|
32
|
+
@media (prefers-reduced-motion: reduce) { .spinning { animation: none; } }
|
|
33
|
+
.marketCard:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary); outline-offset: 2px; }
|
|
34
|
+
.enter { display: flex; align-items: center; justify-content: space-between; gap: 8px; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
|
|
35
|
+
@media (max-width: 640px) { .head { flex-direction: column; } .head > span { width: 100%; max-width: none; min-width: 0; } }
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/** Two catalog levels with project/user actions directly on plugin cards. */
|
|
2
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
3
|
+
import { Button, Input, Pill } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
4
|
+
import { api, type SessionScope } from './api.ts'
|
|
5
|
+
import { t } from './locales.ts'
|
|
6
|
+
import { IconChevronLeftOutline14, IconChevronRightOutline14, IconRefreshOutline14 } from './icons.tsx'
|
|
7
|
+
import type { PluginMarketCatalog } from './plugin-market-catalog.ts'
|
|
8
|
+
import type { InstalledMarketplacePlugin, MarketplacePlugin, PluginMarketScope } from '../plugin-market-shared.ts'
|
|
9
|
+
import css from './PluginMarketView.module.css'
|
|
10
|
+
|
|
11
|
+
export function PluginMarketView({ scope, visible, catalog }: { scope: SessionScope; visible: boolean; catalog: PluginMarketCatalog }): JSX.Element {
|
|
12
|
+
const snapshot = useSyncExternalStore(catalog.subscribe, catalog.getSnapshot)
|
|
13
|
+
const [marketId, setMarketId] = useState<string | null>(null)
|
|
14
|
+
const [tab, setTab] = useState<'markets' | 'installed'>('markets')
|
|
15
|
+
const [query, setQuery] = useState('')
|
|
16
|
+
const [installed, setInstalled] = useState<InstalledMarketplacePlugin[]>([])
|
|
17
|
+
const [recordsReady, setRecordsReady] = useState(false)
|
|
18
|
+
const [recordsError, setRecordsError] = useState('')
|
|
19
|
+
const [message, setMessage] = useState<{ text: string; error: boolean }>()
|
|
20
|
+
const [working, setWorking] = useState<{ id: string; scope: PluginMarketScope }>()
|
|
21
|
+
const [revision, setRevision] = useState(0)
|
|
22
|
+
const pending = useRef(false)
|
|
23
|
+
const breadcrumbNode = useRef<HTMLDivElement>(null)
|
|
24
|
+
const mutationVersion = useRef(0)
|
|
25
|
+
const focusBack = useRef(false)
|
|
26
|
+
const scopeKey = `${scope.sessionId}\0${scope.cwd ?? ''}`
|
|
27
|
+
const latestScope = useRef(scopeKey)
|
|
28
|
+
latestScope.current = scopeKey
|
|
29
|
+
const markets = snapshot.markets?.data?.marketplaces ?? []
|
|
30
|
+
const market = markets.find(item => item.id === marketId)
|
|
31
|
+
const catalogEntry = tab === 'markets' && marketId ? snapshot.plugins[marketId] : snapshot.markets
|
|
32
|
+
const loading = snapshot.refresh?.status === 'loading' || (tab === 'markets' && (!catalogEntry || catalogEntry.status === 'loading'))
|
|
33
|
+
const blocked = loading || snapshot.refresh?.status === 'error'
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!visible) return
|
|
37
|
+
catalog.start()
|
|
38
|
+
if (tab === 'markets' && marketId) catalog.ensure(marketId)
|
|
39
|
+
}, [catalog, visible, tab, marketId])
|
|
40
|
+
useEffect(() => { setQuery('') }, [tab, marketId])
|
|
41
|
+
useEffect(() => { if (focusBack.current) { breadcrumbNode.current?.querySelector('button')?.focus(); focusBack.current = false } }, [marketId])
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
setInstalled([]); setRecordsReady(false); setRecordsError(''); setMessage(undefined)
|
|
45
|
+
}, [scopeKey])
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!visible) return
|
|
48
|
+
const controller = new AbortController()
|
|
49
|
+
let active = true
|
|
50
|
+
let reading = false
|
|
51
|
+
const read = async (): Promise<void> => {
|
|
52
|
+
if (pending.current || reading) return
|
|
53
|
+
reading = true
|
|
54
|
+
const version = mutationVersion.current
|
|
55
|
+
try {
|
|
56
|
+
const records = await api.pluginsInstalled(scope, controller.signal)
|
|
57
|
+
if (active && !pending.current && version === mutationVersion.current) { setInstalled(records); setRecordsReady(true); setRecordsError('') }
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
if (active && version === mutationVersion.current) setRecordsError(cause instanceof Error ? cause.message : String(cause))
|
|
60
|
+
} finally { reading = false }
|
|
61
|
+
}
|
|
62
|
+
void read()
|
|
63
|
+
// Only local installation records are polled for CLI synchronization.
|
|
64
|
+
// Remote catalogs are never polled or refreshed by navigation.
|
|
65
|
+
const timer = setInterval(() => { void read() }, 2000)
|
|
66
|
+
return () => { active = false; clearInterval(timer); controller.abort() }
|
|
67
|
+
}, [scopeKey, visible, revision])
|
|
68
|
+
|
|
69
|
+
const perform = async (plugin: MarketplacePlugin, installScope: PluginMarketScope): Promise<void> => {
|
|
70
|
+
const refreshStatus = catalog.getSnapshot().refresh?.status
|
|
71
|
+
if (pending.current || !recordsReady || blocked || refreshStatus === 'loading' || refreshStatus === 'error') return
|
|
72
|
+
const key = scopeKey
|
|
73
|
+
const present = installed.some(item => item.id === plugin.id && item.scope === installScope)
|
|
74
|
+
pending.current = true
|
|
75
|
+
mutationVersion.current++
|
|
76
|
+
setWorking({ id: plugin.id, scope: installScope })
|
|
77
|
+
setMessage(undefined)
|
|
78
|
+
try {
|
|
79
|
+
if (present) await api.pluginUninstall(scope, plugin.id, installScope)
|
|
80
|
+
else await api.pluginInstall(scope, plugin.id, installScope)
|
|
81
|
+
const records = await api.pluginsInstalled(scope)
|
|
82
|
+
if (latestScope.current === key) {
|
|
83
|
+
setInstalled(records); setRecordsReady(true); setRecordsError('')
|
|
84
|
+
setMessage({ text: t(present ? 'pluginMarketUninstalled' : 'pluginMarketInstalled'), error: false })
|
|
85
|
+
}
|
|
86
|
+
} catch (cause) {
|
|
87
|
+
if (latestScope.current === key) setMessage({ text: cause instanceof Error ? cause.message : String(cause), error: true })
|
|
88
|
+
} finally { pending.current = false; setWorking(undefined) }
|
|
89
|
+
}
|
|
90
|
+
const refresh = (): void => {
|
|
91
|
+
if (loading || pending.current) return
|
|
92
|
+
catalog.refresh(tab === 'markets' && marketId ? marketId : undefined)
|
|
93
|
+
setRevision(value => value + 1)
|
|
94
|
+
setMessage(undefined)
|
|
95
|
+
}
|
|
96
|
+
const term = query.trim().toLocaleLowerCase()
|
|
97
|
+
const matches = (item: { name: string; description: string }): boolean => `${item.name} ${item.description}`.toLocaleLowerCase().includes(term)
|
|
98
|
+
// One installed card per plugin, with independent controls for both scopes.
|
|
99
|
+
const entries = tab === 'installed' ? [...new Map(installed.map(item => [item.id, item])).values()] : marketId ? snapshot.plugins[marketId]?.data ?? [] : []
|
|
100
|
+
const filteredPlugins = entries.filter(matches)
|
|
101
|
+
const filteredMarkets = markets.filter(matches)
|
|
102
|
+
const showMarkets = tab === 'markets' && !marketId
|
|
103
|
+
const error = snapshot.refresh?.error || (tab === 'markets' ? catalogEntry?.error : '') || recordsError
|
|
104
|
+
const warning = [snapshot.markets?.data?.warning, marketId ? snapshot.plugins[marketId]?.warning : ''].filter(Boolean).join('\n')
|
|
105
|
+
const hasData = tab === 'installed' ? recordsReady : !!catalogEntry?.data
|
|
106
|
+
|
|
107
|
+
return <section className={css.root} aria-label={t('pluginMarketTitle')} aria-busy={loading} data-chatcode-plugin-market>
|
|
108
|
+
<header className={css.head}>
|
|
109
|
+
<div><h2 className={css.title}>{t('pluginMarketTitle')}</h2><p className={css.subtitle}>{t('pluginMarketSubtitle')}</p></div>
|
|
110
|
+
<Input disabled={blocked} value={query} onChange={event => setQuery(event.target.value)} placeholder={t('pluginMarketSearch')} aria-label={t('pluginMarketSearch')} />
|
|
111
|
+
</header>
|
|
112
|
+
{scope.cwd ? <p className={css.project}>{t('pluginMarketProjectDirectory', { path: scope.cwd })}</p> : null}
|
|
113
|
+
<div className={css.toolbar}>
|
|
114
|
+
<div className={css.tabs}>
|
|
115
|
+
<Button size="sm" variant="ghost" disabled={blocked} aria-pressed={tab === 'markets'} onClick={() => setTab('markets')}>{t('pluginMarketTitle')}</Button>
|
|
116
|
+
<Button size="sm" variant="ghost" disabled={blocked} aria-pressed={tab === 'installed'} onClick={() => setTab('installed')}>{t('pluginMarketInstalledTab')}</Button>
|
|
117
|
+
</div>
|
|
118
|
+
<Button size="sm" variant="outline" aria-label={t('pluginMarketRefresh')} disabled={snapshot.markets?.status === 'loading' || loading || !!working} onClick={refresh}><span aria-hidden="true" className={loading ? css.spinning : undefined}><IconRefreshOutline14 /></span>{t(loading ? 'pluginMarketRefreshing' : 'pluginMarketRefresh')}</Button>
|
|
119
|
+
</div>
|
|
120
|
+
{warning && tab === 'markets' ? <p className={css.warning} role="status">{warning}</p> : null}
|
|
121
|
+
{error ? <p className={css.error} role="status">{error}</p> : null}
|
|
122
|
+
{message ? <p className={message.error ? css.error : css.notice} role="status">{message.text}</p> : null}
|
|
123
|
+
{tab === 'markets' && marketId ? <div ref={breadcrumbNode} className={css.breadcrumb}>
|
|
124
|
+
<Button size="sm" variant="ghost" disabled={blocked} onClick={() => setMarketId(null)}><IconChevronLeftOutline14 />{t('pluginMarketBack')}</Button>
|
|
125
|
+
<span aria-hidden="true">/</span><span>{market?.name ?? marketId}</span>
|
|
126
|
+
</div> : null}
|
|
127
|
+
{loading ? <p className={css.state} role="status">{t(hasData ? 'pluginMarketRefreshing' : 'pluginMarketLoading')}</p> : null}
|
|
128
|
+
<div className={css.grid} aria-busy={loading}>
|
|
129
|
+
{showMarkets ? filteredMarkets.map(item => <button key={item.id} type="button" disabled={blocked} className={`${css.card} ${css.marketCard}`} onClick={() => { focusBack.current = true; setMarketId(item.id) }}>
|
|
130
|
+
<div className={css.cardHead}><h3>{item.name}</h3>{item.official ? <Pill>{t('pluginMarketDefault')}</Pill> : null}</div>
|
|
131
|
+
<p className={css.description}>{item.description || t('pluginMarketNoDescription')}</p>
|
|
132
|
+
<span className={css.enter}>{t('pluginMarketBrowse')}<IconChevronRightOutline14 /></span>
|
|
133
|
+
</button>) : filteredPlugins.map(item => {
|
|
134
|
+
const records = installed.filter(record => record.id === item.id)
|
|
135
|
+
const marketName = markets.find(value => value.id === item.marketplaceId)?.name ?? item.marketplaceId
|
|
136
|
+
return <article key={item.id} className={css.card} aria-label={item.name} data-plugin-id={item.id}>
|
|
137
|
+
<div className={css.cardHead}><h3>{item.name}</h3>{records.length ? <Pill>{t('pluginMarketInstalledTab')}</Pill> : null}</div>
|
|
138
|
+
<p className={css.description}>{item.description || t('pluginMarketNoDescription')}</p>
|
|
139
|
+
<p className={css.meta}>{marketName}{tab === 'installed' && records[0]?.version ? ` · ${records[0].version}` : ''}</p>
|
|
140
|
+
<div className={css.actions}>
|
|
141
|
+
{(['project', 'user'] as const).map(installScope => {
|
|
142
|
+
const present = records.some(record => record.scope === installScope)
|
|
143
|
+
const busy = working?.id === item.id && working.scope === installScope
|
|
144
|
+
return <Button key={installScope} size="sm" variant={installScope === 'project' && !present ? 'primary' : 'outline'} className={installScope === 'project' && !present ? css.projectInstall : undefined} disabled={blocked || !!working || !recordsReady} onClick={() => { void perform(item, installScope) }}>
|
|
145
|
+
{t(busy ? present ? 'pluginMarketUninstalling' : 'pluginMarketInstalling' : present ? 'pluginMarketUninstall' : 'pluginMarketInstall')} · {t(installScope === 'project' ? 'pluginMarketProject' : 'pluginMarketUser')}
|
|
146
|
+
</Button>
|
|
147
|
+
})}
|
|
148
|
+
</div>
|
|
149
|
+
</article>
|
|
150
|
+
})}
|
|
151
|
+
</div>
|
|
152
|
+
{!loading && hasData && (showMarkets ? filteredMarkets : filteredPlugins).length === 0 ? <p className={css.state}>{t('pluginMarketEmpty')}</p> : null}
|
|
153
|
+
</section>
|
|
154
|
+
}
|
package/src/client/api.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { encodeHtmlUrl } from '../html-route.ts'
|
|
10
10
|
import type { BrowserProbeResult } from './browser.ts'
|
|
11
|
+
import type { MarketplaceCatalog, MarketplacePlugin, InstalledMarketplacePlugin, PluginMarketScope } from '../plugin-market-shared.ts'
|
|
11
12
|
|
|
12
13
|
/** One wire failure. */
|
|
13
14
|
export class SidebarApiError extends Error {
|
|
@@ -287,6 +288,12 @@ function gitPayload(scope: SessionScope, worktree: string | undefined, extra: Re
|
|
|
287
288
|
|
|
288
289
|
/** The sidebar API surface (session scope threaded through every call). */
|
|
289
290
|
export const api = {
|
|
291
|
+
pluginMarketContext: (scope: SessionScope, signal?: AbortSignal) => call<{ sessionId: string; cwd: string }>('plugins.context', scopePayload(scope, {}), signal),
|
|
292
|
+
pluginMarketplaces: (signal?: AbortSignal) => call<MarketplaceCatalog>('plugins.marketplaces', {}, signal),
|
|
293
|
+
pluginCatalog: (marketplaceId: string, signal?: AbortSignal) => call<MarketplacePlugin[] | { plugins: MarketplacePlugin[]; warning: string }>('plugins.catalog', { marketplaceId, includeMetadata: true }, signal),
|
|
294
|
+
pluginsInstalled: (scope: SessionScope, signal?: AbortSignal) => call<InstalledMarketplacePlugin[]>('plugins.installed', scopePayload(scope, {}), signal),
|
|
295
|
+
pluginInstall: (scope: SessionScope, pluginId: string, installScope: PluginMarketScope) => call<InstalledMarketplacePlugin>('plugins.install', scopePayload(scope, { pluginId, scope: installScope })),
|
|
296
|
+
pluginUninstall: (scope: SessionScope, pluginId: string, installScope: PluginMarketScope) => call<{ ok: true }>('plugins.uninstall', scopePayload(scope, { pluginId, scope: installScope })),
|
|
290
297
|
chatcodeStatus: (signal?: AbortSignal) => call<ChatCodeAuthState>('chatcode.status', {}, signal),
|
|
291
298
|
chatcodeStart: () => call<{ sessionId: string; url: string }>('chatcode.start', {}),
|
|
292
299
|
chatcodeCancel: (sessionId: string) => call<{ ok: true }>('chatcode.cancel', { sessionId }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
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
|
|
@@ -17,6 +17,8 @@ import { lazyChunkComponent } from '../lazy-chunk.tsx'
|
|
|
17
17
|
import { GitView } from '../GitView.tsx'
|
|
18
18
|
import { DiffTab } from '../DiffTab.tsx'
|
|
19
19
|
import { BrowserView } from '../BrowserView.tsx'
|
|
20
|
+
import { PluginMarketView } from '../PluginMarketView.tsx'
|
|
21
|
+
import { createPluginMarketCatalog, type PluginMarketCatalog } from '../plugin-market-catalog.ts'
|
|
20
22
|
import {
|
|
21
23
|
IconBranchOutline16,
|
|
22
24
|
IconCodeOutline16,
|
|
@@ -63,6 +65,8 @@ export const TERMINAL_LIMIT = 3
|
|
|
63
65
|
export interface BuiltinTabOptions {
|
|
64
66
|
/** Returns the display title for newly opened terminal tabs. */
|
|
65
67
|
terminalTitle?: () => string
|
|
68
|
+
/** Shared app-lifetime catalog; production supplies the activation's store. */
|
|
69
|
+
pluginMarketCatalog?: PluginMarketCatalog
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
/** A client-side uuid for terminal tab identity (not shown in the UI). */
|
|
@@ -80,8 +84,9 @@ function uiTerminalCount(state: SidebarState): number {
|
|
|
80
84
|
.filter(tab => tab.type === 'terminal' && !isAgentTabId(tab.id)).length
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
/** The
|
|
87
|
+
/** The 6 built-in tab descriptors. */
|
|
84
88
|
export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): readonly TabDescriptor[] {
|
|
89
|
+
const pluginMarketCatalog = options.pluginMarketCatalog ?? createPluginMarketCatalog()
|
|
85
90
|
return [
|
|
86
91
|
{
|
|
87
92
|
id: 'editor',
|
|
@@ -266,6 +271,14 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
|
|
266
271
|
}),
|
|
267
272
|
component: (props) => <BrowserView {...props} />,
|
|
268
273
|
},
|
|
274
|
+
{
|
|
275
|
+
id: 'plugin-market',
|
|
276
|
+
title: () => t('pluginMarketTitle'),
|
|
277
|
+
icon: (size: number) => <IconGlobeOutline16 size={size} />,
|
|
278
|
+
order: 60,
|
|
279
|
+
single: true,
|
|
280
|
+
component: ({ scope, visible }) => <PluginMarketView scope={scope} visible={visible} catalog={pluginMarketCatalog} />,
|
|
281
|
+
},
|
|
269
282
|
{
|
|
270
283
|
id: 'diff',
|
|
271
284
|
title: () => t('git'),
|