@chatcode/cco-better-sidebar 0.18.0-rc.5 → 0.18.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,13 +37,25 @@ export const zh = {
37
37
  pluginMarketUser: '用户级',
38
38
  pluginMarketInstalled: '插件安装成功。',
39
39
  pluginMarketUninstalled: '插件卸载成功。',
40
- pluginMarketSubtitle: '浏览插件市场,选择安装到当前项目或用户环境。',
40
+ pluginMarketSubtitle: '浏览插件市场,选择项目目录安装,或安装到用户环境。',
41
+ pluginMarketChooseProject: '选择项目目录',
42
+ pluginMarketProjectHint: '安装到项目时,请先选择目标项目文件夹。',
43
+ pluginMarketChoosingDirectory: '正在选择目录…',
44
+ pluginMarketInstalledAt: '插件已安装到:{path}',
45
+ pluginMarketInstallPath: '{scope}安装目录:{path}',
46
+ pluginMarketDirectoryPath: '目录路径',
47
+ pluginMarketOpenDirectory: '打开目录',
48
+ pluginMarketSelectDirectory: '选择此目录',
49
+ pluginMarketHomeDirectory: '主目录',
50
+ pluginMarketDirectoryTruncated: '目录过多,仅显示部分结果;可输入完整路径打开项目。',
41
51
  pluginMarketSearch: '搜索市场或插件',
42
52
  pluginMarketDefault: '默认',
43
53
  pluginMarketBrowse: '查看插件',
44
54
  pluginMarketNoDescription: '暂无描述',
45
55
  pluginMarketInstalling: '安装中…',
46
56
  pluginMarketUninstalling: '卸载中…',
57
+ pluginMarketInstallProgress: '正在安装 {name},请稍候…',
58
+ pluginMarketUninstallProgress: '正在卸载 {name},请稍候…',
47
59
  accountMenuSettings: '设置',
48
60
  accountMenuLogout: '退出登录',
49
61
  accountMenuLogoutConfirmTitle: '退出登录?',
@@ -531,13 +543,25 @@ export const en: Record<keyof typeof zh, string> = {
531
543
  pluginMarketUser: 'User',
532
544
  pluginMarketInstalled: 'Plugin installed.',
533
545
  pluginMarketUninstalled: 'Plugin uninstalled.',
534
- pluginMarketSubtitle: 'Browse marketplaces and install plugins for your project or user.',
546
+ pluginMarketSubtitle: 'Browse marketplaces, choose a project directory, or install for your user.',
547
+ pluginMarketChooseProject: 'Choose project directory',
548
+ pluginMarketProjectHint: 'Choose a target project folder when installing for a project.',
549
+ pluginMarketChoosingDirectory: 'Choosing directory…',
550
+ pluginMarketInstalledAt: 'Plugin installed at: {path}',
551
+ pluginMarketInstallPath: '{scope} installation directory: {path}',
552
+ pluginMarketDirectoryPath: 'Directory path',
553
+ pluginMarketOpenDirectory: 'Open directory',
554
+ pluginMarketSelectDirectory: 'Select this directory',
555
+ pluginMarketHomeDirectory: 'Home',
556
+ pluginMarketDirectoryTruncated: 'Only some folders are shown. Enter the full path to open your project.',
535
557
  pluginMarketSearch: 'Search marketplaces or plugins',
536
558
  pluginMarketDefault: 'Default',
537
559
  pluginMarketBrowse: 'Browse plugins',
538
560
  pluginMarketNoDescription: 'No description',
539
561
  pluginMarketInstalling: 'Installing…',
540
562
  pluginMarketUninstalling: 'Uninstalling…',
563
+ pluginMarketInstallProgress: 'Installing {name}, please wait…',
564
+ pluginMarketUninstallProgress: 'Uninstalling {name}, please wait…',
541
565
  accountMenuSettings: 'Settings',
542
566
  accountMenuLogout: 'Log out',
543
567
  accountMenuLogoutConfirmTitle: 'Log out?',
@@ -1038,9 +1062,42 @@ import { zhHK as zhHKDict } from './locales-zh-HK.ts'
1038
1062
  import { zhTW as zhTWDict } from './locales-zh-TW.ts'
1039
1063
  import { zhMO as zhMODict } from './locales-zh-MO.ts'
1040
1064
 
1041
- // English fallback for the new account/model surfaces until third-language translations are supplied.
1065
+ // English fallback for the new account/model/marketplace surfaces until third-language translations are supplied.
1042
1066
  // Keep this list explicit so the dictionary parity check still catches other missing keys.
1043
1067
  const accountAndModelFallback = {
1068
+ pluginMarketTitle: en.pluginMarketTitle,
1069
+ pluginMarketProjectDirectory: en.pluginMarketProjectDirectory,
1070
+ pluginMarketInstalledTab: en.pluginMarketInstalledTab,
1071
+ pluginMarketRefresh: en.pluginMarketRefresh,
1072
+ pluginMarketLoading: en.pluginMarketLoading,
1073
+ pluginMarketRefreshing: en.pluginMarketRefreshing,
1074
+ pluginMarketEmpty: en.pluginMarketEmpty,
1075
+ pluginMarketBack: en.pluginMarketBack,
1076
+ pluginMarketInstall: en.pluginMarketInstall,
1077
+ pluginMarketUninstall: en.pluginMarketUninstall,
1078
+ pluginMarketProject: en.pluginMarketProject,
1079
+ pluginMarketUser: en.pluginMarketUser,
1080
+ pluginMarketInstalled: en.pluginMarketInstalled,
1081
+ pluginMarketUninstalled: en.pluginMarketUninstalled,
1082
+ pluginMarketSubtitle: en.pluginMarketSubtitle,
1083
+ pluginMarketChooseProject: en.pluginMarketChooseProject,
1084
+ pluginMarketProjectHint: en.pluginMarketProjectHint,
1085
+ pluginMarketChoosingDirectory: en.pluginMarketChoosingDirectory,
1086
+ pluginMarketInstalledAt: en.pluginMarketInstalledAt,
1087
+ pluginMarketInstallPath: en.pluginMarketInstallPath,
1088
+ pluginMarketDirectoryPath: en.pluginMarketDirectoryPath,
1089
+ pluginMarketOpenDirectory: en.pluginMarketOpenDirectory,
1090
+ pluginMarketSelectDirectory: en.pluginMarketSelectDirectory,
1091
+ pluginMarketHomeDirectory: en.pluginMarketHomeDirectory,
1092
+ pluginMarketDirectoryTruncated: en.pluginMarketDirectoryTruncated,
1093
+ pluginMarketSearch: en.pluginMarketSearch,
1094
+ pluginMarketDefault: en.pluginMarketDefault,
1095
+ pluginMarketBrowse: en.pluginMarketBrowse,
1096
+ pluginMarketNoDescription: en.pluginMarketNoDescription,
1097
+ pluginMarketInstalling: en.pluginMarketInstalling,
1098
+ pluginMarketUninstalling: en.pluginMarketUninstalling,
1099
+ pluginMarketInstallProgress: en.pluginMarketInstallProgress,
1100
+ pluginMarketUninstallProgress: en.pluginMarketUninstallProgress,
1044
1101
  accountMenuLogoutConfirmTitle: en.accountMenuLogoutConfirmTitle,
1045
1102
  accountMenuLogoutConfirmDescription: en.accountMenuLogoutConfirmDescription,
1046
1103
  chatcodeGateChecking: en.chatcodeGateChecking,
@@ -1,8 +1,15 @@
1
1
  /** Shared config plugin routes inside the sidebar's existing trust fence. */
2
2
  import type { Context } from './context-types.ts'
3
- import type { PluginMarketService, PluginMarketScope } from './plugin-market-shared.ts'
3
+ import { realpath, stat } from 'node:fs/promises'
4
+ import { isAbsolute } from 'node:path'
5
+ import type { PluginMarketService, PluginMarketScope, PluginProjectDirectories } from './plugin-market-shared.ts'
4
6
  import { requireString, SidebarError } from './wire.ts'
5
7
 
8
+ interface DirectoryPicker {
9
+ capability(): { kind: 'native'; pick(signal: AbortSignal): Promise<string | null> }
10
+ | { kind: 'browse'; list(path?: string, signal?: AbortSignal): Promise<PluginProjectDirectories> }
11
+ }
12
+
6
13
  export function buildPluginMarketApi(ctx: Pick<Context, 'get'>, cwdOf: (payload: unknown) => Promise<{ cwd: string }>): Record<string, (payload: unknown) => Promise<unknown>> {
7
14
  const service = (): PluginMarketService => {
8
15
  const value = ctx.get('chatcodePluginMarket', false) as PluginMarketService | undefined
@@ -14,17 +21,46 @@ export function buildPluginMarketApi(ctx: Pick<Context, 'get'>, cwdOf: (payload:
14
21
  if (scope !== 'project' && scope !== 'user') throw new SidebarError('bad-request', 'scope must be project or user')
15
22
  return scope
16
23
  }
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 }> => {
24
+ const selectedProject = async (payload: unknown): Promise<string> => {
25
+ if (typeof (payload as { projectPath?: unknown } | null)?.projectPath !== 'string') throw new SidebarError('bad-request', '请先选择目标项目目录。')
26
+ const path = requireString(payload, 'projectPath')
27
+ const qualified = process.platform !== 'win32' || /^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/][^\\/]+)/.test(path)
28
+ if (!isAbsolute(path) || !qualified || path.includes('\0')) throw new SidebarError('bad-request', '请选择有效的项目目录。')
29
+ const canonical = await realpath(path)
30
+ if (!(await stat(canonical)).isDirectory()) throw new SidebarError('bad-request', '请选择项目文件夹。')
31
+ return canonical
32
+ }
33
+ const picker = () => {
34
+ const value = ctx.get('directoryPicker', false) as DirectoryPicker | undefined
35
+ if (!value) throw new SidebarError('method-error', '目录选择器未挂载,请启用宿主目录选择插件。', 503)
36
+ return value.capability()
37
+ }
38
+ // A sessionless settings page has no implicit project. Only user-scoped
39
+ // operations may use the launch cwd internally, which never becomes a target.
40
+ const contextOf = async (payload: unknown): Promise<{ sessionId: string; cwd?: string }> => {
21
41
  const sessionId = (payload as { sessionId?: unknown } | null)?.sessionId
22
- if (sessionId === undefined || sessionId === '') return { sessionId: '', cwd: process.cwd() }
42
+ if (sessionId === undefined || sessionId === '') return { sessionId: '' }
23
43
  requireString(payload, 'sessionId')
24
44
  return { sessionId: sessionId as string, cwd: (await cwdOf(payload)).cwd }
25
45
  }
46
+ const projectOf = async (payload: unknown): Promise<string | undefined> => {
47
+ if ((payload as { projectPath?: unknown } | null)?.projectPath !== undefined) return selectedProject(payload)
48
+ return (await contextOf(payload)).cwd
49
+ }
26
50
  return {
27
51
  'plugins.context': contextOf,
52
+ 'plugins.project.pick': async () => {
53
+ const capability = picker()
54
+ if (capability.kind === 'browse') return { kind: 'browse' }
55
+ const path = await capability.pick(AbortSignal.timeout(120_000))
56
+ return { kind: 'native', path: path === null ? null : await selectedProject({ projectPath: path }) }
57
+ },
58
+ 'plugins.project.directories': async payload => {
59
+ const capability = picker()
60
+ if (capability.kind !== 'browse') throw new SidebarError('method-error', '当前宿主使用系统文件夹选择窗口。')
61
+ const path = (payload as { path?: unknown } | null)?.path
62
+ return capability.list(path === undefined ? undefined : requireString(payload, 'path'), AbortSignal.timeout(15_000))
63
+ },
28
64
  'plugins.marketplaces': () => service().marketplaces(),
29
65
  'plugins.catalog': async payload => {
30
66
  const market = service()
@@ -32,10 +68,20 @@ export function buildPluginMarketApi(ctx: Pick<Context, 'get'>, cwdOf: (payload:
32
68
  if ((payload as { includeMetadata?: unknown }).includeMetadata !== true) return market.plugins(id)
33
69
  return market.catalog ? market.catalog(id) : { plugins: await market.plugins(id), warning: '' }
34
70
  },
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)),
71
+ 'plugins.installed': async payload => {
72
+ const project = await projectOf(payload)
73
+ const records = await service().installed(project ?? process.cwd())
74
+ return project ? records : records.filter(record => record.scope === 'user')
75
+ },
76
+ 'plugins.install': async payload => {
77
+ const scope = scopeOf(payload)
78
+ const project = scope === 'project' ? await selectedProject(payload) : await projectOf(payload)
79
+ return service().install(project ?? process.cwd(), requireString(payload, 'pluginId'), scope)
80
+ },
37
81
  'plugins.uninstall': async payload => {
38
- await service().uninstall((await contextOf(payload)).cwd, requireString(payload, 'pluginId'), scopeOf(payload))
82
+ const scope = scopeOf(payload)
83
+ const project = scope === 'project' ? await selectedProject(payload) : await projectOf(payload)
84
+ await service().uninstall(project ?? process.cwd(), requireString(payload, 'pluginId'), scope)
39
85
  return { ok: true }
40
86
  },
41
87
  }
@@ -11,6 +11,15 @@ export interface InstalledMarketplacePlugin extends MarketplacePlugin {
11
11
  }
12
12
  export interface MarketplaceCatalog { marketplaces: PluginMarketplace[]; warning: string }
13
13
  export interface PluginCatalog { plugins: MarketplacePlugin[]; warning: string }
14
+ /** Host-owned paths for the remote Web directory browser. */
15
+ export interface PluginProjectDirectories {
16
+ path: string
17
+ home: string
18
+ crumbs: Array<{ name: string; path: string; hidden: boolean }>
19
+ entries: Array<{ name: string; path: string; hidden: boolean }>
20
+ truncated: boolean
21
+ }
22
+ export type PluginProjectPick = { kind: 'native'; path: string | null } | { kind: 'browse' }
14
23
  export interface PluginMarketService {
15
24
  marketplaces(): Promise<MarketplaceCatalog>
16
25
  plugins(marketplaceId: string): Promise<MarketplacePlugin[]>