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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,30 +7,38 @@ import { IconChevronLeftOutline14, IconChevronRightOutline14, IconRefreshOutline
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'
10
11
 
11
12
  export function PluginMarketView({ scope, visible, catalog }: { scope: SessionScope; visible: boolean; catalog: PluginMarketCatalog }): JSX.Element {
12
13
  const snapshot = useSyncExternalStore(catalog.subscribe, catalog.getSnapshot)
13
14
  const [marketId, setMarketId] = useState<string | null>(null)
14
15
  const [tab, setTab] = useState<'markets' | 'installed'>('markets')
15
16
  const [query, setQuery] = useState('')
16
- const [installed, setInstalled] = useState<InstalledMarketplacePlugin[]>([])
17
- const [recordsReady, setRecordsReady] = useState(false)
18
- const [recordsError, setRecordsError] = useState('')
17
+ const [recordState, setRecordState] = useState<{ key: string; records: InstalledMarketplacePlugin[]; ready: boolean; error: string }>()
19
18
  const [message, setMessage] = useState<{ text: string; error: boolean }>()
20
- const [working, setWorking] = useState<{ id: string; scope: PluginMarketScope }>()
19
+ const [working, setWorking] = useState<{ id: string; name: string; scope: PluginMarketScope; uninstall: boolean; choosing?: boolean }>()
20
+ const [selection, setSelection] = useState<{ key: string; path: string }>()
21
+ const [picker, setPicker] = useState<PluginProjectRequest>()
22
+ const pickerRef = useRef<PluginProjectRequest>()
21
23
  const [revision, setRevision] = useState(0)
22
24
  const pending = useRef(false)
23
25
  const breadcrumbNode = useRef<HTMLDivElement>(null)
24
26
  const mutationVersion = useRef(0)
25
27
  const focusBack = useRef(false)
26
28
  const scopeKey = `${scope.sessionId}\0${scope.cwd ?? ''}`
29
+ const projectPath = selection?.key === scopeKey ? selection.path : scope.cwd
30
+ const recordsKey = `${scopeKey}\0${projectPath ?? ''}`
31
+ const installed = recordState?.key === recordsKey ? recordState.records : []
32
+ const recordsReady = recordState?.key === recordsKey && recordState.ready
33
+ const recordsError = recordState?.key === recordsKey ? recordState.error : ''
27
34
  const latestScope = useRef(scopeKey)
28
35
  latestScope.current = scopeKey
29
36
  const markets = snapshot.markets?.data?.marketplaces ?? []
30
37
  const market = markets.find(item => item.id === marketId)
31
38
  const catalogEntry = tab === 'markets' && marketId ? snapshot.plugins[marketId] : snapshot.markets
32
39
  const loading = snapshot.refresh?.status === 'loading' || (tab === 'markets' && (!catalogEntry || catalogEntry.status === 'loading'))
33
- const blocked = loading || snapshot.refresh?.status === 'error'
40
+ const operating = !!working && !working.choosing
41
+ const blocked = loading || snapshot.refresh?.status === 'error' || !!working
34
42
 
35
43
  useEffect(() => {
36
44
  if (!visible) return
@@ -40,9 +48,11 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
40
48
  useEffect(() => { setQuery('') }, [tab, marketId])
41
49
  useEffect(() => { if (focusBack.current) { breadcrumbNode.current?.querySelector('button')?.focus(); focusBack.current = false } }, [marketId])
42
50
 
43
- useEffect(() => {
44
- setInstalled([]); setRecordsReady(false); setRecordsError(''); setMessage(undefined)
45
- }, [scopeKey])
51
+ useEffect(() => { setMessage(undefined) }, [scopeKey])
52
+ useEffect(() => () => {
53
+ pickerRef.current?.controller.abort()
54
+ pickerRef.current?.finish(null)
55
+ }, [])
46
56
  useEffect(() => {
47
57
  if (!visible) return
48
58
  const controller = new AbortController()
@@ -53,10 +63,10 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
53
63
  reading = true
54
64
  const version = mutationVersion.current
55
65
  try {
56
- const records = await api.pluginsInstalled(scope, controller.signal)
57
- if (active && !pending.current && version === mutationVersion.current) { setInstalled(records); setRecordsReady(true); setRecordsError('') }
66
+ const records = await api.pluginsInstalled(scope, controller.signal, projectPath)
67
+ if (active && !pending.current && version === mutationVersion.current) setRecordState({ key: recordsKey, records, ready: true, error: '' })
58
68
  } catch (cause) {
59
- if (active && version === mutationVersion.current) setRecordsError(cause instanceof Error ? cause.message : String(cause))
69
+ 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
70
  } finally { reading = false }
61
71
  }
62
72
  void read()
@@ -64,28 +74,70 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
64
74
  // Remote catalogs are never polled or refreshed by navigation.
65
75
  const timer = setInterval(() => { void read() }, 2000)
66
76
  return () => { active = false; clearInterval(timer); controller.abort() }
67
- }, [scopeKey, visible, revision])
77
+ }, [recordsKey, visible, revision])
78
+
79
+ const chooseDirectory = (): Promise<string | null> => new Promise(finish => {
80
+ const controller = new AbortController()
81
+ const request = { result: api.pluginProjectPick(controller.signal), controller, finish }
82
+ pickerRef.current = request
83
+ setPicker(request)
84
+ })
85
+ const finishPicker = (path: string | null): void => {
86
+ pickerRef.current?.finish(path)
87
+ pickerRef.current?.controller.abort()
88
+ pickerRef.current = undefined
89
+ setPicker(undefined)
90
+ }
91
+ const chooseProject = async (): Promise<void> => {
92
+ if (pending.current || blocked) return
93
+ pending.current = true
94
+ mutationVersion.current++
95
+ setMessage(undefined)
96
+ const key = scopeKey
97
+ try {
98
+ const path = await chooseDirectory()
99
+ if (path && latestScope.current === key) setSelection({ key, path })
100
+ } finally { pending.current = false; setRevision(value => value + 1) }
101
+ }
68
102
 
69
103
  const perform = async (plugin: MarketplacePlugin, installScope: PluginMarketScope): Promise<void> => {
70
104
  const refreshStatus = catalog.getSnapshot().refresh?.status
71
105
  if (pending.current || !recordsReady || blocked || refreshStatus === 'loading' || refreshStatus === 'error') return
72
106
  const key = scopeKey
73
- const present = installed.some(item => item.id === plugin.id && item.scope === installScope)
107
+ const record = installed.find(item => item.id === plugin.id && item.scope === installScope)
108
+ const present = !!record
74
109
  pending.current = true
75
110
  mutationVersion.current++
76
- setWorking({ id: plugin.id, scope: installScope })
111
+ const operation = { id: plugin.id, name: plugin.name, scope: installScope, uninstall: present }
112
+ setWorking({ ...operation, choosing: installScope === 'project' && !present })
77
113
  setMessage(undefined)
78
114
  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)
115
+ let target = record?.scope === 'project' ? record.projectPath : projectPath
116
+ if (installScope === 'project' && !present) {
117
+ const chosen = await chooseDirectory()
118
+ if (!chosen || latestScope.current !== key) return
119
+ target = chosen
120
+ setSelection({ key, path: target })
121
+ setWorking(operation)
122
+ }
123
+ let installedPath = ''
124
+ if (present) await api.pluginUninstall(scope, plugin.id, installScope, target)
125
+ else {
126
+ const result = await api.pluginInstall(scope, plugin.id, installScope, target)
127
+ installedPath = result.installPath
128
+ if (installScope === 'project') {
129
+ target = result.projectPath
130
+ if (latestScope.current === key) setSelection({ key, path: target })
131
+ }
132
+ }
133
+ const records = await api.pluginsInstalled(scope, undefined, target)
82
134
  if (latestScope.current === key) {
83
- setInstalled(records); setRecordsReady(true); setRecordsError('')
84
- setMessage({ text: t(present ? 'pluginMarketUninstalled' : 'pluginMarketInstalled'), error: false })
135
+ setRecordState({ key: `${key}\0${target ?? ''}`, records, ready: true, error: '' })
136
+ setMessage({ text: present ? t('pluginMarketUninstalled') : t('pluginMarketInstalledAt', { path: installedPath }), error: false })
85
137
  }
86
138
  } catch (cause) {
87
139
  if (latestScope.current === key) setMessage({ text: cause instanceof Error ? cause.message : String(cause), error: true })
88
- } finally { pending.current = false; setWorking(undefined) }
140
+ } finally { pending.current = false; setWorking(undefined); setRevision(value => value + 1) }
89
141
  }
90
142
  const refresh = (): void => {
91
143
  if (loading || pending.current) return
@@ -104,12 +156,15 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
104
156
  const warning = [snapshot.markets?.data?.warning, marketId ? snapshot.plugins[marketId]?.warning : ''].filter(Boolean).join('\n')
105
157
  const hasData = tab === 'installed' ? recordsReady : !!catalogEntry?.data
106
158
 
107
- return <section className={css.root} aria-label={t('pluginMarketTitle')} aria-busy={loading} data-chatcode-plugin-market>
159
+ return <section className={css.root} aria-label={t('pluginMarketTitle')} aria-busy={loading || operating} data-chatcode-plugin-market>
108
160
  <header className={css.head}>
109
161
  <div><h2 className={css.title}>{t('pluginMarketTitle')}</h2><p className={css.subtitle}>{t('pluginMarketSubtitle')}</p></div>
110
162
  <Input disabled={blocked} value={query} onChange={event => setQuery(event.target.value)} placeholder={t('pluginMarketSearch')} aria-label={t('pluginMarketSearch')} />
111
163
  </header>
112
- {scope.cwd ? <p className={css.project}>{t('pluginMarketProjectDirectory', { path: scope.cwd })}</p> : null}
164
+ <div className={css.projectRow}>
165
+ <p className={css.project}>{projectPath ? t('pluginMarketProjectDirectory', { path: projectPath }) : t('pluginMarketProjectHint')}</p>
166
+ <Button size="sm" variant="outline" disabled={blocked || !!working || !!picker} onClick={() => { void chooseProject() }}>{t('pluginMarketChooseProject')}</Button>
167
+ </div>
113
168
  <div className={css.toolbar}>
114
169
  <div className={css.tabs}>
115
170
  <Button size="sm" variant="ghost" disabled={blocked} aria-pressed={tab === 'markets'} onClick={() => setTab('markets')}>{t('pluginMarketTitle')}</Button>
@@ -117,6 +172,10 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
117
172
  </div>
118
173
  <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
174
  </div>
175
+ {operating ? <div className={css.operation} role="status">
176
+ <span className={css.spinning} aria-hidden="true"><IconRefreshOutline14 /></span>
177
+ <span>{t(working.uninstall ? 'pluginMarketUninstallProgress' : 'pluginMarketInstallProgress', { name: working.name })}</span>
178
+ </div> : null}
120
179
  {warning && tab === 'markets' ? <p className={css.warning} role="status">{warning}</p> : null}
121
180
  {error ? <p className={css.error} role="status">{error}</p> : null}
122
181
  {message ? <p className={message.error ? css.error : css.notice} role="status">{message.text}</p> : null}
@@ -125,7 +184,7 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
125
184
  <span aria-hidden="true">/</span><span>{market?.name ?? marketId}</span>
126
185
  </div> : null}
127
186
  {loading ? <p className={css.state} role="status">{t(hasData ? 'pluginMarketRefreshing' : 'pluginMarketLoading')}</p> : null}
128
- <div className={css.grid} aria-busy={loading}>
187
+ <div className={css.grid} aria-busy={loading || operating}>
129
188
  {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
189
  <div className={css.cardHead}><h3>{item.name}</h3>{item.official ? <Pill>{t('pluginMarketDefault')}</Pill> : null}</div>
131
190
  <p className={css.description}>{item.description || t('pluginMarketNoDescription')}</p>
@@ -137,12 +196,14 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
137
196
  <div className={css.cardHead}><h3>{item.name}</h3>{records.length ? <Pill>{t('pluginMarketInstalledTab')}</Pill> : null}</div>
138
197
  <p className={css.description}>{item.description || t('pluginMarketNoDescription')}</p>
139
198
  <p className={css.meta}>{marketName}{tab === 'installed' && records[0]?.version ? ` · ${records[0].version}` : ''}</p>
199
+ {records.map(record => <p key={record.scope} className={css.installPath}>{t('pluginMarketInstallPath', { scope: t(record.scope === 'project' ? 'pluginMarketProject' : 'pluginMarketUser'), path: record.installPath })}</p>)}
140
200
  <div className={css.actions}>
141
201
  {(['project', 'user'] as const).map(installScope => {
142
202
  const present = records.some(record => record.scope === installScope)
143
203
  const busy = working?.id === item.id && working.scope === installScope
144
204
  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')}
205
+ {busy && operating ? <span className={css.spinning} aria-hidden="true"><IconRefreshOutline14 /></span> : null}
206
+ {t(busy ? working?.choosing ? 'pluginMarketChoosingDirectory' : working?.uninstall ? 'pluginMarketUninstalling' : 'pluginMarketInstalling' : present ? 'pluginMarketUninstall' : 'pluginMarketInstall')} · {t(installScope === 'project' ? 'pluginMarketProject' : 'pluginMarketUser')}
146
207
  </Button>
147
208
  })}
148
209
  </div>
@@ -150,5 +211,6 @@ export function PluginMarketView({ scope, visible, catalog }: { scope: SessionSc
150
211
  })}
151
212
  </div>
152
213
  {!loading && hasData && (showMarkets ? filteredMarkets : filteredPlugins).length === 0 ? <p className={css.state}>{t('pluginMarketEmpty')}</p> : null}
214
+ {picker ? <PluginProjectPicker request={picker} initialPath={projectPath} onPicked={finishPicker} onCancel={() => finishPicker(null)} onError={text => { setMessage({ text, error: true }); finishPicker(null) }} /> : null}
153
215
  </section>
154
216
  }
@@ -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: string }>('plugins.context', scopePayload(scope, {}), signal),
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 }),
@@ -30,15 +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
+ pluginMarketInstalledAt: 'プラグインのインストール先:{path}',
38
+ pluginMarketInstallPath: '{scope}のインストール先:{path}',
39
+ pluginMarketDirectoryPath: 'ディレクトリパス',
40
+ pluginMarketOpenDirectory: 'ディレクトリを開く',
41
+ pluginMarketSelectDirectory: 'このディレクトリを選択',
42
+ pluginMarketHomeDirectory: 'ホーム',
43
+ pluginMarketDirectoryTruncated: '一部のフォルダーのみ表示しています。完全なパスを入力してプロジェクトを開けます。',
34
44
  pluginMarketSearch: 'マーケットやプラグインを検索',
35
45
  pluginMarketDefault: 'デフォルト',
36
46
  pluginMarketBrowse: 'プラグインを見る',
37
47
  pluginMarketNoDescription: '説明なし',
38
48
  pluginMarketInstalling: 'インストール中…',
39
49
  pluginMarketUninstalling: 'アンインストール中…',
50
+ pluginMarketInstallProgress: '{name} をインストールしています。しばらくお待ちください…',
51
+ pluginMarketUninstallProgress: '{name} をアンインストールしています。しばらくお待ちください…',
40
52
  accountMenuSettings: '設定',
41
53
  accountMenuLogout: 'ログアウト',
54
+ accountMenuLogoutConfirmTitle: 'ログアウトしますか?',
55
+ accountMenuLogoutConfirmDescription: 'ChatCode CLI からログアウトしますか?続行するには再度ログインする必要があります。',
42
56
  accountMenuLabel: 'アカウントメニュー',
43
57
  updateMenu: '更新を確認 / 今すぐ更新',
44
58
  updateNav: '更新',
@@ -37,15 +37,29 @@ 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: '退出登录',
61
+ accountMenuLogoutConfirmTitle: '退出登录?',
62
+ accountMenuLogoutConfirmDescription: '确定要退出 ChatCode CLI 吗?退出后需要重新登录才能继续使用。',
49
63
  accountMenuLabel: '账户菜单',
50
64
  updateMenu: '检查更新 / 一键更新',
51
65
  updateNav: '更新',
@@ -529,15 +543,29 @@ export const en: Record<keyof typeof zh, string> = {
529
543
  pluginMarketUser: 'User',
530
544
  pluginMarketInstalled: 'Plugin installed.',
531
545
  pluginMarketUninstalled: 'Plugin uninstalled.',
532
- 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.',
533
557
  pluginMarketSearch: 'Search marketplaces or plugins',
534
558
  pluginMarketDefault: 'Default',
535
559
  pluginMarketBrowse: 'Browse plugins',
536
560
  pluginMarketNoDescription: 'No description',
537
561
  pluginMarketInstalling: 'Installing…',
538
562
  pluginMarketUninstalling: 'Uninstalling…',
563
+ pluginMarketInstallProgress: 'Installing {name}, please wait…',
564
+ pluginMarketUninstallProgress: 'Uninstalling {name}, please wait…',
539
565
  accountMenuSettings: 'Settings',
540
566
  accountMenuLogout: 'Log out',
567
+ accountMenuLogoutConfirmTitle: 'Log out?',
568
+ accountMenuLogoutConfirmDescription: 'Are you sure you want to log out of ChatCode CLI? You will need to sign in again to continue.',
541
569
  accountMenuLabel: 'Account menu',
542
570
  updateMenu: 'Check for updates / Update now',
543
571
  updateNav: 'Update',
@@ -1034,9 +1062,44 @@ import { zhHK as zhHKDict } from './locales-zh-HK.ts'
1034
1062
  import { zhTW as zhTWDict } from './locales-zh-TW.ts'
1035
1063
  import { zhMO as zhMODict } from './locales-zh-MO.ts'
1036
1064
 
1037
- // 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.
1038
1066
  // Keep this list explicit so the dictionary parity check still catches other missing keys.
1039
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,
1101
+ accountMenuLogoutConfirmTitle: en.accountMenuLogoutConfirmTitle,
1102
+ accountMenuLogoutConfirmDescription: en.accountMenuLogoutConfirmDescription,
1040
1103
  chatcodeGateChecking: en.chatcodeGateChecking,
1041
1104
  chatcodeGateUnverified: en.chatcodeGateUnverified,
1042
1105
  chatcodeGatePending: en.chatcodeGatePending,
@@ -442,13 +442,11 @@
442
442
  box-sizing: border-box;
443
443
  gap: 8px;
444
444
  width: 100%;
445
- min-height: 52px;
446
- padding: 10px 14px max(10px, env(safe-area-inset-bottom));
445
+ min-height: 44px;
446
+ padding: 8px 12px max(8px, env(safe-area-inset-bottom));
447
447
  border-radius: 0;
448
448
  background: var(--dsw-alias-bg-layer-1);
449
- font-family: var(--dsw-font-family-body);
450
- font-size: var(--dsw-font-size-body-sm);
451
- line-height: var(--dsw-font-line-height-body-sm);
449
+ font: var(--dsw-font-xs-13);
452
450
  transition: background var(--ds-transition-duration-slow) var(--ds-ease-in-out);
453
451
  }
454
452
 
@@ -466,18 +464,21 @@
466
464
  .accountAvatar {
467
465
  position: relative;
468
466
  display: grid;
469
- width: 28px;
470
- height: 28px;
467
+ box-sizing: border-box;
468
+ width: 24px;
469
+ height: 24px;
471
470
  flex: none;
472
471
  place-items: center;
473
472
  border-radius: 50%;
474
473
  background: var(--dsw-alias-bg-layer-3);
475
474
  border: 1px solid var(--dsw-alias-border-l2);
476
475
  color: var(--dsw-alias-fg-accent);
476
+ font: var(--dsw-font-xxs-12);
477
477
  font-weight: 600;
478
478
  }
479
479
 
480
480
  .accountName {
481
+ min-width: 0;
481
482
  overflow: hidden;
482
483
  text-overflow: ellipsis;
483
484
  white-space: nowrap;
@@ -490,10 +491,10 @@
490
491
  left: 8px;
491
492
  display: grid;
492
493
  box-sizing: border-box;
493
- width: min(252px, calc(100% - 16px));
494
- padding: 10px 8px 8px;
494
+ width: min(224px, calc(100% - 16px));
495
+ padding: 8px 6px 6px;
495
496
  border: 1px solid var(--dsw-alias-border-l2);
496
- border-radius: 12px;
497
+ border-radius: 10px;
497
498
  background: var(--dsw-alias-bg-layer-2);
498
499
  z-index: 1;
499
500
  }
@@ -502,16 +503,15 @@
502
503
  display: flex;
503
504
  align-items: center;
504
505
  gap: 8px;
505
- min-height: 36px;
506
- padding: 0 8px 8px;
507
- font-family: var(--dsw-font-family-body);
508
- font-size: var(--dsw-font-size-body-sm);
509
- line-height: var(--dsw-font-line-height-body-sm);
506
+ min-width: 0;
507
+ min-height: 28px;
508
+ padding: 0 6px 6px;
509
+ font: var(--dsw-font-xs-13);
510
510
  }
511
511
 
512
512
  .accountMenuDivider {
513
513
  height: 1px;
514
- margin: 0 4px 6px;
514
+ margin: 0 4px 4px;
515
515
  background: var(--dsw-alias-border-l2);
516
516
  }
517
517
 
@@ -519,12 +519,12 @@
519
519
  display: flex;
520
520
  align-items: center;
521
521
  gap: 8px;
522
- min-height: 32px;
523
- padding: 0 10px;
522
+ min-height: 30px;
523
+ padding: 0 8px;
524
524
  border-radius: 7px;
525
525
  background: transparent;
526
526
  text-align: left;
527
- font: var(--dsw-font-s-14);
527
+ font: var(--dsw-font-xs-13);
528
528
  }
529
529
 
530
530
  .accountItem:hover {
@@ -546,6 +546,68 @@
546
546
  border-color: var(--dsw-alias-bg-layer-1);
547
547
  }
548
548
 
549
+ /* The native top layer is viewport-centered even though the account rail is positioned. */
550
+ .accountLogoutDialog {
551
+ box-sizing: border-box;
552
+ width: min(400px, calc(100vw - 32px));
553
+ max-width: calc(100vw - 32px);
554
+ max-height: calc(100dvh - 32px);
555
+ margin: auto;
556
+ padding: 24px;
557
+ overflow: auto;
558
+ border: 1px solid var(--dsw-alias-border-l2);
559
+ border-radius: 16px;
560
+ background: var(--dsw-alias-bg-layer-2);
561
+ color: var(--dsw-alias-fg-base);
562
+ font: var(--dsw-font-s-14);
563
+ }
564
+
565
+ .accountLogoutDialog::backdrop {
566
+ background: var(--dsw-alias-bg-mask-1);
567
+ }
568
+
569
+ .accountLogoutTitle {
570
+ margin: 0;
571
+ font: var(--dsw-font-base-strong-16);
572
+ }
573
+
574
+ .accountLogoutDescription {
575
+ margin: 12px 0 24px;
576
+ color: var(--dsw-alias-label-secondary);
577
+ }
578
+
579
+ .accountLogoutActions {
580
+ display: flex;
581
+ flex-wrap: wrap;
582
+ justify-content: flex-end;
583
+ gap: 8px;
584
+ }
585
+
586
+ .accountLogoutActions button {
587
+ min-height: 34px;
588
+ padding: 6px 14px;
589
+ border: 1px solid var(--dsw-alias-border-l2);
590
+ border-radius: 8px;
591
+ background: var(--dsw-alias-bg-layer-2);
592
+ color: var(--dsw-alias-fg-base);
593
+ font: var(--dsw-font-xs-13);
594
+ cursor: pointer;
595
+ }
596
+
597
+ .accountLogoutActions button:hover {
598
+ background: var(--dsw-alias-bg-hover);
599
+ }
600
+
601
+ .accountLogoutActions button:focus-visible {
602
+ outline: 2px solid var(--dsw-alias-border-accent);
603
+ outline-offset: 2px;
604
+ }
605
+
606
+ .accountLogoutActions .accountLogoutConfirm {
607
+ border-color: var(--dsw-alias-state-error-primary);
608
+ color: var(--dsw-alias-state-error-primary);
609
+ }
610
+
549
611
  /* The compatibility action cluster used when the host exposes no native
550
612
  right-sidebar affordance. It stays pinned to the viewport corner, sitting
551
613
  flush in the 34px tab strip (28px buttons at top: 3) whose right end it