@chatcode/cco-better-sidebar 0.18.0-rc.5 → 0.18.0-rc.7
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 +1 -1
- package/README_EN.md +1 -1
- package/lib/client-editor.js +346 -273
- package/lib/client-mermaid.js +338 -269
- package/lib/client-registry.js +861 -441
- package/lib/client-terminal.js +344 -271
- package/lib/client.js +855 -435
- package/lib/index.js +47 -7
- 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 +16 -0
- package/lib/types/plugin-market-shared.d.ts +22 -0
- package/package.json +1 -1
- package/src/client/PluginMarketView.module.css +15 -0
- package/src/client/PluginMarketView.tsx +113 -24
- package/src/client/PluginProjectPicker.tsx +73 -0
- package/src/client/api.ts +7 -5
- package/src/client/locales-ja.ts +17 -1
- package/src/client/locales.ts +72 -3
- package/src/plugin-market-api.ts +55 -9
- package/src/plugin-market-shared.ts +9 -0
package/lib/index.js
CHANGED
|
@@ -6005,20 +6005,50 @@ function buildPluginMarketApi(ctx, cwdOf) {
|
|
|
6005
6005
|
if (scope !== "project" && scope !== "user") throw new SidebarError("bad-request", "scope must be project or user");
|
|
6006
6006
|
return scope;
|
|
6007
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
|
+
};
|
|
6008
6022
|
const contextOf = async (payload) => {
|
|
6009
6023
|
const sessionId = payload?.sessionId;
|
|
6010
|
-
if (sessionId === void 0 || sessionId === "") return {
|
|
6011
|
-
sessionId: "",
|
|
6012
|
-
cwd: process.cwd()
|
|
6013
|
-
};
|
|
6024
|
+
if (sessionId === void 0 || sessionId === "") return { sessionId: "" };
|
|
6014
6025
|
requireString(payload, "sessionId");
|
|
6015
6026
|
return {
|
|
6016
6027
|
sessionId,
|
|
6017
6028
|
cwd: (await cwdOf(payload)).cwd
|
|
6018
6029
|
};
|
|
6019
6030
|
};
|
|
6031
|
+
const projectOf = async (payload) => {
|
|
6032
|
+
if (payload?.projectPath !== void 0) return selectedProject(payload);
|
|
6033
|
+
return (await contextOf(payload)).cwd;
|
|
6034
|
+
};
|
|
6020
6035
|
return {
|
|
6021
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
|
+
},
|
|
6022
6052
|
"plugins.marketplaces": () => service().marketplaces(),
|
|
6023
6053
|
"plugins.catalog": async (payload) => {
|
|
6024
6054
|
const market = service();
|
|
@@ -6029,10 +6059,20 @@ function buildPluginMarketApi(ctx, cwdOf) {
|
|
|
6029
6059
|
warning: ""
|
|
6030
6060
|
};
|
|
6031
6061
|
},
|
|
6032
|
-
"plugins.installed": async (payload) =>
|
|
6033
|
-
|
|
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
|
+
},
|
|
6034
6072
|
"plugins.uninstall": async (payload) => {
|
|
6035
|
-
|
|
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);
|
|
6036
6076
|
return { ok: true };
|
|
6037
6077
|
}
|
|
6038
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,12 +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
|
+
pluginMarketAwaitingConfirmation: string;
|
|
44
|
+
pluginMarketConfirmProjectInstall: string;
|
|
45
|
+
pluginMarketConfirmProjectDescription: string;
|
|
46
|
+
pluginMarketConfirmInstall: string;
|
|
47
|
+
pluginMarketInstalledAt: string;
|
|
48
|
+
pluginMarketInstallPath: string;
|
|
49
|
+
pluginMarketDirectoryPath: string;
|
|
50
|
+
pluginMarketOpenDirectory: string;
|
|
51
|
+
pluginMarketSelectDirectory: string;
|
|
52
|
+
pluginMarketHomeDirectory: string;
|
|
53
|
+
pluginMarketDirectoryTruncated: string;
|
|
40
54
|
pluginMarketSearch: string;
|
|
41
55
|
pluginMarketDefault: string;
|
|
42
56
|
pluginMarketBrowse: string;
|
|
43
57
|
pluginMarketNoDescription: string;
|
|
44
58
|
pluginMarketInstalling: string;
|
|
45
59
|
pluginMarketUninstalling: string;
|
|
60
|
+
pluginMarketInstallProgress: string;
|
|
61
|
+
pluginMarketUninstallProgress: string;
|
|
46
62
|
accountMenuSettings: string;
|
|
47
63
|
accountMenuLogout: string;
|
|
48
64
|
accountMenuLogoutConfirmTitle: string;
|
|
@@ -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.7",
|
|
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": {
|
|
@@ -4,6 +4,18 @@
|
|
|
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
|
+
.confirmPath { margin: 0; padding: 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
13
|
+
.directoryPath { display: flex; align-items: center; gap: 8px; }
|
|
14
|
+
.directoryPath > span { flex: 1; min-width: 0; }
|
|
15
|
+
.directoryCrumbs { display: flex; flex-wrap: wrap; gap: 4px; margin: 12px 0; }
|
|
16
|
+
.directoryList { max-height: 320px; overflow: auto; min-height: 100px; }
|
|
17
|
+
.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; }
|
|
18
|
+
.directoryList > button:hover, .directoryList > button:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
|
|
7
19
|
.toolbar { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px; margin: 16px 0 12px; }
|
|
8
20
|
.tabs { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
|
|
9
21
|
.tabs button { white-space: nowrap; }
|
|
@@ -11,6 +23,9 @@
|
|
|
11
23
|
.breadcrumb { display: flex; align-items: center; gap: 8px; margin: 4px 0 12px; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; }
|
|
12
24
|
.breadcrumb button { flex-shrink: 0; white-space: nowrap; }
|
|
13
25
|
.breadcrumb > span:last-child { min-width: 0; }
|
|
26
|
+
.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; }
|
|
27
|
+
.operation > span:first-child { flex-shrink: 0; }
|
|
28
|
+
.operation > span:last-child { min-width: 0; overflow-wrap: anywhere; }
|
|
14
29
|
.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
30
|
.error, .notice { font-size: 12px; padding: 8px 10px; border-radius: 8px; margin: 10px 0; overflow-wrap: anywhere; }
|
|
16
31
|
.error { color: var(--dsw-alias-state-danger-primary); background: var(--dsw-alias-state-danger-bg); }
|
|
@@ -1,36 +1,48 @@
|
|
|
1
1
|
/** Two catalog levels with project/user actions directly on plugin cards. */
|
|
2
2
|
import { useEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
3
|
-
import { Button, Input, Pill } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
3
|
+
import { Button, Input, Modal, Pill } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
4
4
|
import { api, type SessionScope } from './api.ts'
|
|
5
5
|
import { t } from './locales.ts'
|
|
6
6
|
import { IconChevronLeftOutline14, IconChevronRightOutline14, IconRefreshOutline14 } from './icons.tsx'
|
|
7
7
|
import type { PluginMarketCatalog } from './plugin-market-catalog.ts'
|
|
8
8
|
import type { InstalledMarketplacePlugin, MarketplacePlugin, PluginMarketScope } from '../plugin-market-shared.ts'
|
|
9
9
|
import css from './PluginMarketView.module.css'
|
|
10
|
+
import { PluginProjectPicker, type PluginProjectRequest } from './PluginProjectPicker.tsx'
|
|
11
|
+
|
|
12
|
+
interface ProjectInstallConfirmation { name: string; path: string; finish: (confirmed: boolean) => void }
|
|
10
13
|
|
|
11
14
|
export function PluginMarketView({ scope, visible, catalog }: { scope: SessionScope; visible: boolean; catalog: PluginMarketCatalog }): JSX.Element {
|
|
12
15
|
const snapshot = useSyncExternalStore(catalog.subscribe, catalog.getSnapshot)
|
|
13
16
|
const [marketId, setMarketId] = useState<string | null>(null)
|
|
14
17
|
const [tab, setTab] = useState<'markets' | 'installed'>('markets')
|
|
15
18
|
const [query, setQuery] = useState('')
|
|
16
|
-
const [
|
|
17
|
-
const [recordsReady, setRecordsReady] = useState(false)
|
|
18
|
-
const [recordsError, setRecordsError] = useState('')
|
|
19
|
+
const [recordState, setRecordState] = useState<{ key: string; records: InstalledMarketplacePlugin[]; ready: boolean; error: string }>()
|
|
19
20
|
const [message, setMessage] = useState<{ text: string; error: boolean }>()
|
|
20
|
-
const [working, setWorking] = useState<{ id: string; scope: PluginMarketScope }>()
|
|
21
|
+
const [working, setWorking] = useState<{ id: string; name: string; scope: PluginMarketScope; uninstall: boolean; confirming?: boolean }>()
|
|
22
|
+
const [confirmation, setConfirmation] = useState<ProjectInstallConfirmation>()
|
|
23
|
+
const confirmationRef = useRef<ProjectInstallConfirmation>()
|
|
24
|
+
const [selection, setSelection] = useState<{ key: string; path: string }>()
|
|
25
|
+
const [picker, setPicker] = useState<PluginProjectRequest>()
|
|
26
|
+
const pickerRef = useRef<PluginProjectRequest>()
|
|
21
27
|
const [revision, setRevision] = useState(0)
|
|
22
28
|
const pending = useRef(false)
|
|
23
29
|
const breadcrumbNode = useRef<HTMLDivElement>(null)
|
|
24
30
|
const mutationVersion = useRef(0)
|
|
25
31
|
const focusBack = useRef(false)
|
|
26
32
|
const scopeKey = `${scope.sessionId}\0${scope.cwd ?? ''}`
|
|
33
|
+
const projectPath = selection?.key === scopeKey ? selection.path : scope.cwd
|
|
34
|
+
const recordsKey = `${scopeKey}\0${projectPath ?? ''}`
|
|
35
|
+
const installed = recordState?.key === recordsKey ? recordState.records : []
|
|
36
|
+
const recordsReady = recordState?.key === recordsKey && recordState.ready
|
|
37
|
+
const recordsError = recordState?.key === recordsKey ? recordState.error : ''
|
|
27
38
|
const latestScope = useRef(scopeKey)
|
|
28
39
|
latestScope.current = scopeKey
|
|
29
40
|
const markets = snapshot.markets?.data?.marketplaces ?? []
|
|
30
41
|
const market = markets.find(item => item.id === marketId)
|
|
31
42
|
const catalogEntry = tab === 'markets' && marketId ? snapshot.plugins[marketId] : snapshot.markets
|
|
32
43
|
const loading = snapshot.refresh?.status === 'loading' || (tab === 'markets' && (!catalogEntry || catalogEntry.status === 'loading'))
|
|
33
|
-
const
|
|
44
|
+
const operating = !!working && !working.confirming
|
|
45
|
+
const blocked = loading || snapshot.refresh?.status === 'error' || !!working
|
|
34
46
|
|
|
35
47
|
useEffect(() => {
|
|
36
48
|
if (!visible) return
|
|
@@ -40,9 +52,17 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
40
52
|
useEffect(() => { setQuery('') }, [tab, marketId])
|
|
41
53
|
useEffect(() => { if (focusBack.current) { breadcrumbNode.current?.querySelector('button')?.focus(); focusBack.current = false } }, [marketId])
|
|
42
54
|
|
|
55
|
+
useEffect(() => { setMessage(undefined) }, [scopeKey])
|
|
43
56
|
useEffect(() => {
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
confirmationRef.current?.finish(false)
|
|
58
|
+
confirmationRef.current = undefined
|
|
59
|
+
setConfirmation(undefined)
|
|
60
|
+
}, [scopeKey, visible])
|
|
61
|
+
useEffect(() => () => {
|
|
62
|
+
pickerRef.current?.controller.abort()
|
|
63
|
+
pickerRef.current?.finish(null)
|
|
64
|
+
confirmationRef.current?.finish(false)
|
|
65
|
+
}, [])
|
|
46
66
|
useEffect(() => {
|
|
47
67
|
if (!visible) return
|
|
48
68
|
const controller = new AbortController()
|
|
@@ -53,10 +73,10 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
53
73
|
reading = true
|
|
54
74
|
const version = mutationVersion.current
|
|
55
75
|
try {
|
|
56
|
-
const records = await api.pluginsInstalled(scope, controller.signal)
|
|
57
|
-
if (active && !pending.current && version === mutationVersion.current) {
|
|
76
|
+
const records = await api.pluginsInstalled(scope, controller.signal, projectPath)
|
|
77
|
+
if (active && !pending.current && version === mutationVersion.current) setRecordState({ key: recordsKey, records, ready: true, error: '' })
|
|
58
78
|
} catch (cause) {
|
|
59
|
-
if (active && version === mutationVersion.current)
|
|
79
|
+
if (active && version === mutationVersion.current) setRecordState(previous => ({ key: recordsKey, records: previous?.key === recordsKey ? previous.records : [], ready: previous?.key === recordsKey && previous.ready, error: cause instanceof Error ? cause.message : String(cause) }))
|
|
60
80
|
} finally { reading = false }
|
|
61
81
|
}
|
|
62
82
|
void read()
|
|
@@ -64,28 +84,83 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
64
84
|
// Remote catalogs are never polled or refreshed by navigation.
|
|
65
85
|
const timer = setInterval(() => { void read() }, 2000)
|
|
66
86
|
return () => { active = false; clearInterval(timer); controller.abort() }
|
|
67
|
-
}, [
|
|
87
|
+
}, [recordsKey, visible, revision])
|
|
88
|
+
|
|
89
|
+
const chooseDirectory = (): Promise<string | null> => new Promise(finish => {
|
|
90
|
+
const controller = new AbortController()
|
|
91
|
+
const request = { result: api.pluginProjectPick(controller.signal), controller, finish }
|
|
92
|
+
pickerRef.current = request
|
|
93
|
+
setPicker(request)
|
|
94
|
+
})
|
|
95
|
+
const finishPicker = (path: string | null): void => {
|
|
96
|
+
pickerRef.current?.finish(path)
|
|
97
|
+
pickerRef.current?.controller.abort()
|
|
98
|
+
pickerRef.current = undefined
|
|
99
|
+
setPicker(undefined)
|
|
100
|
+
}
|
|
101
|
+
const confirmProjectInstall = (name: string, path: string): Promise<boolean> => new Promise(finish => {
|
|
102
|
+
const request = { name, path, finish }
|
|
103
|
+
confirmationRef.current = request
|
|
104
|
+
setConfirmation(request)
|
|
105
|
+
})
|
|
106
|
+
const finishConfirmation = (confirmed: boolean): void => {
|
|
107
|
+
const request = confirmationRef.current
|
|
108
|
+
confirmationRef.current = undefined
|
|
109
|
+
setConfirmation(undefined)
|
|
110
|
+
request?.finish(confirmed)
|
|
111
|
+
}
|
|
112
|
+
const chooseProject = async (): Promise<void> => {
|
|
113
|
+
if (pending.current || blocked) return
|
|
114
|
+
pending.current = true
|
|
115
|
+
mutationVersion.current++
|
|
116
|
+
setMessage(undefined)
|
|
117
|
+
const key = scopeKey
|
|
118
|
+
try {
|
|
119
|
+
const path = await chooseDirectory()
|
|
120
|
+
if (path && latestScope.current === key) setSelection({ key, path })
|
|
121
|
+
} finally { pending.current = false; setRevision(value => value + 1) }
|
|
122
|
+
}
|
|
68
123
|
|
|
69
124
|
const perform = async (plugin: MarketplacePlugin, installScope: PluginMarketScope): Promise<void> => {
|
|
70
125
|
const refreshStatus = catalog.getSnapshot().refresh?.status
|
|
71
126
|
if (pending.current || !recordsReady || blocked || refreshStatus === 'loading' || refreshStatus === 'error') return
|
|
72
127
|
const key = scopeKey
|
|
73
|
-
const
|
|
128
|
+
const record = installed.find(item => item.id === plugin.id && item.scope === installScope)
|
|
129
|
+
const present = !!record
|
|
130
|
+
if (installScope === 'project' && !present && !projectPath) {
|
|
131
|
+
setMessage({ text: t('pluginMarketProjectHint'), error: true })
|
|
132
|
+
return
|
|
133
|
+
}
|
|
74
134
|
pending.current = true
|
|
75
135
|
mutationVersion.current++
|
|
76
|
-
|
|
136
|
+
const operation = { id: plugin.id, name: plugin.name, scope: installScope, uninstall: present }
|
|
137
|
+
setWorking({ ...operation, confirming: installScope === 'project' && !present })
|
|
77
138
|
setMessage(undefined)
|
|
78
139
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
140
|
+
let target = record?.scope === 'project' ? record.projectPath : projectPath
|
|
141
|
+
if (installScope === 'project' && !present) {
|
|
142
|
+
const confirmed = await confirmProjectInstall(plugin.name, target!)
|
|
143
|
+
if (!confirmed || latestScope.current !== key) return
|
|
144
|
+
setWorking(operation)
|
|
145
|
+
}
|
|
146
|
+
let installedPath = ''
|
|
147
|
+
if (present) await api.pluginUninstall(scope, plugin.id, installScope, target)
|
|
148
|
+
else {
|
|
149
|
+
const result = await api.pluginInstall(scope, plugin.id, installScope, target)
|
|
150
|
+
installedPath = result.installPath
|
|
151
|
+
if (installScope === 'project') {
|
|
152
|
+
target = result.projectPath
|
|
153
|
+
if (latestScope.current === key) setSelection({ key, path: target })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const records = await api.pluginsInstalled(scope, undefined, target)
|
|
82
157
|
if (latestScope.current === key) {
|
|
83
|
-
|
|
84
|
-
setMessage({ text:
|
|
158
|
+
setRecordState({ key: `${key}\0${target ?? ''}`, records, ready: true, error: '' })
|
|
159
|
+
setMessage({ text: present ? t('pluginMarketUninstalled') : t('pluginMarketInstalledAt', { path: installedPath }), error: false })
|
|
85
160
|
}
|
|
86
161
|
} catch (cause) {
|
|
87
162
|
if (latestScope.current === key) setMessage({ text: cause instanceof Error ? cause.message : String(cause), error: true })
|
|
88
|
-
} finally { pending.current = false; setWorking(undefined) }
|
|
163
|
+
} finally { pending.current = false; setWorking(undefined); setRevision(value => value + 1) }
|
|
89
164
|
}
|
|
90
165
|
const refresh = (): void => {
|
|
91
166
|
if (loading || pending.current) return
|
|
@@ -104,12 +179,15 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
104
179
|
const warning = [snapshot.markets?.data?.warning, marketId ? snapshot.plugins[marketId]?.warning : ''].filter(Boolean).join('\n')
|
|
105
180
|
const hasData = tab === 'installed' ? recordsReady : !!catalogEntry?.data
|
|
106
181
|
|
|
107
|
-
return <section className={css.root} aria-label={t('pluginMarketTitle')} aria-busy={loading} data-chatcode-plugin-market>
|
|
182
|
+
return <section className={css.root} aria-label={t('pluginMarketTitle')} aria-busy={loading || operating} data-chatcode-plugin-market>
|
|
108
183
|
<header className={css.head}>
|
|
109
184
|
<div><h2 className={css.title}>{t('pluginMarketTitle')}</h2><p className={css.subtitle}>{t('pluginMarketSubtitle')}</p></div>
|
|
110
185
|
<Input disabled={blocked} value={query} onChange={event => setQuery(event.target.value)} placeholder={t('pluginMarketSearch')} aria-label={t('pluginMarketSearch')} />
|
|
111
186
|
</header>
|
|
112
|
-
|
|
187
|
+
<div className={css.projectRow}>
|
|
188
|
+
<p className={css.project}>{projectPath ? t('pluginMarketProjectDirectory', { path: projectPath }) : t('pluginMarketProjectHint')}</p>
|
|
189
|
+
<Button size="sm" variant="outline" disabled={blocked || !!working || !!picker} onClick={() => { void chooseProject() }}>{t('pluginMarketChooseProject')}</Button>
|
|
190
|
+
</div>
|
|
113
191
|
<div className={css.toolbar}>
|
|
114
192
|
<div className={css.tabs}>
|
|
115
193
|
<Button size="sm" variant="ghost" disabled={blocked} aria-pressed={tab === 'markets'} onClick={() => setTab('markets')}>{t('pluginMarketTitle')}</Button>
|
|
@@ -117,6 +195,10 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
117
195
|
</div>
|
|
118
196
|
<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
197
|
</div>
|
|
198
|
+
{operating ? <div className={css.operation} role="status">
|
|
199
|
+
<span className={css.spinning} aria-hidden="true"><IconRefreshOutline14 /></span>
|
|
200
|
+
<span>{t(working.uninstall ? 'pluginMarketUninstallProgress' : 'pluginMarketInstallProgress', { name: working.name })}</span>
|
|
201
|
+
</div> : null}
|
|
120
202
|
{warning && tab === 'markets' ? <p className={css.warning} role="status">{warning}</p> : null}
|
|
121
203
|
{error ? <p className={css.error} role="status">{error}</p> : null}
|
|
122
204
|
{message ? <p className={message.error ? css.error : css.notice} role="status">{message.text}</p> : null}
|
|
@@ -125,7 +207,7 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
125
207
|
<span aria-hidden="true">/</span><span>{market?.name ?? marketId}</span>
|
|
126
208
|
</div> : null}
|
|
127
209
|
{loading ? <p className={css.state} role="status">{t(hasData ? 'pluginMarketRefreshing' : 'pluginMarketLoading')}</p> : null}
|
|
128
|
-
<div className={css.grid} aria-busy={loading}>
|
|
210
|
+
<div className={css.grid} aria-busy={loading || operating}>
|
|
129
211
|
{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
212
|
<div className={css.cardHead}><h3>{item.name}</h3>{item.official ? <Pill>{t('pluginMarketDefault')}</Pill> : null}</div>
|
|
131
213
|
<p className={css.description}>{item.description || t('pluginMarketNoDescription')}</p>
|
|
@@ -137,12 +219,14 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
137
219
|
<div className={css.cardHead}><h3>{item.name}</h3>{records.length ? <Pill>{t('pluginMarketInstalledTab')}</Pill> : null}</div>
|
|
138
220
|
<p className={css.description}>{item.description || t('pluginMarketNoDescription')}</p>
|
|
139
221
|
<p className={css.meta}>{marketName}{tab === 'installed' && records[0]?.version ? ` · ${records[0].version}` : ''}</p>
|
|
222
|
+
{records.map(record => <p key={record.scope} className={css.installPath}>{t('pluginMarketInstallPath', { scope: t(record.scope === 'project' ? 'pluginMarketProject' : 'pluginMarketUser'), path: record.installPath })}</p>)}
|
|
140
223
|
<div className={css.actions}>
|
|
141
224
|
{(['project', 'user'] as const).map(installScope => {
|
|
142
225
|
const present = records.some(record => record.scope === installScope)
|
|
143
226
|
const busy = working?.id === item.id && working.scope === installScope
|
|
144
227
|
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
|
-
{
|
|
228
|
+
{busy && operating ? <span className={css.spinning} aria-hidden="true"><IconRefreshOutline14 /></span> : null}
|
|
229
|
+
{t(busy ? working?.confirming ? 'pluginMarketAwaitingConfirmation' : working?.uninstall ? 'pluginMarketUninstalling' : 'pluginMarketInstalling' : present ? 'pluginMarketUninstall' : 'pluginMarketInstall')} · {t(installScope === 'project' ? 'pluginMarketProject' : 'pluginMarketUser')}
|
|
146
230
|
</Button>
|
|
147
231
|
})}
|
|
148
232
|
</div>
|
|
@@ -150,5 +234,10 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
|
|
|
150
234
|
})}
|
|
151
235
|
</div>
|
|
152
236
|
{!loading && hasData && (showMarkets ? filteredMarkets : filteredPlugins).length === 0 ? <p className={css.state}>{t('pluginMarketEmpty')}</p> : null}
|
|
237
|
+
{confirmation ? <Modal open title={t('pluginMarketConfirmProjectInstall')} description={t('pluginMarketConfirmProjectDescription', { name: confirmation.name })} closeLabel={t('cancel')} onClose={() => finishConfirmation(false)} className={css.projectPicker}
|
|
238
|
+
footer={<><Button variant="outline" data-modal-autofocus onClick={() => finishConfirmation(false)}>{t('cancel')}</Button><Button variant="primary" onClick={() => finishConfirmation(true)}>{t('pluginMarketConfirmInstall')}</Button></>}>
|
|
239
|
+
<p className={css.confirmPath}>{confirmation.path}</p>
|
|
240
|
+
</Modal> : null}
|
|
241
|
+
{picker ? <PluginProjectPicker request={picker} initialPath={projectPath} onPicked={finishPicker} onCancel={() => finishPicker(null)} onError={text => { setMessage({ text, error: true }); finishPicker(null) }} /> : null}
|
|
153
242
|
</section>
|
|
154
243
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Native chooser first; the host browse backend supplies the remote Web dialog. */
|
|
2
|
+
import { useEffect, useRef, useState } from 'react'
|
|
3
|
+
import { Button, Input, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
4
|
+
import { api } from './api.ts'
|
|
5
|
+
import { t } from './locales.ts'
|
|
6
|
+
import { IconFolderOpen16 } from './icons.tsx'
|
|
7
|
+
import type { PluginProjectDirectories, PluginProjectPick } from '../plugin-market-shared.ts'
|
|
8
|
+
import css from './PluginMarketView.module.css'
|
|
9
|
+
|
|
10
|
+
export interface PluginProjectRequest {
|
|
11
|
+
result: Promise<PluginProjectPick>
|
|
12
|
+
controller: AbortController
|
|
13
|
+
finish: (path: string | null) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function PluginProjectPicker({ request, initialPath, onPicked, onCancel, onError }: {
|
|
17
|
+
request: PluginProjectRequest
|
|
18
|
+
initialPath?: string
|
|
19
|
+
onPicked: (path: string) => void
|
|
20
|
+
onCancel: () => void
|
|
21
|
+
onError: (message: string) => void
|
|
22
|
+
}): JSX.Element | null {
|
|
23
|
+
const [browse, setBrowse] = useState(false)
|
|
24
|
+
const [path, setPath] = useState(initialPath ?? '')
|
|
25
|
+
const [draft, setDraft] = useState(initialPath ?? '')
|
|
26
|
+
const [listing, setListing] = useState<PluginProjectDirectories>()
|
|
27
|
+
const [loading, setLoading] = useState(true)
|
|
28
|
+
const [error, setError] = useState('')
|
|
29
|
+
const [revision, setRevision] = useState(0)
|
|
30
|
+
const callbacks = useRef({ onPicked, onCancel, onError })
|
|
31
|
+
callbacks.current = { onPicked, onCancel, onError }
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
let active = true
|
|
34
|
+
void request.result.then(result => {
|
|
35
|
+
if (!active) return
|
|
36
|
+
if (result.kind === 'browse') setBrowse(true)
|
|
37
|
+
else if (result.path === null) callbacks.current.onCancel()
|
|
38
|
+
else callbacks.current.onPicked(result.path)
|
|
39
|
+
}).catch(cause => { if (active) callbacks.current.onError(cause instanceof Error ? cause.message : String(cause)) })
|
|
40
|
+
return () => { active = false }
|
|
41
|
+
}, [request])
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!browse) return
|
|
44
|
+
const controller = new AbortController()
|
|
45
|
+
let active = true
|
|
46
|
+
setLoading(true); setListing(undefined); setError('')
|
|
47
|
+
void api.pluginProjectDirectories(path || undefined, controller.signal).then(value => {
|
|
48
|
+
if (active) { setListing(value); setDraft(value.path); setLoading(false) }
|
|
49
|
+
}).catch(cause => {
|
|
50
|
+
if (active) { setError(cause instanceof Error ? cause.message : String(cause)); setLoading(false) }
|
|
51
|
+
})
|
|
52
|
+
return () => { active = false; controller.abort() }
|
|
53
|
+
}, [browse, path, revision])
|
|
54
|
+
const navigate = (next: string): void => { setLoading(true); setListing(undefined); setPath(next); setRevision(value => value + 1) }
|
|
55
|
+
if (!browse) return null
|
|
56
|
+
return <Modal open title={t('pluginMarketChooseProject')} description={t('pluginMarketProjectHint')} closeLabel={t('cancel')} onClose={onCancel} className={css.projectPicker}
|
|
57
|
+
footer={<><Button variant="outline" onClick={onCancel}>{t('cancel')}</Button><Button variant="primary" disabled={loading || !listing} onClick={() => { if (listing) onPicked(listing.path) }}>{t('pluginMarketSelectDirectory')}</Button></>}>
|
|
58
|
+
<form className={css.directoryPath} onSubmit={event => { event.preventDefault(); navigate(draft) }}>
|
|
59
|
+
<Input data-modal-autofocus disabled={loading} aria-label={t('pluginMarketDirectoryPath')} value={draft} onChange={event => setDraft(event.target.value)} />
|
|
60
|
+
<Button type="submit" variant="outline" disabled={loading}>{t('pluginMarketOpenDirectory')}</Button>
|
|
61
|
+
</form>
|
|
62
|
+
<nav className={css.directoryCrumbs} aria-label={t('pluginMarketDirectoryPath')}>
|
|
63
|
+
<Button size="sm" variant="ghost" disabled={loading} onClick={() => navigate('')}>{t('pluginMarketHomeDirectory')}</Button>
|
|
64
|
+
{listing?.crumbs.map(crumb => <Button key={crumb.path} size="sm" variant="ghost" disabled={loading} onClick={() => navigate(crumb.path)}>{crumb.name}</Button>)}
|
|
65
|
+
</nav>
|
|
66
|
+
{error ? <p className={css.error} role="alert">{error}</p> : null}
|
|
67
|
+
{loading ? <p className={css.state} role="status">{t('pluginMarketLoading')}</p> : null}
|
|
68
|
+
<div className={css.directoryList} aria-busy={loading}>
|
|
69
|
+
{listing?.entries.map(entry => <button key={entry.path} type="button" onClick={() => navigate(entry.path)}><IconFolderOpen16 />{entry.name}</button>)}
|
|
70
|
+
</div>
|
|
71
|
+
{listing?.truncated ? <p className={css.state}>{t('pluginMarketDirectoryTruncated')}</p> : null}
|
|
72
|
+
</Modal>
|
|
73
|
+
}
|
package/src/client/api.ts
CHANGED
|
@@ -8,7 +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
|
+
import type { MarketplaceCatalog, MarketplacePlugin, InstalledMarketplacePlugin, PluginMarketScope, PluginProjectPick, PluginProjectDirectories } from '../plugin-market-shared.ts'
|
|
12
12
|
|
|
13
13
|
/** One wire failure. */
|
|
14
14
|
export class SidebarApiError extends Error {
|
|
@@ -288,12 +288,14 @@ function gitPayload(scope: SessionScope, worktree: string | undefined, extra: Re
|
|
|
288
288
|
|
|
289
289
|
/** The sidebar API surface (session scope threaded through every call). */
|
|
290
290
|
export const api = {
|
|
291
|
-
pluginMarketContext: (scope: SessionScope, signal?: AbortSignal) => call<{ sessionId: string; cwd
|
|
291
|
+
pluginMarketContext: (scope: SessionScope, signal?: AbortSignal) => call<{ sessionId: string; cwd?: string }>('plugins.context', scopePayload(scope, {}), signal),
|
|
292
|
+
pluginProjectPick: (signal?: AbortSignal) => call<PluginProjectPick>('plugins.project.pick', {}, signal),
|
|
293
|
+
pluginProjectDirectories: (path?: string, signal?: AbortSignal) => call<PluginProjectDirectories>('plugins.project.directories', { path }, signal),
|
|
292
294
|
pluginMarketplaces: (signal?: AbortSignal) => call<MarketplaceCatalog>('plugins.marketplaces', {}, signal),
|
|
293
295
|
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 })),
|
|
296
|
+
pluginsInstalled: (scope: SessionScope, signal?: AbortSignal, projectPath?: string) => call<InstalledMarketplacePlugin[]>('plugins.installed', scopePayload(scope, { projectPath }), signal),
|
|
297
|
+
pluginInstall: (scope: SessionScope, pluginId: string, installScope: PluginMarketScope, projectPath?: string) => call<InstalledMarketplacePlugin>('plugins.install', scopePayload(scope, { pluginId, scope: installScope, projectPath })),
|
|
298
|
+
pluginUninstall: (scope: SessionScope, pluginId: string, installScope: PluginMarketScope, projectPath?: string) => call<{ ok: true }>('plugins.uninstall', scopePayload(scope, { pluginId, scope: installScope, projectPath })),
|
|
297
299
|
chatcodeStatus: (signal?: AbortSignal) => call<ChatCodeAuthState>('chatcode.status', {}, signal),
|
|
298
300
|
chatcodeStart: () => call<{ sessionId: string; url: string }>('chatcode.start', {}),
|
|
299
301
|
chatcodeCancel: (sessionId: string) => call<{ ok: true }>('chatcode.cancel', { sessionId }),
|
package/src/client/locales-ja.ts
CHANGED
|
@@ -30,13 +30,29 @@ export const ja: Record<string, string> = {
|
|
|
30
30
|
pluginMarketUser: 'ユーザー',
|
|
31
31
|
pluginMarketInstalled: 'プラグインをインストールしました。',
|
|
32
32
|
pluginMarketUninstalled: 'プラグインをアンインストールしました。',
|
|
33
|
-
pluginMarketSubtitle: '
|
|
33
|
+
pluginMarketSubtitle: 'マーケットを参照し、プロジェクトディレクトリを選ぶかユーザー環境にインストールします。',
|
|
34
|
+
pluginMarketChooseProject: 'プロジェクトディレクトリを選択',
|
|
35
|
+
pluginMarketProjectHint: 'プロジェクトにインストールする場合は対象フォルダーを選択してください。',
|
|
36
|
+
pluginMarketChoosingDirectory: 'ディレクトリを選択中…',
|
|
37
|
+
pluginMarketAwaitingConfirmation: '確認待ち…',
|
|
38
|
+
pluginMarketConfirmProjectInstall: 'プロジェクトへのインストールを確認',
|
|
39
|
+
pluginMarketConfirmProjectDescription: '{name} を以下のプロジェクトディレクトリにインストールします。変更する場合はキャンセルし、ページ上部でディレクトリを選択してください。',
|
|
40
|
+
pluginMarketConfirmInstall: 'インストールを確認',
|
|
41
|
+
pluginMarketInstalledAt: 'プラグインのインストール先:{path}',
|
|
42
|
+
pluginMarketInstallPath: '{scope}のインストール先:{path}',
|
|
43
|
+
pluginMarketDirectoryPath: 'ディレクトリパス',
|
|
44
|
+
pluginMarketOpenDirectory: 'ディレクトリを開く',
|
|
45
|
+
pluginMarketSelectDirectory: 'このディレクトリを選択',
|
|
46
|
+
pluginMarketHomeDirectory: 'ホーム',
|
|
47
|
+
pluginMarketDirectoryTruncated: '一部のフォルダーのみ表示しています。完全なパスを入力してプロジェクトを開けます。',
|
|
34
48
|
pluginMarketSearch: 'マーケットやプラグインを検索',
|
|
35
49
|
pluginMarketDefault: 'デフォルト',
|
|
36
50
|
pluginMarketBrowse: 'プラグインを見る',
|
|
37
51
|
pluginMarketNoDescription: '説明なし',
|
|
38
52
|
pluginMarketInstalling: 'インストール中…',
|
|
39
53
|
pluginMarketUninstalling: 'アンインストール中…',
|
|
54
|
+
pluginMarketInstallProgress: '{name} をインストールしています。しばらくお待ちください…',
|
|
55
|
+
pluginMarketUninstallProgress: '{name} をアンインストールしています。しばらくお待ちください…',
|
|
40
56
|
accountMenuSettings: '設定',
|
|
41
57
|
accountMenuLogout: 'ログアウト',
|
|
42
58
|
accountMenuLogoutConfirmTitle: 'ログアウトしますか?',
|