@chatcode/cco-better-sidebar 0.18.0-alpha.1 → 0.18.0-alpha.2

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.
@@ -0,0 +1,4 @@
1
+ /** Build the local ChatCode login gate URL while preserving the current page. */
2
+ export declare function chatcodeLoginRoute(location: Pick<Location, 'pathname' | 'search' | 'hash'>): string;
3
+ /** Enter the existing local login flow after account state cannot be read. */
4
+ export declare function redirectToChatcodeLogin(location?: Location): void;
@@ -0,0 +1,11 @@
1
+ import type { Context } from '../context-types.ts';
2
+ /**
3
+ * Read the session selected by the host's scoped-session adapter.
4
+ *
5
+ * Newer hosts deliberately removed `sessions.list.current`: selection is UI
6
+ * scope state, not catalog state. The adapter is authoritative on both old
7
+ * and new hosts and tracks main-view retain/release transitions.
8
+ */
9
+ export declare function currentSessionId(ctx: Context): string | undefined;
10
+ /** React subscription to the same authoritative current-session source. */
11
+ export declare function useCurrentSessionId(ctx: Context): string | undefined;
@@ -289,7 +289,8 @@ export interface SidebarSessionPersistenceService {
289
289
  }
290
290
  /** The client session list snapshot the sidebar subscribes to. */
291
291
  export interface SidebarSessionList {
292
- current: string | undefined;
292
+ /** @deprecated Selection moved to `uiSession.adapter.current`; old hosts may still expose this field. */
293
+ current?: string;
293
294
  byId: Record<string, SidebarSessionSummary>;
294
295
  /** Direct durable catalogs keyed by their selected parent address. */
295
296
  subagentsByParent?: Readonly<Record<string, SidebarSubagentCatalog>>;
@@ -356,6 +357,17 @@ export interface SidebarSessionsService {
356
357
  */
357
358
  refreshSubagents?(parentSessionId: string): Promise<void>;
358
359
  }
360
+ /** The host's authoritative current Session scope binding. */
361
+ export interface SidebarUiSessionService {
362
+ adapter: {
363
+ current: {
364
+ getSnapshot(): {
365
+ readonly key: string | undefined;
366
+ };
367
+ subscribe(fn: () => void): () => void;
368
+ };
369
+ };
370
+ }
359
371
  /**
360
372
  * The client locale service face (mirror of @deepseek-ai/dsh-client-locale's
361
373
  * LocaleRuntime — only the slices the sidebar touches). The sidebar follows
@@ -475,6 +487,8 @@ export interface SidebarContextShape {
475
487
  webServer: SidebarWebServer;
476
488
  /** The session store (host `.get`) and the client list feed (`.list`) faces. */
477
489
  sessions: SidebarSessionStore & SidebarSessionsService;
490
+ /** Current UI Session scope; selection no longer belongs to the Session list catalog. */
491
+ uiSession: SidebarUiSessionService;
478
492
  /** The web runtime trust list (bind-derived). */
479
493
  webRuntime: SidebarWebRuntime;
480
494
  /** The client slot registry (register/inject). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatcode/cco-better-sidebar",
3
- "version": "0.18.0-alpha.1",
3
+ "version": "0.18.0-alpha.2",
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": {
@@ -10,6 +10,7 @@
10
10
  import { useEffect, useRef, useState } from 'react'
11
11
  import { api } from './api.ts'
12
12
  import { useChatcodeAccount } from './useChatcodeAccount.ts'
13
+ import { redirectToChatcodeLogin } from './chatcode-login-route.ts'
13
14
  import { t } from './locales.ts'
14
15
  import css from './sidebar.module.css'
15
16
 
@@ -93,6 +94,10 @@ export function AccountMenu(): JSX.Element {
93
94
  return () => { document.removeEventListener('pointerdown', onPointerDown) }
94
95
  }, [])
95
96
 
97
+ useEffect(() => {
98
+ if (error === 'status') redirectToChatcodeLogin()
99
+ }, [error])
100
+
96
101
  // The host has no slot for replacing this one closed-shell control. Keep
97
102
  // the original fully recoverable: its inline style is restored on disposal
98
103
  // and it remains the target invoked by our Settings menu item.
@@ -127,7 +132,8 @@ export function AccountMenu(): JSX.Element {
127
132
  const signedIn = chatcode?.validation === 'valid' && chatcode.expired !== true
128
133
  // A saved credential remains usable for the next check when the company
129
134
  // network is unavailable. Do not turn that transient state into a login CTA.
130
- const canStartChatcodeLogin = chatcode?.available === true && !signedIn
135
+ const hasChatcodeAccount = chatcode?.available === true && chatcode.configured === true
136
+ const canStartChatcodeLogin = chatcode?.available === true && !hasChatcodeAccount
131
137
  && !(chatcode.configured === true && chatcode.validation === 'unavailable')
132
138
 
133
139
  return (
@@ -144,15 +150,15 @@ export function AccountMenu(): JSX.Element {
144
150
  <SettingsIcon />
145
151
  {t('accountMenuSettings')}
146
152
  </button>
147
- {error && <div role="alert" className={css.accountItem}>{t(error === 'status' ? 'accountMenuChatcodeStatusError' : 'accountMenuChatcodeError')}</div>}
153
+ {error === 'operation' && <div role="alert" className={css.accountItem}>{t('accountMenuChatcodeError')}</div>}
148
154
  {(chatcode?.login?.state === 'failed' || chatcode?.login?.state === 'timed-out') && <div role="alert" className={css.accountItem}>{t('accountMenuChatcodeRetry')}</div>}
149
155
  {loginUrl && <a data-dsh-external="true" className={css.accountItem} href={loginUrl} target="_blank" rel="noopener noreferrer" onClick={event => { event.stopPropagation() }}>{t('accountMenuChatcodeOpen')}</a>}
150
156
  {chatcode?.available === true && <>
151
157
  {canStartChatcodeLogin && <button type="button" role="menuitem" className={css.accountItem} disabled={busy} onClick={startLogin}>
152
158
  <LogoutIcon />{busy ? t('accountMenuChatcodeStarting') : t('accountMenuChatcodeLogin')}
153
159
  </button>}
154
- {chatcode.configured && <button type="button" role="menuitem" className={css.accountItem} disabled={busy} onClick={logoutChatcode}>
155
- <LogoutIcon />{t('accountMenuChatcodeLogout')}
160
+ {hasChatcodeAccount && <button type="button" role="menuitem" className={css.accountItem} disabled={busy} onClick={logoutChatcode}>
161
+ <LogoutIcon />{t('accountMenuLogout')}
156
162
  </button>}
157
163
  </>}
158
164
  {chatcode?.available === false && <button type="button" role="menuitem" className={css.accountItem} onClick={logout}>
@@ -2,6 +2,7 @@
2
2
  import { useEffect, useRef } from 'react'
3
3
  import type { ChatCodeAuthState } from './api.ts'
4
4
  import { useChatcodeAccount } from './useChatcodeAccount.ts'
5
+ import { redirectToChatcodeLogin } from './chatcode-login-route.ts'
5
6
 
6
7
  type GateDecision = 'allow' | 'login' | 'wait'
7
8
 
@@ -28,8 +29,7 @@ export function ChatcodeGate(): JSX.Element | null {
28
29
  // this visit has passed that check, a later company-network outage does
29
30
  // not interrupt ongoing work.
30
31
  if (chatcodeGateDecision(chatcode, verifiedThisVisit.current) !== 'login') return
31
- const back = `${window.location.pathname}${window.location.search}${window.location.hash}`
32
- window.location.replace(`/sidebar/login?return=${encodeURIComponent(back)}`)
32
+ redirectToChatcodeLogin()
33
33
  }, [chatcode])
34
34
  return null
35
35
  }
@@ -64,6 +64,7 @@ import { OrphanedTab } from './OrphanedTab.tsx'
64
64
  import { RenderBoundary } from './RenderBoundary.tsx'
65
65
  import { tabContentCompare, type TabContentMemoKey } from './tab-content-memo.ts'
66
66
  import { t } from './locales.ts'
67
+ import { useCurrentSessionId } from './current-session.ts'
67
68
  import { api, type SessionScope } from './api.ts'
68
69
  import { AccountMenu } from './AccountMenu.tsx'
69
70
  import css from './sidebar.module.css'
@@ -322,7 +323,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
322
323
  useMemo(() => (callback: () => void) => ctx.sessions.list.subscribe(callback), [ctx]),
323
324
  useCallback(() => ctx.sessions.list.getSnapshot(), [ctx]),
324
325
  )
325
- const current = sessionList.current
326
+ const current = useCurrentSessionId(ctx)
326
327
 
327
328
  // Per-session sidebar state.
328
329
  const snapshot = useSyncExternalStore(
@@ -1385,6 +1386,7 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
1385
1386
  // focus is the only way the existing Tooltip can explain what is missing.
1386
1387
  return (
1387
1388
  <div data-dsh-panel-host {...osFileDragShield}>
1389
+ <AccountMenu />
1388
1390
  {nativeToolbarHost !== null ? createPortal(
1389
1391
  <div className={clsx(css.toggleCluster, css.toggleClusterNative)} data-dsh-toggle-cluster data-dsh-native-toolbar-cluster>
1390
1392
  {!narrow && (
@@ -0,0 +1,11 @@
1
+ /** Build the local ChatCode login gate URL while preserving the current page. */
2
+ export function chatcodeLoginRoute(location: Pick<Location, 'pathname' | 'search' | 'hash'>): string {
3
+ const back = `${location.pathname}${location.search}${location.hash}`
4
+ return `/sidebar/login?return=${encodeURIComponent(back)}`
5
+ }
6
+
7
+ /** Enter the existing local login flow after account state cannot be read. */
8
+ export function redirectToChatcodeLogin(location: Location = window.location): void {
9
+ if (location.pathname === '/sidebar/login') return
10
+ location.replace(chatcodeLoginRoute(location))
11
+ }
@@ -0,0 +1,22 @@
1
+ import { useCallback, useSyncExternalStore } from 'react'
2
+ import type { Context } from '../context-types.ts'
3
+
4
+ /**
5
+ * Read the session selected by the host's scoped-session adapter.
6
+ *
7
+ * Newer hosts deliberately removed `sessions.list.current`: selection is UI
8
+ * scope state, not catalog state. The adapter is authoritative on both old
9
+ * and new hosts and tracks main-view retain/release transitions.
10
+ */
11
+ export function currentSessionId(ctx: Context): string | undefined {
12
+ return ctx.uiSession.adapter.current.getSnapshot().key
13
+ }
14
+
15
+ /** React subscription to the same authoritative current-session source. */
16
+ export function useCurrentSessionId(ctx: Context): string | undefined {
17
+ const source = ctx.uiSession.adapter.current
18
+ return useSyncExternalStore(
19
+ useCallback(listener => source.subscribe(listener), [source]),
20
+ useCallback(() => source.getSnapshot().key, [source]),
21
+ )
22
+ }
@@ -43,7 +43,7 @@ import { apply as applyUiTweaksClient } from './ui-tweaks/index.tsx'
43
43
  * locale service backs the sidebar's copy — see locales.ts). `modules`
44
44
  * (rc.8+) is the client module system the chunk loader resolves its
45
45
  * externals through — Cordis guards service access without inject. */
46
- export const inject = ['slots', 'sessions', 'workspaces', 'locale', 'modules', 'commandUi', 'conversation']
46
+ export const inject = ['slots', 'sessions', 'uiSession', 'workspaces', 'locale', 'modules', 'commandUi', 'conversation']
47
47
 
48
48
  /**
49
49
  * Error boundary over the sidebar tree (root scope): a render error in the
@@ -11,6 +11,7 @@ import { firstLeaf, revealPaths, togglePanel, type SidebarStore } from './state.
11
11
  import { t } from './locales.ts'
12
12
  import { resolveSidebarPath, selectProducedFiles } from './produced-files.ts'
13
13
  import { wrapOpenPath } from './openpath-intercept.ts'
14
+ import { currentSessionId } from './current-session.ts'
14
15
  import css from './sidebar.module.css'
15
16
 
16
17
  /** Open a file in the sidebar's editor (used by the intercepted row and the explorer). */
@@ -190,7 +191,7 @@ export function registerOpenPathInterception(ctx: Context, store: SidebarStore):
190
191
  takeoverEnabled: () => !store.getSuspended()
191
192
  && store.getPrefs().interceptOpenPath !== false
192
193
  && store.getPrefs().tabsEnabled['editor'] !== false,
193
- currentSessionId: () => ctx.sessions.list.getSnapshot().current,
194
+ currentSessionId: () => currentSessionId(ctx),
194
195
  openInSidebar: (path, sessionId) => { openSidebarFile(ctx, store, sessionId, path) },
195
196
  revealInExplorer: (_path, sessionId) => { revealInExplorer(ctx, store, sessionId, lastProduced) },
196
197
  })
@@ -16,6 +16,7 @@ import { parsePrefs } from './prefs.ts'
16
16
  import { t } from './locales.ts'
17
17
  import { IconPinOutline16 } from './icons.tsx'
18
18
  import css from './sidebar.module.css'
19
+ import { useCurrentSessionId } from './current-session.ts'
19
20
 
20
21
  const PIN_SETTINGS_KEY = 'session-pins'
21
22
 
@@ -221,6 +222,7 @@ function PinnedSessions({ ctx, store, onChange }: {
221
222
  onChange: (ids: readonly string[]) => void
222
223
  }) {
223
224
  const list = useSyncExternalStore(ctx.sessions.list.subscribe, ctx.sessions.list.getSnapshot, ctx.sessions.list.getSnapshot)
225
+ const currentSessionId = useCurrentSessionId(ctx)
224
226
  const prefs = useSyncExternalStore(store.subscribe.bind(store), store.getSnapshot.bind(store), store.getSnapshot.bind(store)).prefs
225
227
  const ids = pinnedSessionIdsOf(prefs)
226
228
  const pinKey = ids.join('\0')
@@ -290,7 +292,7 @@ function PinnedSessions({ ctx, store, onChange }: {
290
292
  <button
291
293
  type="button"
292
294
  className={css.pinnedSessionOpen}
293
- aria-current={session.id === list.current ? 'page' : undefined}
295
+ aria-current={session.id === currentSessionId ? 'page' : undefined}
294
296
  onClick={() => { ctx.sessions.open?.(session.id) }}
295
297
  >
296
298
  <span className={css.pinnedSessionDot} data-running={session.running || undefined} aria-hidden="true" />
@@ -306,7 +306,8 @@ export interface SidebarSessionPersistenceService {
306
306
 
307
307
  /** The client session list snapshot the sidebar subscribes to. */
308
308
  export interface SidebarSessionList {
309
- current: string | undefined
309
+ /** @deprecated Selection moved to `uiSession.adapter.current`; old hosts may still expose this field. */
310
+ current?: string
310
311
  byId: Record<string, SidebarSessionSummary>
311
312
  /** Direct durable catalogs keyed by their selected parent address. */
312
313
  subagentsByParent?: Readonly<Record<string, SidebarSubagentCatalog>>
@@ -371,6 +372,16 @@ export interface SidebarSessionsService {
371
372
  refreshSubagents?(parentSessionId: string): Promise<void>
372
373
  }
373
374
 
375
+ /** The host's authoritative current Session scope binding. */
376
+ export interface SidebarUiSessionService {
377
+ adapter: {
378
+ current: {
379
+ getSnapshot(): { readonly key: string | undefined }
380
+ subscribe(fn: () => void): () => void
381
+ }
382
+ }
383
+ }
384
+
374
385
  /**
375
386
  * The client locale service face (mirror of @deepseek-ai/dsh-client-locale's
376
387
  * LocaleRuntime — only the slices the sidebar touches). The sidebar follows
@@ -494,6 +505,8 @@ export interface SidebarContextShape {
494
505
  webServer: SidebarWebServer
495
506
  /** The session store (host `.get`) and the client list feed (`.list`) faces. */
496
507
  sessions: SidebarSessionStore & SidebarSessionsService
508
+ /** Current UI Session scope; selection no longer belongs to the Session list catalog. */
509
+ uiSession: SidebarUiSessionService
497
510
  /** The web runtime trust list (bind-derived). */
498
511
  webRuntime: SidebarWebRuntime
499
512
  /** The client slot registry (register/inject). */