From 5c6fe20d3f223fccdebeb781df6f363901666e72 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Feb 2026 18:10:06 +0700 Subject: [PATCH 01/11] fix(accounts): improve shared-context editing and discoverability --- README.md | 8 + docs/dashboard-auth-cli.md | 8 +- docs/session-sharing-technical-analysis.md | 70 +++++++++ src/auth/auth-commands.ts | 3 + src/commands/help-command.ts | 1 + src/web-server/routes/account-routes.ts | 111 +++++++++++++- .../web-server/account-routes-context.test.ts | 89 +++++++++++ ui/src/components/account/accounts-table.tsx | 26 +++- .../account/create-auth-profile-dialog.tsx | 7 +- .../account/edit-account-context-dialog.tsx | 139 ++++++++++++++++++ ui/src/components/account/index.ts | 1 + ui/src/hooks/use-accounts.ts | 29 +++- ui/src/lib/api-client.ts | 10 ++ ui/src/pages/accounts.tsx | 16 +- 14 files changed, 501 insertions(+), 17 deletions(-) create mode 100644 docs/session-sharing-technical-analysis.md create mode 100644 ui/src/components/account/edit-account-context-dialog.tsx diff --git a/README.md b/README.md index 6a91a87a..8b4588e8 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,12 @@ ccs auth create backup --share-context ccs auth create backup2 --context-group sprint-a ``` +Update existing accounts without recreating login: + +1. Run `ccs config` +2. Open `Accounts` +3. Click the pencil icon in Actions and set `isolated` or `shared` mode + Shared mode metadata in `~/.ccs/config.yaml`: ```yaml @@ -300,6 +306,8 @@ accounts: Shared context links project workspace data only. Credentials remain isolated per account. +Technical details: [`docs/session-sharing-technical-analysis.md`](docs/session-sharing-technical-analysis.md) +
## Maintenance diff --git a/docs/dashboard-auth-cli.md b/docs/dashboard-auth-cli.md index 85d1a7ed..b810656f 100644 --- a/docs/dashboard-auth-cli.md +++ b/docs/dashboard-auth-cli.md @@ -1,6 +1,6 @@ # Dashboard Authentication CLI -Last Updated: 2026-02-24 +Last Updated: 2026-02-26 CLI commands for managing CCS dashboard authentication. @@ -39,6 +39,12 @@ Account context is isolation-first: - normalizes valid shared `context_group` before save - rejects `context_group` when mode is not `shared` +Dashboard accounts context editing: + +- `PUT /api/accounts/:name/context` updates context mode/group for existing auth accounts +- rejects CLIProxy OAuth account keys for this route +- applies normalization/validation rules above + ## Commands ### `ccs config auth setup` diff --git a/docs/session-sharing-technical-analysis.md b/docs/session-sharing-technical-analysis.md new file mode 100644 index 00000000..93c8b466 --- /dev/null +++ b/docs/session-sharing-technical-analysis.md @@ -0,0 +1,70 @@ +# Session Sharing Technical Analysis + +Last Updated: 2026-02-26 + +## Summary + +CCS supports practical cross-account continuity by sharing workspace context files between selected accounts, while keeping credentials isolated per account. + +This is implemented as a context policy per account: + +- `isolated` (default): account keeps its own workspace context +- `shared`: account workspace context is linked to a shared context group + +## Why This Is Safe Enough + +CCS only shares workspace context paths (project/session context files). It does **not** merge or copy authentication credentials between accounts. + +Credential storage remains per account instance. + +## Implementation Model + +Account metadata is stored in `~/.ccs/config.yaml`: + +```yaml +accounts: + work: + created: "2026-02-24T00:00:00.000Z" + last_used: null + context_mode: "shared" + context_group: "team-alpha" +``` + +Rules: + +- `context_mode` must be `isolated` or `shared` +- `context_group` is required when `context_mode=shared` +- group normalization: trim, lowercase, internal spaces -> `-` +- group must start with a letter and only include `[a-zA-Z0-9_-]` +- max length: `64` + +## User Workflows + +### New account with shared context + +```bash +ccs auth create work2 --share-context +ccs auth create backup --context-group sprint-a +``` + +### Existing account + +- Open `ccs config` +- Go to `Accounts` +- Click the pencil icon (`Edit Context`) +- Choose `isolated` or `shared` and set group + +No account recreation required for this workflow. + +## Current Limitations + +- Shared context is local filesystem sharing. It does not bypass remote provider permission models. +- Session continuity still depends on what the upstream tool/provider stores and allows. +- Context sharing should only be enabled for accounts you intentionally trust to share workspace history. + +## Validation Checklist + +- Confirm account row shows `shared ()` in Dashboard Accounts table +- Switch between accounts in the same group and verify workspace continuity +- Run `ccs doctor` if symlink/context health looks inconsistent + diff --git a/src/auth/auth-commands.ts b/src/auth/auth-commands.ts index 2ed849ee..c94a91c5 100644 --- a/src/auth/auth-commands.ts +++ b/src/auth/auth-commands.ts @@ -128,6 +128,9 @@ class AuthCommands { console.log( ` Account profiles stay isolated unless you opt in with ${color('--share-context', 'command')}.` ); + console.log( + ` Existing profiles: open ${color('ccs config', 'command')} -> Accounts -> Edit Context.` + ); console.log(` Shared context groups are normalized (trim + lowercase) and spaces become "-".`); console.log( ` ${color('context_group', 'path')} must be non-empty and <= ${MAX_CONTEXT_GROUP_LENGTH} chars in shared mode.` diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 1db7717c..461ea979 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -151,6 +151,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); [ ['ccs auth --help', 'Show account management commands'], ['ccs auth create ', 'Create account profile (supports context sharing flags)'], + ['ccs config', 'Dashboard: Accounts table can edit context mode/group'], ['ccs auth list', 'List all account profiles'], ['ccs auth default ', 'Set default profile'], ['ccs auth reset-default', 'Restore original CCS default'], diff --git a/src/web-server/routes/account-routes.ts b/src/web-server/routes/account-routes.ts index b6ebc359..81d263b6 100644 --- a/src/web-server/routes/account-routes.ts +++ b/src/web-server/routes/account-routes.ts @@ -19,7 +19,11 @@ import { soloAccount, } from '../../cliproxy/account-manager'; import { isCLIProxyProvider } from '../../cliproxy/provider-capabilities'; -import { resolveAccountContextPolicy } from '../../auth/account-context'; +import { + isValidContextGroupName, + normalizeContextGroupName, + resolveAccountContextPolicy, +} from '../../auth/account-context'; import { buildCliproxyAccountKey, parseCliproxyKey, @@ -149,6 +153,111 @@ router.post('/default', (req: Request, res: Response): void => { } }); +/** + * PUT /api/accounts/:name/context - Update account context mode/group + */ +router.put('/:name/context', async (req: Request, res: Response): Promise => { + try { + const { name } = req.params; + + if (!name) { + res.status(400).json({ error: 'Missing account name' }); + return; + } + + // CLIProxy OAuth accounts do not support local account context metadata. + const cliproxyKey = !hasAuthAccount(name) ? parseCliproxyKey(name) : null; + if (cliproxyKey) { + res + .status(400) + .json({ error: `Context mode is not supported for CLIProxy account: ${name}` }); + return; + } + + const existsUnified = isUnifiedMode() && registry.hasAccountUnified(name); + const existsLegacy = registry.hasProfile(name); + if (!existsUnified && !existsLegacy) { + res.status(404).json({ error: `Account not found: ${name}` }); + return; + } + + const mode = req.body?.context_mode; + const rawGroup = req.body?.context_group; + + if (mode !== 'isolated' && mode !== 'shared') { + res.status(400).json({ error: 'Missing or invalid context_mode: expected isolated|shared' }); + return; + } + + if (mode !== 'shared' && rawGroup !== undefined) { + res + .status(400) + .json({ error: 'Invalid payload: context_group requires context_mode=shared' }); + return; + } + + let normalizedGroup: string | undefined; + if (mode === 'shared') { + if (typeof rawGroup !== 'string' || rawGroup.trim().length === 0) { + res + .status(400) + .json({ error: 'Invalid payload: shared context_mode requires non-empty context_group' }); + return; + } + + normalizedGroup = normalizeContextGroupName(rawGroup); + if (!isValidContextGroupName(normalizedGroup)) { + res.status(400).json({ + error: + 'Invalid context_group. Use letters/numbers/dash/underscore, start with a letter, max 64 chars.', + }); + return; + } + } + + const metadata = + mode === 'shared' + ? { + context_mode: 'shared' as const, + context_group: normalizedGroup, + } + : { + context_mode: 'isolated' as const, + }; + const policy = resolveAccountContextPolicy(metadata); + + const previousUnified = existsUnified ? registry.getAllAccountsUnified()[name] : undefined; + const previousLegacy = existsLegacy ? registry.getProfile(name) : undefined; + + try { + if (existsUnified) { + registry.updateAccountUnified(name, metadata); + } + if (existsLegacy) { + registry.updateProfile(name, metadata); + } + + await instanceMgr.ensureInstance(name, policy); + } catch (error) { + if (existsUnified && previousUnified) { + registry.updateAccountUnified(name, previousUnified); + } + if (existsLegacy && previousLegacy) { + registry.updateProfile(name, previousLegacy); + } + throw error; + } + + res.json({ + name, + context_mode: policy.mode, + context_group: policy.group ?? null, + }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); + /** * DELETE /api/accounts/reset-default - Reset to CCS default */ diff --git a/tests/unit/web-server/account-routes-context.test.ts b/tests/unit/web-server/account-routes-context.test.ts index c3b27999..2456ff37 100644 --- a/tests/unit/web-server/account-routes-context.test.ts +++ b/tests/unit/web-server/account-routes-context.test.ts @@ -18,6 +18,14 @@ async function deletePath(baseUrl: string, routePath: string): Promise return fetch(`${baseUrl}${routePath}`, { method: 'DELETE' }); } +async function putJson(baseUrl: string, routePath: string, body: unknown): Promise { + return fetch(`${baseUrl}${routePath}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + describe('web-server account-routes context normalization', () => { let server: Server; let baseUrl = ''; @@ -27,6 +35,7 @@ describe('web-server account-routes context normalization', () => { beforeAll(async () => { const app = express(); + app.use(express.json()); app.use('/api/accounts', accountRoutes); await new Promise((resolve, reject) => { @@ -173,4 +182,84 @@ describe('web-server account-routes context normalization', () => { InstanceManager.prototype.deleteInstance = originalDeleteInstance; } }); + + it('updates existing account context metadata and normalizes shared group', async () => { + const ccsDir = path.join(tempHome, '.ccs'); + fs.mkdirSync(ccsDir, { recursive: true }); + + fs.writeFileSync( + path.join(ccsDir, 'config.yaml'), + [ + 'version: 8', + 'accounts:', + ' work:', + ' created: "2026-02-01T00:00:00.000Z"', + ' last_used: null', + ' context_mode: isolated', + 'profiles: {}', + 'cliproxy:', + ' oauth_accounts: {}', + ' providers: {}', + ' variants: {}', + ].join('\n'), + 'utf8' + ); + + const response = await putJson(baseUrl, '/api/accounts/work/context', { + context_mode: 'shared', + context_group: ' Team Alpha ', + }); + expect(response.status).toBe(200); + const payload = (await response.json()) as { context_mode: string; context_group: string | null }; + expect(payload.context_mode).toBe('shared'); + expect(payload.context_group).toBe('team-alpha'); + + const accountsPayload = await getJson<{ + accounts: Array<{ name: string; context_mode?: string; context_group?: string }>; + }>(baseUrl, '/api/accounts'); + const work = accountsPayload.accounts.find((account) => account.name === 'work'); + expect(work?.context_mode).toBe('shared'); + expect(work?.context_group).toBe('team-alpha'); + }); + + it('rejects shared mode updates without context_group', async () => { + const ccsDir = path.join(tempHome, '.ccs'); + fs.mkdirSync(ccsDir, { recursive: true }); + + fs.writeFileSync( + path.join(ccsDir, 'config.yaml'), + [ + 'version: 8', + 'accounts:', + ' work:', + ' created: "2026-02-01T00:00:00.000Z"', + ' last_used: null', + 'profiles: {}', + 'cliproxy:', + ' oauth_accounts: {}', + ' providers: {}', + ' variants: {}', + ].join('\n'), + 'utf8' + ); + + const response = await putJson(baseUrl, '/api/accounts/work/context', { + context_mode: 'shared', + }); + expect(response.status).toBe(400); + + const payload = (await response.json()) as { error: string }; + expect(payload.error).toContain('context_group'); + }); + + it('rejects context updates for CLIProxy account identifiers', async () => { + const response = await putJson(baseUrl, '/api/accounts/gemini:test/context', { + context_mode: 'shared', + context_group: 'default', + }); + expect(response.status).toBe(400); + + const payload = (await response.json()) as { error: string }; + expect(payload.error).toContain('CLIProxy'); + }); }); diff --git a/ui/src/components/account/accounts-table.tsx b/ui/src/components/account/accounts-table.tsx index baaddc25..69467192 100644 --- a/ui/src/components/account/accounts-table.tsx +++ b/ui/src/components/account/accounts-table.tsx @@ -24,7 +24,8 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; -import { Check, Trash2, RotateCcw } from 'lucide-react'; +import { Check, Pencil, Trash2, RotateCcw } from 'lucide-react'; +import { EditAccountContextDialog } from '@/components/account/edit-account-context-dialog'; import { useSetDefaultAccount, useDeleteAccount, @@ -35,7 +36,6 @@ import type { Account } from '@/lib/api-client'; interface AccountsTableProps { data: Account[]; defaultAccount: string | null; - onRefresh?: () => void; } export function AccountsTable({ data, defaultAccount }: AccountsTableProps) { @@ -43,6 +43,7 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) { const deleteMutation = useDeleteAccount(); const resetDefaultMutation = useResetDefaultAccount(); const [deleteTarget, setDeleteTarget] = useState(null); + const [contextTarget, setContextTarget] = useState(null); const columns: ColumnDef[] = [ { @@ -108,13 +109,26 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) { { id: 'actions', header: 'Actions', - size: 180, + size: 220, cell: ({ row }) => { const isDefault = row.original.name === defaultAccount; const isPending = setDefaultMutation.isPending || deleteMutation.isPending; + const isCliproxy = row.original.type === 'cliproxy'; return (
+ {!isCliproxy && ( + + )} + + + + + ); +} diff --git a/ui/src/components/account/index.ts b/ui/src/components/account/index.ts index 0b75ccb8..fe6da6ea 100644 --- a/ui/src/components/account/index.ts +++ b/ui/src/components/account/index.ts @@ -6,6 +6,7 @@ export { AccountsTable } from './accounts-table'; export { AddAccountDialog } from './add-account-dialog'; export { CreateAuthProfileDialog } from './create-auth-profile-dialog'; +export { EditAccountContextDialog } from './edit-account-context-dialog'; // Flow visualization (from subdirectory) export { AccountFlowViz } from './flow-viz'; diff --git a/ui/src/hooks/use-accounts.ts b/ui/src/hooks/use-accounts.ts index b46691af..1e616022 100644 --- a/ui/src/hooks/use-accounts.ts +++ b/ui/src/hooks/use-accounts.ts @@ -1,5 +1,5 @@ /** - * React Query hooks for accounts (profiles.json) + * React Query hooks for account management * Dashboard parity: Full CRUD operations for auth profiles */ @@ -58,3 +58,30 @@ export function useDeleteAccount() { }, }); } + +export function useUpdateAccountContext() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + name, + context_mode, + context_group, + }: { + name: string; + context_mode: 'isolated' | 'shared'; + context_group?: string; + }) => api.accounts.updateContext(name, { context_mode, context_group }), + onSuccess: (_data, vars) => { + queryClient.invalidateQueries({ queryKey: ['accounts'] }); + const contextSummary = + vars.context_mode === 'shared' + ? `shared (${(vars.context_group || 'default').trim().toLowerCase().replace(/\s+/g, '-')})` + : 'isolated'; + toast.success(`Updated "${vars.name}" context to ${contextSummary}`); + }, + onError: (error: Error) => { + toast.error(error.message); + }, + }); +} diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index 12ec590e..bab3455e 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -457,6 +457,11 @@ export interface Account { context_group?: string; } +export interface UpdateAccountContext { + context_mode: 'isolated' | 'shared'; + context_group?: string; +} + // Unified config types export interface ConfigFormat { format: 'yaml' | 'json' | 'none'; @@ -785,6 +790,11 @@ export const api = { }), resetDefault: () => request('/accounts/reset-default', { method: 'DELETE' }), delete: (name: string) => request(`/accounts/${name}`, { method: 'DELETE' }), + updateContext: (name: string, data: UpdateAccountContext) => + request(`/accounts/${encodeURIComponent(name)}/context`, { + method: 'PUT', + body: JSON.stringify(data), + }), }, // Unified config API config: { diff --git a/ui/src/pages/accounts.tsx b/ui/src/pages/accounts.tsx index bbed9c89..9a47b1df 100644 --- a/ui/src/pages/accounts.tsx +++ b/ui/src/pages/accounts.tsx @@ -11,7 +11,7 @@ import { Button } from '@/components/ui/button'; import { useAccounts } from '@/hooks/use-accounts'; export function AccountsPage() { - const { data, isLoading, refetch } = useAccounts(); + const { data, isLoading } = useAccounts(); const [createDialogOpen, setCreateDialogOpen] = useState(false); return ( @@ -20,7 +20,7 @@ export function AccountsPage() {

Accounts

- Manage multi-account Claude sessions (profiles.json) + Manage multi-account Claude sessions and shared context groups

+ {cliproxyCount > 0 && ( + + )} +
- - {isLoading ? ( -
Loading accounts...
- ) : ( -
-

- New profile login: Create Account. - Existing profile context: use the pencil icon in the table. -

- -
+
+ + + Total Auth Accounts + + + {authAccounts.length} + + + + + + + + Shared (Linked) + + + + {sharedCount} + + + + + + + + Isolated (Separate) + + + + {isolatedCount} + + + +
+ + {cliproxyCount > 0 && ( + + + OAuth accounts are managed elsewhere + + This screen hides {cliproxyCount} CLIProxy OAuth account + {cliproxyCount > 1 ? 's' : ''}. Use CLIProxy Plus to manage Gemini, + Codex, Antigravity, and other OAuth providers. + + )} + {legacyContextCount > 0 && ( + + + Legacy accounts need context review + + {legacyContextCount} account{legacyContextCount > 1 ? 's were' : ' was'} onboarded + before context controls and currently default to isolated mode. Use the pencil action in + the table to explicitly choose isolated or shared. + + + )} + + + + CCS Auth Accounts + + New onboarding: Create Account. + Existing accounts: use the pencil action to control linkage flexibility per account. + + + + {isLoading ? ( +
Loading accounts...
+ ) : ( + + )} +
+
+ setCreateDialogOpen(false)} /> ); From b6475baab3380328da9293905572d2b1274d6dfa Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Feb 2026 19:01:13 +0700 Subject: [PATCH 03/11] feat(accounts): add advanced deeper continuity mode and claude pool discoverability --- README.md | 17 +- docs/codebase-summary.md | 5 +- docs/dashboard-auth-cli.md | 11 +- docs/session-sharing-technical-analysis.md | 27 ++- src/auth/account-context.ts | 44 ++++- src/auth/auth-commands.ts | 11 ++ src/auth/commands/create-command.ts | 12 +- src/auth/commands/list-command.ts | 4 +- src/auth/commands/show-command.ts | 1 + src/auth/commands/types.ts | 4 + src/auth/profile-detector.ts | 1 + src/auth/profile-registry.ts | 11 ++ src/commands/help-command.ts | 8 +- src/config/migration-manager.ts | 4 + src/config/unified-config-types.ts | 2 + src/management/instance-manager.ts | 1 + src/management/shared-manager.ts | 165 ++++++++++++++++++ src/types/config.ts | 2 + .../routes/account-route-helpers.ts | 2 + src/web-server/routes/account-routes.ts | 39 +++++ src/web-server/routes/config-routes.ts | 24 ++- tests/unit/account-context.test.ts | 31 ++++ tests/unit/auth-command-args.test.ts | 8 + tests/unit/auth-list-context.test.ts | 16 +- ...ile-registry-context-normalization.test.ts | 2 + tests/unit/config/migration-manager.test.ts | 4 + tests/unit/shared-context-policy.test.ts | 70 ++++++++ .../web-server/account-routes-context.test.ts | 52 +++++- .../config-routes-account-context.test.ts | 43 +++++ ui/src/components/account/accounts-table.tsx | 16 +- .../account/create-auth-profile-dialog.tsx | 27 ++- .../account/edit-account-context-dialog.tsx | 48 ++++- ui/src/hooks/use-accounts.ts | 24 ++- ui/src/lib/api-client.ts | 3 + ui/src/pages/accounts.tsx | 109 +++++++++--- 35 files changed, 782 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 8b4588e8..80c78858 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,11 @@ Account profiles are isolated by default. | `isolated` | Yes | No `context_group` required | | `shared` | No (explicit opt-in) | Valid non-empty `context_group` | +Shared mode continuity depth: + +- `standard` (default): shares project workspace context only +- `deeper` (advanced opt-in): additionally syncs `session-env`, `file-history`, `shell-snapshots`, `todos` + Opt in to shared context when needed: ```bash @@ -277,13 +282,16 @@ ccs auth create backup --share-context # Share context only within named group ccs auth create backup2 --context-group sprint-a + +# Advanced deeper continuity mode (requires shared mode) +ccs auth create backup3 --context-group sprint-a --deeper-continuity ``` Update existing accounts without recreating login: 1. Run `ccs config` 2. Open `Accounts` -3. Click the pencil icon in Actions and set `isolated` or `shared` mode +3. Click the pencil icon in Actions and set `isolated` or `shared` mode + continuity depth Shared mode metadata in `~/.ccs/config.yaml`: @@ -294,6 +302,7 @@ accounts: last_used: null context_mode: "shared" context_group: "team-alpha" + continuity_mode: "standard" ``` `context_group` rules: @@ -304,7 +313,11 @@ accounts: - non-empty after normalization - normalized by trim + lowercase + whitespace collapse (`" Team Alpha "` -> `"team-alpha"`) -Shared context links project workspace data only. Credentials remain isolated per account. +Shared context with `standard` depth links project workspace data. `deeper` depth links additional continuity artifacts. Credentials remain isolated per account. + +Alternative path for lower manual switching: + +- Use CLIProxy Claude pool (`ccs cliproxy auth claude`) and manage pool behavior in `ccs config` -> `CLIProxy Plus`. Technical details: [`docs/session-sharing-technical-analysis.md`](docs/session-sharing-technical-analysis.md) diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md index d138f34b..ae78f67a 100644 --- a/docs/codebase-summary.md +++ b/docs/codebase-summary.md @@ -203,15 +203,16 @@ src/ ### Account Context Metadata Flow -- Source fields: `accounts..context_mode` and `accounts..context_group` in `~/.ccs/config.yaml`. +- Source fields: `accounts..context_mode`, `accounts..context_group`, `accounts..continuity_mode` in `~/.ccs/config.yaml`. - Runtime policy resolver: `src/auth/account-context.ts`. - Metadata storage normalization: `src/auth/profile-registry.ts`. - API write validation: `PUT /api/config` in `src/web-server/routes/config-routes.ts`. - Rules: - mode is isolation-first (`isolated` default, `shared` opt-in) - shared mode requires non-empty valid `context_group` + - shared mode continuity depth is `standard` by default, optional `deeper` - `context_group` is normalized (trim + lowercase + whitespace collapse to `-`) - - API route rejects `context_group` when mode is not `shared` + - API route rejects `context_group`/`continuity_mode` when mode is not `shared` - registry normalization drops malformed persisted `context_group` values ### Target Adapter Module diff --git a/docs/dashboard-auth-cli.md b/docs/dashboard-auth-cli.md index b810656f..1449e0ec 100644 --- a/docs/dashboard-auth-cli.md +++ b/docs/dashboard-auth-cli.md @@ -24,6 +24,11 @@ Account context is isolation-first: | `isolated` | Yes | No `context_group` required | | `shared` | No (opt-in) | Valid non-empty `context_group` | +Shared continuity depth: + +- `standard` (default): shares project workspace context only +- `deeper` (advanced opt-in): also syncs `session-env`, `file-history`, `shell-snapshots`, `todos` + `context_group` normalization and validation: - trim + lowercase + collapse internal whitespace to `-` @@ -31,17 +36,21 @@ Account context is isolation-first: - must start with a letter - max length: 64 - shared mode requires non-empty value after normalization +- `continuity_mode` is only valid when mode is `shared` `PUT /api/config` behavior for account context: - rejects invalid unified payloads - rejects explicit `context_mode: shared` with invalid/empty `context_group` +- rejects invalid `continuity_mode` values - normalizes valid shared `context_group` before save +- defaults missing shared `continuity_mode` to `standard` - rejects `context_group` when mode is not `shared` +- rejects `continuity_mode` when mode is not `shared` Dashboard accounts context editing: -- `PUT /api/accounts/:name/context` updates context mode/group for existing auth accounts +- `PUT /api/accounts/:name/context` updates context mode/group/continuity for existing auth accounts - rejects CLIProxy OAuth account keys for this route - applies normalization/validation rules above diff --git a/docs/session-sharing-technical-analysis.md b/docs/session-sharing-technical-analysis.md index 93c8b466..9e4b9a68 100644 --- a/docs/session-sharing-technical-analysis.md +++ b/docs/session-sharing-technical-analysis.md @@ -9,7 +9,8 @@ CCS supports practical cross-account continuity by sharing workspace context fil This is implemented as a context policy per account: - `isolated` (default): account keeps its own workspace context -- `shared`: account workspace context is linked to a shared context group +- `shared` + `standard` (default): account workspace context is linked to a shared context group +- `shared` + `deeper` (advanced opt-in): account also shares continuity artifacts ## Why This Is Safe Enough @@ -28,16 +29,27 @@ accounts: last_used: null context_mode: "shared" context_group: "team-alpha" + continuity_mode: "deeper" ``` Rules: - `context_mode` must be `isolated` or `shared` - `context_group` is required when `context_mode=shared` +- `continuity_mode` is valid only when `context_mode=shared` (`standard` or `deeper`) - group normalization: trim, lowercase, internal spaces -> `-` - group must start with a letter and only include `[a-zA-Z0-9_-]` - max length: `64` +Deeper continuity links these directories per context group: + +- `session-env` +- `file-history` +- `shell-snapshots` +- `todos` + +`.anthropic` and account credentials remain isolated. + ## User Workflows ### New account with shared context @@ -45,14 +57,15 @@ Rules: ```bash ccs auth create work2 --share-context ccs auth create backup --context-group sprint-a +ccs auth create backup2 --context-group sprint-a --deeper-continuity ``` ### Existing account - Open `ccs config` - Go to `Accounts` -- Click the pencil icon (`Edit Context`) -- Choose `isolated` or `shared` and set group +- Click the pencil icon (`Edit History Sync`) +- Choose `isolated` or `shared`, set group, and (optionally) choose deeper continuity No account recreation required for this workflow. @@ -62,9 +75,15 @@ No account recreation required for this workflow. - Session continuity still depends on what the upstream tool/provider stores and allows. - Context sharing should only be enabled for accounts you intentionally trust to share workspace history. +## Alternative: CLIProxy Claude Pool + +For users who prefer lower manual account switching, use CLIProxy Claude pool instead: + +- Authenticate pool accounts via `ccs cliproxy auth claude` +- Manage account pool behavior in `ccs config` -> `CLIProxy Plus` + ## Validation Checklist - Confirm account row shows `shared ()` in Dashboard Accounts table - Switch between accounts in the same group and verify workspace continuity - Run `ccs doctor` if symlink/context health looks inconsistent - diff --git a/src/auth/account-context.ts b/src/auth/account-context.ts index c30a0f7c..a8c28b50 100644 --- a/src/auth/account-context.ts +++ b/src/auth/account-context.ts @@ -6,20 +6,24 @@ */ export type AccountContextMode = 'isolated' | 'shared'; +export type AccountContinuityMode = 'standard' | 'deeper'; export interface AccountContextMetadata { context_mode?: AccountContextMode; context_group?: string; + continuity_mode?: AccountContinuityMode; } export interface AccountContextPolicy { mode: AccountContextMode; group?: string; + continuityMode?: AccountContinuityMode; } export interface CreateAccountContextInput { shareContext: boolean; contextGroup?: string; + deeperContinuity?: boolean; } export interface ResolvedCreateAccountContext { @@ -29,6 +33,7 @@ export interface ResolvedCreateAccountContext { export const DEFAULT_ACCOUNT_CONTEXT_MODE: AccountContextMode = 'isolated'; export const DEFAULT_ACCOUNT_CONTEXT_GROUP = 'default'; +export const DEFAULT_ACCOUNT_CONTINUITY_MODE: AccountContinuityMode = 'standard'; export const MAX_CONTEXT_GROUP_LENGTH = 64; export const ACCOUNT_PROFILE_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/; @@ -66,11 +71,22 @@ export function isAccountContextMetadata(value: unknown): value is AccountContex const candidate = value as Record; const mode = candidate['context_mode']; const group = candidate['context_group']; + const continuity = candidate['continuity_mode']; const modeValid = mode === undefined || mode === 'isolated' || mode === 'shared'; const groupValid = group === undefined || typeof group === 'string'; + const continuityValid = + continuity === undefined || continuity === 'standard' || continuity === 'deeper'; - return modeValid && groupValid; + if (!modeValid || !groupValid || !continuityValid) { + return false; + } + + if (mode !== 'shared' && continuity !== undefined) { + return false; + } + + return true; } /** @@ -80,6 +96,15 @@ export function resolveCreateAccountContext( input: CreateAccountContextInput ): ResolvedCreateAccountContext { const hasGroupFlag = input.contextGroup !== undefined; + const continuityMode: AccountContinuityMode = input.deeperContinuity ? 'deeper' : 'standard'; + + if (input.deeperContinuity && !input.shareContext && !hasGroupFlag) { + return { + policy: { mode: 'isolated' }, + error: + 'Advanced deeper continuity requires shared context (--share-context or --context-group).', + }; + } if (hasGroupFlag) { if (!input.contextGroup || input.contextGroup.trim().length === 0) { @@ -101,6 +126,7 @@ export function resolveCreateAccountContext( policy: { mode: 'shared', group: normalizedGroup, + continuityMode, }, }; } @@ -110,6 +136,7 @@ export function resolveCreateAccountContext( policy: { mode: 'shared', group: DEFAULT_ACCOUNT_CONTEXT_GROUP, + continuityMode, }, }; } @@ -128,15 +155,21 @@ export function resolveAccountContextPolicy( const mode: AccountContextMode = metadata?.context_mode === 'shared' ? 'shared' : 'isolated'; if (mode === 'shared') { + const continuityMode: AccountContinuityMode = + metadata?.continuity_mode === 'deeper' ? 'deeper' : 'standard'; const rawGroup = metadata?.context_group; if (rawGroup && rawGroup.trim().length > 0) { const normalized = normalizeContextGroupName(rawGroup); if (isValidContextGroupName(normalized)) { - return { mode: 'shared', group: normalized }; + return { mode: 'shared', group: normalized, continuityMode }; } } - return { mode: 'shared', group: DEFAULT_ACCOUNT_CONTEXT_GROUP }; + return { + mode: 'shared', + group: DEFAULT_ACCOUNT_CONTEXT_GROUP, + continuityMode, + }; } return { mode: 'isolated' }; @@ -152,6 +185,8 @@ export function policyToAccountContextMetadata( return { context_mode: 'shared', context_group: policy.group || DEFAULT_ACCOUNT_CONTEXT_GROUP, + continuity_mode: + policy.continuityMode === 'deeper' ? 'deeper' : DEFAULT_ACCOUNT_CONTINUITY_MODE, }; } @@ -165,7 +200,8 @@ export function policyToAccountContextMetadata( */ export function formatAccountContextPolicy(policy: AccountContextPolicy): string { if (policy.mode === 'shared') { - return `shared (${policy.group || DEFAULT_ACCOUNT_CONTEXT_GROUP})`; + const continuity = policy.continuityMode === 'deeper' ? 'deeper continuity' : 'standard'; + return `shared (${policy.group || DEFAULT_ACCOUNT_CONTEXT_GROUP}, ${continuity})`; } return 'isolated'; diff --git a/src/auth/auth-commands.ts b/src/auth/auth-commands.ts index c94a91c5..86a3c710 100644 --- a/src/auth/auth-commands.ts +++ b/src/auth/auth-commands.ts @@ -86,6 +86,11 @@ class AuthCommands { console.log(` ${dim('# Share context only within a specific group')}`); console.log(` ${color('ccs auth create backup --context-group sprint-a', 'command')}`); console.log(''); + console.log(` ${dim('# Advanced: deeper shared continuity for session history artifacts')}`); + console.log( + ` ${color('ccs auth create backup --context-group sprint-a --deeper-continuity', 'command')}` + ); + console.log(''); console.log(` ${dim('# Set work as default')}`); console.log(` ${color('ccs auth default work', 'command')}`); console.log(''); @@ -108,6 +113,9 @@ class AuthCommands { console.log( ` ${color('--context-group ', 'command')} Share context only within a named group` ); + console.log( + ` ${color('--deeper-continuity', 'command')} Advanced shared mode: sync additional continuity artifacts` + ); console.log( ` ${color('--yes, -y', 'command')} Skip confirmation prompts (remove)` ); @@ -128,6 +136,9 @@ class AuthCommands { console.log( ` Account profiles stay isolated unless you opt in with ${color('--share-context', 'command')}.` ); + console.log( + ` ${color('--deeper-continuity', 'command')} requires shared mode and syncs session-env/file-history/todos/shell-snapshots.` + ); console.log( ` Existing profiles: open ${color('ccs config', 'command')} -> Accounts -> Edit Context.` ); diff --git a/src/auth/commands/create-command.ts b/src/auth/commands/create-command.ts index 8c169c1d..93550a2e 100644 --- a/src/auth/commands/create-command.ts +++ b/src/auth/commands/create-command.ts @@ -31,14 +31,15 @@ function sanitizeProfileNameForInstance(name: string): string { */ export async function handleCreate(ctx: CommandContext, args: string[]): Promise { await initUI(); - const { profileName, force, shareContext, contextGroup, unknownFlags } = parseArgs(args); + const { profileName, force, shareContext, contextGroup, deeperContinuity, unknownFlags } = + parseArgs(args); if (unknownFlags && unknownFlags.length > 0) { const unknownList = unknownFlags.map((flag) => `"${flag}"`).join(', '); console.log(fail(`Unknown option(s): ${unknownList}`)); console.log(''); console.log( - `Usage: ${color('ccs auth create [--force] [--share-context] [--context-group ]', 'command')}` + `Usage: ${color('ccs auth create [--force] [--share-context] [--context-group ] [--deeper-continuity]', 'command')}` ); console.log(`Help: ${color('ccs auth --help', 'command')}`); console.log(''); @@ -49,7 +50,7 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise console.log(fail('Profile name is required')); console.log(''); console.log( - `Usage: ${color('ccs auth create [--force] [--share-context] [--context-group ]', 'command')}` + `Usage: ${color('ccs auth create [--force] [--share-context] [--context-group ] [--deeper-continuity]', 'command')}` ); console.log(''); console.log('Example:'); @@ -89,6 +90,7 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise const resolvedContext = resolveCreateAccountContext({ shareContext: !!shareContext, contextGroup, + deeperContinuity: !!deeperContinuity, }); if (resolvedContext.error) { @@ -212,7 +214,9 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise console.log(''); const launchDescription = contextPolicy.mode === 'shared' - ? `Starting Claude with shared context group "${contextPolicy.group || 'default'}"...` + ? contextPolicy.continuityMode === 'deeper' + ? `Starting Claude with shared context group "${contextPolicy.group || 'default'}" (deeper continuity)...` + : `Starting Claude with shared context group "${contextPolicy.group || 'default'}"...` : 'Starting Claude in isolated instance...'; console.log(warn(launchDescription)); console.log(warn('You will be prompted to login with your account.')); diff --git a/src/auth/commands/list-command.ts b/src/auth/commands/list-command.ts index a248c2ee..f25ab770 100644 --- a/src/auth/commands/list-command.ts +++ b/src/auth/commands/list-command.ts @@ -32,6 +32,7 @@ export async function handleList(ctx: CommandContext, args: string[]): Promise // Last usage time * context_mode?: 'isolated' | 'shared' // Workspace context policy * context_group?: // Shared context group when mode=shared + * continuity_mode?: 'standard' | 'deeper' // Shared continuity depth * } * * Removed fields from v2.x: @@ -43,6 +44,7 @@ interface CreateMetadata { last_used?: string | null; context_mode?: 'isolated' | 'shared'; context_group?: string; + continuity_mode?: 'standard' | 'deeper'; } export class ProfileRegistry { @@ -70,6 +72,7 @@ export class ProfileRegistry { if (normalized.context_mode !== 'shared') { delete normalized.context_group; + delete normalized.continuity_mode; } else { const normalizedGroup = this.normalizeContextGroupValue(normalized.context_group); if (normalizedGroup) { @@ -77,6 +80,8 @@ export class ProfileRegistry { } else { delete normalized.context_group; } + + normalized.continuity_mode = normalized.continuity_mode === 'deeper' ? 'deeper' : 'standard'; } return normalized; @@ -87,6 +92,7 @@ export class ProfileRegistry { if (normalized.context_mode !== 'shared') { delete normalized.context_group; + delete normalized.continuity_mode; } else { const normalizedGroup = this.normalizeContextGroupValue(normalized.context_group); if (normalizedGroup) { @@ -94,6 +100,8 @@ export class ProfileRegistry { } else { delete normalized.context_group; } + + normalized.continuity_mode = normalized.continuity_mode === 'deeper' ? 'deeper' : 'standard'; } return normalized; @@ -164,6 +172,7 @@ export class ProfileRegistry { last_used: metadata.last_used || null, context_mode: metadata.context_mode, context_group: metadata.context_group, + continuity_mode: metadata.continuity_mode, }); // Note: No longer auto-set as default @@ -311,6 +320,7 @@ export class ProfileRegistry { last_used: null, context_mode: metadata.context_mode, context_group: metadata.context_group, + continuity_mode: metadata.continuity_mode, }); saveUnifiedConfig(config); } @@ -438,6 +448,7 @@ export class ProfileRegistry { last_used: account.last_used, context_mode: account.context_mode, context_group: account.context_group, + continuity_mode: account.continuity_mode, }; } diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 461ea979..9c8561b5 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -150,11 +150,15 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['Run multiple Claude accounts concurrently'], [ ['ccs auth --help', 'Show account management commands'], - ['ccs auth create ', 'Create account profile (supports context sharing flags)'], - ['ccs config', 'Dashboard: Accounts table can edit context mode/group'], + [ + 'ccs auth create ', + 'Create account profile (supports shared groups + --deeper-continuity)', + ], + ['ccs config', 'Dashboard: Accounts table can edit context mode/group/continuity depth'], ['ccs auth list', 'List all account profiles'], ['ccs auth default ', 'Set default profile'], ['ccs auth reset-default', 'Restore original CCS default'], + ['ccs cliproxy auth claude', 'Alternative: authenticate Claude account pool via CLIProxy'], ] ); diff --git a/src/config/migration-manager.ts b/src/config/migration-manager.ts index cf9628d2..38659b74 100644 --- a/src/config/migration-manager.ts +++ b/src/config/migration-manager.ts @@ -151,7 +151,10 @@ export async function migrate(dryRun = false): Promise { const metadata = meta as Record; const rawContextMode = metadata.context_mode; const rawContextGroup = metadata.context_group; + const rawContinuityMode = metadata.continuity_mode; const contextMode = rawContextMode === 'shared' ? 'shared' : 'isolated'; + const continuityMode = + contextMode === 'shared' && rawContinuityMode === 'deeper' ? 'deeper' : 'standard'; let contextGroup: string | undefined; if (typeof rawContextGroup === 'string' && rawContextGroup.trim().length > 0) { const normalizedGroup = normalizeContextGroupName(rawContextGroup); @@ -168,6 +171,7 @@ export async function migrate(dryRun = false): Promise { last_used: (metadata.last_used as string) || null, context_mode: contextMode, context_group: contextMode === 'shared' ? contextGroup : undefined, + continuity_mode: contextMode === 'shared' ? continuityMode : undefined, }; unifiedConfig.accounts[name] = account; } diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index f668cf17..287cba72 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -44,6 +44,8 @@ export interface AccountConfig { context_mode?: 'isolated' | 'shared'; /** Context-sharing group when context_mode='shared' */ context_group?: string; + /** Shared continuity depth when context_mode='shared' */ + continuity_mode?: 'standard' | 'deeper'; } /** diff --git a/src/management/instance-manager.ts b/src/management/instance-manager.ts index c0142f06..8f49cf74 100644 --- a/src/management/instance-manager.ts +++ b/src/management/instance-manager.ts @@ -48,6 +48,7 @@ class InstanceManager { // Apply context policy (isolated by default, optional shared group). await this.sharedManager.syncProjectContext(instancePath, contextPolicy); + await this.sharedManager.syncAdvancedContinuityArtifacts(instancePath, contextPolicy); }); return instancePath; diff --git a/src/management/shared-manager.ts b/src/management/shared-manager.ts index 4fc0e394..9a010edd 100644 --- a/src/management/shared-manager.ts +++ b/src/management/shared-manager.ts @@ -27,6 +27,12 @@ class SharedManager { private readonly claudeDir: string; private readonly instancesDir: string; private readonly sharedItems: SharedItem[]; + private readonly advancedContinuityItems: readonly string[] = [ + 'session-env', + 'file-history', + 'shell-snapshots', + 'todos', + ]; constructor() { this.homeDir = os.homedir(); @@ -314,6 +320,133 @@ class SharedManager { await this.ensureDirectory(projectsPath); } + /** + * Sync advanced continuity artifacts for shared deeper mode. + * + * - shared + deeper: artifacts are linked per context group. + * - shared + standard / isolated: artifacts stay local to instance. + */ + async syncAdvancedContinuityArtifacts( + instancePath: string, + policy: AccountContextPolicy + ): Promise { + const instanceName = path.basename(instancePath); + const useSharedContinuity = policy.mode === 'shared' && policy.continuityMode === 'deeper'; + const contextGroup = policy.group || DEFAULT_ACCOUNT_CONTEXT_GROUP; + + for (const artifactName of this.advancedContinuityItems) { + const instanceArtifactPath = path.join(instancePath, artifactName); + + if (useSharedContinuity) { + const sharedArtifactPath = path.join( + this.sharedDir, + 'context-groups', + contextGroup, + 'continuity', + artifactName + ); + + await this.ensureDirectory(sharedArtifactPath); + await this.ensureDirectory(path.dirname(instanceArtifactPath)); + + const currentStats = await this.getLstat(instanceArtifactPath); + if (!currentStats) { + await this.linkDirectoryWithFallback(sharedArtifactPath, instanceArtifactPath); + continue; + } + + if (currentStats.isSymbolicLink()) { + if (await this.isSymlinkTarget(instanceArtifactPath, sharedArtifactPath)) { + continue; + } + + const currentTarget = await this.resolveSymlinkTargetPath(instanceArtifactPath); + if ( + currentTarget && + path.resolve(currentTarget) !== path.resolve(sharedArtifactPath) && + this.isSafeContinuityMergeSource(currentTarget, instanceName, artifactName) && + (await this.pathExists(currentTarget)) + ) { + await this.mergeDirectoryWithConflictCopies( + currentTarget, + sharedArtifactPath, + instanceName + ); + } else if ( + currentTarget && + !this.isSafeContinuityMergeSource(currentTarget, instanceName, artifactName) + ) { + console.log( + warn( + `Skipping unsafe ${artifactName} merge source outside CCS roots: ${currentTarget}` + ) + ); + } + + await fs.promises.unlink(instanceArtifactPath); + await this.linkDirectoryWithFallback(sharedArtifactPath, instanceArtifactPath); + continue; + } + + if (currentStats.isDirectory()) { + await this.mergeDirectoryWithConflictCopies( + instanceArtifactPath, + sharedArtifactPath, + instanceName + ); + await fs.promises.rm(instanceArtifactPath, { recursive: true, force: true }); + await this.linkDirectoryWithFallback(sharedArtifactPath, instanceArtifactPath); + continue; + } + + await fs.promises.rm(instanceArtifactPath, { force: true }); + await this.linkDirectoryWithFallback(sharedArtifactPath, instanceArtifactPath); + continue; + } + + const currentStats = await this.getLstat(instanceArtifactPath); + if (!currentStats) { + await this.ensureDirectory(instanceArtifactPath); + continue; + } + + if (currentStats.isDirectory()) { + continue; + } + + if (currentStats.isSymbolicLink()) { + const currentTarget = await this.resolveSymlinkTargetPath(instanceArtifactPath); + await fs.promises.unlink(instanceArtifactPath); + await this.ensureDirectory(instanceArtifactPath); + + if ( + currentTarget && + path.resolve(currentTarget) !== path.resolve(instanceArtifactPath) && + this.isSafeContinuityMergeSource(currentTarget, instanceName, artifactName) && + (await this.pathExists(currentTarget)) + ) { + await this.mergeDirectoryWithConflictCopies( + currentTarget, + instanceArtifactPath, + instanceName + ); + } else if ( + currentTarget && + !this.isSafeContinuityMergeSource(currentTarget, instanceName, artifactName) + ) { + console.log( + warn(`Skipping unsafe ${artifactName} merge source outside CCS roots: ${currentTarget}`) + ); + } + + continue; + } + + await fs.promises.rm(instanceArtifactPath, { force: true }); + await this.ensureDirectory(instanceArtifactPath); + } + } + /** * Ensure all project memory directories for an instance are shared. * @@ -712,6 +845,38 @@ class SharedManager { ); } + /** + * Guard advanced continuity merge operations to known CCS-managed roots only. + */ + private isSafeContinuityMergeSource( + sourcePath: string, + instanceName: string, + artifactName: string + ): boolean { + const resolvedSource = this.resolveCanonicalPath(sourcePath); + const sharedContextRoot = this.resolveCanonicalPath( + path.join(this.sharedDir, 'context-groups') + ); + const instanceArtifactRoot = this.resolveCanonicalPath( + path.join(this.instancesDir, instanceName, artifactName) + ); + + const normalizedSource = + process.platform === 'win32' ? resolvedSource.toLowerCase() : resolvedSource; + const continuitySegment = + process.platform === 'win32' + ? `${path.sep}continuity${path.sep}`.toLowerCase() + : `${path.sep}continuity${path.sep}`; + + const withinSharedContinuity = + this.isPathWithinDirectory(resolvedSource, sharedContextRoot) && + normalizedSource.includes(continuitySegment); + + return ( + withinSharedContinuity || this.isPathWithinDirectory(resolvedSource, instanceArtifactRoot) + ); + } + /** * Link directory with Windows fallback to recursive copy. */ diff --git a/src/types/config.ts b/src/types/config.ts index c385eef9..bd3787ae 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -96,6 +96,8 @@ export interface ProfileMetadata { context_mode?: 'isolated' | 'shared'; /** Context-sharing group when context_mode='shared' */ context_group?: string; + /** Shared continuity depth when context_mode='shared' */ + continuity_mode?: 'standard' | 'deeper'; } export interface ProfilesRegistry { diff --git a/src/web-server/routes/account-route-helpers.ts b/src/web-server/routes/account-route-helpers.ts index d8c2e9cc..bbf05469 100644 --- a/src/web-server/routes/account-route-helpers.ts +++ b/src/web-server/routes/account-route-helpers.ts @@ -7,7 +7,9 @@ export interface MergedAccountEntry { last_used: string | null; context_mode?: 'isolated' | 'shared'; context_group?: string; + continuity_mode?: 'standard' | 'deeper'; context_inferred?: boolean; + continuity_inferred?: boolean; provider?: string; displayName?: string; } diff --git a/src/web-server/routes/account-routes.ts b/src/web-server/routes/account-routes.ts index fdf1610a..d96009d4 100644 --- a/src/web-server/routes/account-routes.ts +++ b/src/web-server/routes/account-routes.ts @@ -20,6 +20,7 @@ import { } from '../../cliproxy/account-manager'; import { isCLIProxyProvider } from '../../cliproxy/provider-capabilities'; import { + DEFAULT_ACCOUNT_CONTINUITY_MODE, isValidContextGroupName, normalizeContextGroupName, resolveAccountContextPolicy, @@ -58,13 +59,18 @@ router.get('/', (_req: Request, res: Response): void => { const contextPolicy = resolveAccountContextPolicy(meta); const hasExplicitContextMode = meta.context_mode === 'isolated' || meta.context_mode === 'shared'; + const hasExplicitContinuityMode = + meta.continuity_mode === 'standard' || meta.continuity_mode === 'deeper'; merged[name] = { type: meta.type || 'account', created: meta.created, last_used: meta.last_used || null, context_mode: contextPolicy.mode, context_group: contextPolicy.group, + continuity_mode: contextPolicy.mode === 'shared' ? contextPolicy.continuityMode : undefined, context_inferred: !hasExplicitContextMode, + continuity_inferred: + contextPolicy.mode === 'shared' ? !hasExplicitContinuityMode : undefined, }; } @@ -73,13 +79,18 @@ router.get('/', (_req: Request, res: Response): void => { const contextPolicy = resolveAccountContextPolicy(account); const hasExplicitContextMode = account.context_mode === 'isolated' || account.context_mode === 'shared'; + const hasExplicitContinuityMode = + account.continuity_mode === 'standard' || account.continuity_mode === 'deeper'; merged[name] = { type: 'account', created: account.created, last_used: account.last_used, context_mode: contextPolicy.mode, context_group: contextPolicy.group, + continuity_mode: contextPolicy.mode === 'shared' ? contextPolicy.continuityMode : undefined, context_inferred: !hasExplicitContextMode, + continuity_inferred: + contextPolicy.mode === 'shared' ? !hasExplicitContinuityMode : undefined, }; } @@ -189,6 +200,7 @@ router.put('/:name/context', async (req: Request, res: Response): Promise const mode = req.body?.context_mode; const rawGroup = req.body?.context_group; + const rawContinuityMode = req.body?.continuity_mode; if (mode !== 'isolated' && mode !== 'shared') { res.status(400).json({ error: 'Missing or invalid context_mode: expected isolated|shared' }); @@ -202,7 +214,15 @@ router.put('/:name/context', async (req: Request, res: Response): Promise return; } + if (mode !== 'shared' && rawContinuityMode !== undefined) { + res + .status(400) + .json({ error: 'Invalid payload: continuity_mode requires context_mode=shared' }); + return; + } + let normalizedGroup: string | undefined; + let continuityMode: 'standard' | 'deeper' | undefined; if (mode === 'shared') { if (typeof rawGroup !== 'string' || rawGroup.trim().length === 0) { res @@ -219,6 +239,19 @@ router.put('/:name/context', async (req: Request, res: Response): Promise }); return; } + + if ( + rawContinuityMode !== undefined && + rawContinuityMode !== 'standard' && + rawContinuityMode !== 'deeper' + ) { + res.status(400).json({ + error: 'Invalid continuity_mode: expected standard|deeper', + }); + return; + } + + continuityMode = rawContinuityMode === 'deeper' ? 'deeper' : DEFAULT_ACCOUNT_CONTINUITY_MODE; } const metadata = @@ -226,6 +259,7 @@ router.put('/:name/context', async (req: Request, res: Response): Promise ? { context_mode: 'shared' as const, context_group: normalizedGroup, + continuity_mode: continuityMode, } : { context_mode: 'isolated' as const, @@ -258,7 +292,12 @@ router.put('/:name/context', async (req: Request, res: Response): Promise name, context_mode: policy.mode, context_group: policy.group ?? null, + continuity_mode: + policy.mode === 'shared' + ? (policy.continuityMode ?? DEFAULT_ACCOUNT_CONTINUITY_MODE) + : null, context_inferred: false, + continuity_inferred: false, }); } catch (error) { res.status(500).json({ error: (error as Error).message }); diff --git a/src/web-server/routes/config-routes.ts b/src/web-server/routes/config-routes.ts index 31a25cc7..7b3ed8fd 100644 --- a/src/web-server/routes/config-routes.ts +++ b/src/web-server/routes/config-routes.ts @@ -18,7 +18,11 @@ import { getBackupDirectories, } from '../../config/migration-manager'; import { isUnifiedConfig } from '../../config/unified-config-types'; -import { isValidContextGroupName, normalizeContextGroupName } from '../../auth/account-context'; +import { + DEFAULT_ACCOUNT_CONTINUITY_MODE, + isValidContextGroupName, + normalizeContextGroupName, +} from '../../auth/account-context'; const router = Router(); @@ -45,6 +49,7 @@ function validateAndNormalizeAccountContextMetadata(config: unknown): string | n const account = accountValue as Record; const mode = account.context_mode; const group = account.context_group; + const continuity = account.continuity_mode; if (mode !== undefined && mode !== 'isolated' && mode !== 'shared') { return `Invalid config.accounts.${accountName}.context_mode: expected isolated|shared`; @@ -54,10 +59,18 @@ function validateAndNormalizeAccountContextMetadata(config: unknown): string | n return `Invalid config.accounts.${accountName}.context_group: expected string`; } + if (continuity !== undefined && continuity !== 'standard' && continuity !== 'deeper') { + return `Invalid config.accounts.${accountName}.continuity_mode: expected standard|deeper`; + } + if (mode !== 'shared' && group !== undefined) { return `Invalid config.accounts.${accountName}: context_group requires context_mode=shared`; } + if (mode !== 'shared' && continuity !== undefined) { + return `Invalid config.accounts.${accountName}: continuity_mode requires context_mode=shared`; + } + if (mode === 'shared' && typeof group === 'string' && group.trim().length > 0) { const normalizedGroup = normalizeContextGroupName(group); if (!isValidContextGroupName(normalizedGroup)) { @@ -66,6 +79,11 @@ function validateAndNormalizeAccountContextMetadata(config: unknown): string | n account.context_group = normalizedGroup; } + if (mode === 'shared') { + account.continuity_mode = + continuity === 'deeper' ? 'deeper' : DEFAULT_ACCOUNT_CONTINUITY_MODE; + } + if (mode === 'shared' && typeof group === 'string' && group.trim().length === 0) { return `Invalid config.accounts.${accountName}.context_group: shared mode requires a non-empty value`; } @@ -73,6 +91,10 @@ function validateAndNormalizeAccountContextMetadata(config: unknown): string | n if (mode === 'isolated' && group !== undefined) { delete account.context_group; } + + if (mode === 'isolated' && continuity !== undefined) { + delete account.continuity_mode; + } } return null; diff --git a/tests/unit/account-context.test.ts b/tests/unit/account-context.test.ts index 2c8f658f..ecb21c74 100644 --- a/tests/unit/account-context.test.ts +++ b/tests/unit/account-context.test.ts @@ -51,4 +51,35 @@ describe('account context helpers', () => { expect(result.policy.mode).toBe('shared'); expect(result.policy.group).toBe('team-alpha'); }); + + it('supports deeper continuity for shared create flows', () => { + const result = resolveCreateAccountContext({ + shareContext: true, + deeperContinuity: true, + }); + + expect(result.error).toBeUndefined(); + expect(result.policy.mode).toBe('shared'); + expect(result.policy.continuityMode).toBe('deeper'); + }); + + it('rejects deeper continuity without shared context flags', () => { + const result = resolveCreateAccountContext({ + shareContext: false, + deeperContinuity: true, + }); + + expect(result.error).toContain('requires shared context'); + }); + + it('defaults shared continuity mode to standard for legacy metadata', () => { + const resolved = resolveAccountContextPolicy({ + context_mode: 'shared', + context_group: 'team-alpha', + }); + + expect(resolved.mode).toBe('shared'); + expect(resolved.group).toBe('team-alpha'); + expect(resolved.continuityMode).toBe('standard'); + }); }); diff --git a/tests/unit/auth-command-args.test.ts b/tests/unit/auth-command-args.test.ts index e6ba81e7..8811f4d6 100644 --- a/tests/unit/auth-command-args.test.ts +++ b/tests/unit/auth-command-args.test.ts @@ -39,6 +39,14 @@ describe('auth command args parsing', () => { expect(parsed.contextGroup).toBe(''); }); + it('parses deeper continuity flag for create command', () => { + const parsed = parseArgs(['work', '--share-context', '--deeper-continuity']); + + expect(parsed.profileName).toBe('work'); + expect(parsed.shareContext).toBe(true); + expect(parsed.deeperContinuity).toBe(true); + }); + it('tracks unknown flags and keeps positional profile intact', () => { const parsed = parseArgs(['--foo', 'bar', 'work']); diff --git a/tests/unit/auth-list-context.test.ts b/tests/unit/auth-list-context.test.ts index b62cb798..adcb7ba8 100644 --- a/tests/unit/auth-list-context.test.ts +++ b/tests/unit/auth-list-context.test.ts @@ -77,13 +77,19 @@ describe('auth list context metadata', () => { } const payload = JSON.parse(lines.join('\n')) as { - profiles: Array<{ name: string; context_mode?: string; context_group?: string | null }>; + profiles: Array<{ + name: string; + context_mode?: string; + context_group?: string | null; + continuity_mode?: string | null; + }>; }; const work = payload.profiles.find((profile) => profile.name === 'work'); expect(work).toBeTruthy(); expect(work?.context_mode).toBe('shared'); expect(work?.context_group).toBe('sprint-a'); + expect(work?.continuity_mode).toBe('standard'); }); it('prefers unified context metadata over legacy when profile names overlap', async () => { @@ -151,12 +157,18 @@ describe('auth list context metadata', () => { } const payload = JSON.parse(lines.join('\n')) as { - profiles: Array<{ name: string; context_mode?: string; context_group?: string | null }>; + profiles: Array<{ + name: string; + context_mode?: string; + context_group?: string | null; + continuity_mode?: string | null; + }>; }; const work = payload.profiles.find((profile) => profile.name === 'work'); expect(work).toBeTruthy(); expect(work?.context_mode).toBe('shared'); expect(work?.context_group).toBe('sprint-a'); + expect(work?.continuity_mode).toBe('standard'); }); }); diff --git a/tests/unit/auth/profile-registry-context-normalization.test.ts b/tests/unit/auth/profile-registry-context-normalization.test.ts index 95882b35..8fb895aa 100644 --- a/tests/unit/auth/profile-registry-context-normalization.test.ts +++ b/tests/unit/auth/profile-registry-context-normalization.test.ts @@ -58,6 +58,7 @@ describe('profile-registry context normalization', () => { expect(profile.context_mode).toBe('shared'); expect(profile.context_group).toBeUndefined(); + expect(profile.continuity_mode).toBe('standard'); }); it('drops non-string unified context_group values without throwing', () => { @@ -88,5 +89,6 @@ describe('profile-registry context normalization', () => { expect(accounts.work.context_mode).toBe('shared'); expect(accounts.work.context_group).toBeUndefined(); + expect(accounts.work.continuity_mode).toBe('standard'); }); }); diff --git a/tests/unit/config/migration-manager.test.ts b/tests/unit/config/migration-manager.test.ts index 1e09fcaf..c05cdb51 100644 --- a/tests/unit/config/migration-manager.test.ts +++ b/tests/unit/config/migration-manager.test.ts @@ -166,8 +166,10 @@ describe('migration-manager legacy kimi compatibility', () => { expect(unified).toBeTruthy(); expect(unified?.accounts.work.context_mode).toBe('shared'); expect(unified?.accounts.work.context_group).toBe('sprint-a'); + expect(unified?.accounts.work.continuity_mode).toBe('standard'); expect(unified?.accounts.personal.context_mode).toBe('isolated'); expect(unified?.accounts.personal.context_group).toBeUndefined(); + expect(unified?.accounts.personal.continuity_mode).toBeUndefined(); }); it('normalizes valid legacy shared groups and drops invalid ones during migration', async () => { @@ -210,5 +212,7 @@ describe('migration-manager legacy kimi compatibility', () => { expect(unified?.accounts.work.context_group).toBe('sprint-a'); expect(unified?.accounts.broken.context_mode).toBe('shared'); expect(unified?.accounts.broken.context_group).toBeUndefined(); + expect(unified?.accounts.work.continuity_mode).toBe('standard'); + expect(unified?.accounts.broken.continuity_mode).toBe('standard'); }); }); diff --git a/tests/unit/shared-context-policy.test.ts b/tests/unit/shared-context-policy.test.ts index b882b5d5..7b724f14 100644 --- a/tests/unit/shared-context-policy.test.ts +++ b/tests/unit/shared-context-policy.test.ts @@ -64,6 +64,20 @@ describe('SharedManager context policy', () => { return { instancePath, ccsDir }; } + async function applyPolicyWithContinuity( + policy: AccountContextPolicy + ): Promise<{ instancePath: string; ccsDir: string }> { + const ccsDir = getTestCcsDir(); + const instancePath = path.join(ccsDir, 'instances', 'work'); + fs.mkdirSync(instancePath, { recursive: true }); + + const manager = new SharedManager(); + await manager.syncProjectContext(instancePath, policy); + await manager.syncAdvancedContinuityArtifacts(instancePath, policy); + + return { instancePath, ccsDir }; + } + it('keeps projects isolated by default', async () => { const { instancePath } = await applyPolicy({ mode: 'isolated' }); const projectsPath = path.join(instancePath, 'projects'); @@ -141,6 +155,62 @@ describe('SharedManager context policy', () => { expect(stats.isDirectory() || stats.isSymbolicLink()).toBe(true); }); + it('links advanced continuity artifacts for shared deeper mode', async () => { + const { instancePath, ccsDir } = await applyPolicyWithContinuity({ + mode: 'shared', + group: 'sprint-a', + continuityMode: 'deeper', + }); + + const artifactPath = path.join(instancePath, 'session-env'); + const targetFile = path.join( + ccsDir, + 'shared', + 'context-groups', + 'sprint-a', + 'continuity', + 'session-env', + 'session.json' + ); + + fs.mkdirSync(path.dirname(targetFile), { recursive: true }); + fs.writeFileSync(targetFile, '{"id":"shared"}', 'utf8'); + + expect(fs.lstatSync(artifactPath).isSymbolicLink()).toBe(true); + expect(fs.readFileSync(path.join(artifactPath, 'session.json'), 'utf8')).toContain('shared'); + }); + + it('detaches advanced continuity artifacts when moving from deeper to standard shared mode', async () => { + const { instancePath, ccsDir } = await applyPolicyWithContinuity({ + mode: 'shared', + group: 'sprint-a', + continuityMode: 'deeper', + }); + + const sharedTodo = path.join( + ccsDir, + 'shared', + 'context-groups', + 'sprint-a', + 'continuity', + 'todos', + 'todo.md' + ); + fs.mkdirSync(path.dirname(sharedTodo), { recursive: true }); + fs.writeFileSync(sharedTodo, '- shared todo', 'utf8'); + + const manager = new SharedManager(); + await manager.syncAdvancedContinuityArtifacts(instancePath, { + mode: 'shared', + group: 'sprint-a', + continuityMode: 'standard', + }); + + const localTodoDir = path.join(instancePath, 'todos'); + expect(fs.lstatSync(localTodoDir).isDirectory()).toBe(true); + expect(fs.readFileSync(path.join(localTodoDir, 'todo.md'), 'utf8')).toContain('shared todo'); + }); + it('skips merge when projects symlink target is outside canonical CCS roots', async () => { const ccsDir = getTestCcsDir(); const instancePath = path.join(ccsDir, 'instances', 'work'); diff --git a/tests/unit/web-server/account-routes-context.test.ts b/tests/unit/web-server/account-routes-context.test.ts index 4f9efb68..8e35607c 100644 --- a/tests/unit/web-server/account-routes-context.test.ts +++ b/tests/unit/web-server/account-routes-context.test.ts @@ -108,6 +108,7 @@ describe('web-server account-routes context normalization', () => { name: string; context_mode?: string; context_group?: string; + continuity_mode?: string; context_inferred?: boolean; }>; }>(baseUrl, '/api/accounts'); @@ -117,6 +118,7 @@ describe('web-server account-routes context normalization', () => { expect(work?.context_mode).toBe('isolated'); expect(work?.context_inferred).toBe(true); expect(work && 'context_group' in work).toBe(false); + expect(work && 'continuity_mode' in work).toBe(false); }); it('falls back shared accounts with invalid groups to default shared group', async () => { @@ -147,7 +149,9 @@ describe('web-server account-routes context normalization', () => { name: string; context_mode?: string; context_group?: string; + continuity_mode?: string; context_inferred?: boolean; + continuity_inferred?: boolean; }>; }>(baseUrl, '/api/accounts'); @@ -155,7 +159,9 @@ describe('web-server account-routes context normalization', () => { expect(work).toBeTruthy(); expect(work?.context_mode).toBe('shared'); expect(work?.context_group).toBe('default'); + expect(work?.continuity_mode).toBe('standard'); expect(work?.context_inferred).toBe(false); + expect(work?.continuity_inferred).toBe(true); }); it('does not delete metadata when instance deletion fails', async () => { @@ -220,23 +226,34 @@ describe('web-server account-routes context normalization', () => { const response = await putJson(baseUrl, '/api/accounts/work/context', { context_mode: 'shared', context_group: ' Team Alpha ', + continuity_mode: 'deeper', }); expect(response.status).toBe(200); const payload = (await response.json()) as { context_mode: string; context_group: string | null; + continuity_mode?: string | null; context_inferred?: boolean; + continuity_inferred?: boolean; }; expect(payload.context_mode).toBe('shared'); expect(payload.context_group).toBe('team-alpha'); + expect(payload.continuity_mode).toBe('deeper'); expect(payload.context_inferred).toBe(false); + expect(payload.continuity_inferred).toBe(false); const accountsPayload = await getJson<{ - accounts: Array<{ name: string; context_mode?: string; context_group?: string }>; + accounts: Array<{ + name: string; + context_mode?: string; + context_group?: string; + continuity_mode?: string; + }>; }>(baseUrl, '/api/accounts'); const work = accountsPayload.accounts.find((account) => account.name === 'work'); expect(work?.context_mode).toBe('shared'); expect(work?.context_group).toBe('team-alpha'); + expect(work?.continuity_mode).toBe('deeper'); }); it('rejects shared mode updates without context_group', async () => { @@ -273,10 +290,43 @@ describe('web-server account-routes context normalization', () => { const response = await putJson(baseUrl, '/api/accounts/gemini:test/context', { context_mode: 'shared', context_group: 'default', + continuity_mode: 'deeper', }); expect(response.status).toBe(400); const payload = (await response.json()) as { error: string }; expect(payload.error).toContain('CLIProxy'); }); + + it('rejects invalid continuity mode updates', async () => { + const ccsDir = path.join(tempHome, '.ccs'); + fs.mkdirSync(ccsDir, { recursive: true }); + + fs.writeFileSync( + path.join(ccsDir, 'config.yaml'), + [ + 'version: 8', + 'accounts:', + ' work:', + ' created: "2026-02-01T00:00:00.000Z"', + ' last_used: null', + 'profiles: {}', + 'cliproxy:', + ' oauth_accounts: {}', + ' providers: {}', + ' variants: {}', + ].join('\n'), + 'utf8' + ); + + const response = await putJson(baseUrl, '/api/accounts/work/context', { + context_mode: 'shared', + context_group: 'default', + continuity_mode: 'extreme', + }); + + expect(response.status).toBe(400); + const payload = (await response.json()) as { error: string }; + expect(payload.error).toContain('continuity_mode'); + }); }); diff --git a/tests/unit/web-server/config-routes-account-context.test.ts b/tests/unit/web-server/config-routes-account-context.test.ts index a1bcb728..f24328ff 100644 --- a/tests/unit/web-server/config-routes-account-context.test.ts +++ b/tests/unit/web-server/config-routes-account-context.test.ts @@ -115,6 +115,47 @@ describe('web-server config-routes account context validation', () => { expect(payload.error).toContain('context_group requires context_mode=shared'); }); + it('rejects continuity_mode when mode is not shared', async () => { + const response = await putJson(baseUrl, '/api/config', { + version: 8, + accounts: { + work: { + created: '2026-01-01T00:00:00.000Z', + last_used: null, + context_mode: 'isolated', + continuity_mode: 'deeper', + }, + }, + profiles: {}, + cliproxy: { oauth_accounts: {}, providers: [], variants: {} }, + }); + + expect(response.status).toBe(400); + const payload = (await response.json()) as { error: string }; + expect(payload.error).toContain('continuity_mode requires context_mode=shared'); + }); + + it('rejects invalid shared continuity_mode values', async () => { + const response = await putJson(baseUrl, '/api/config', { + version: 8, + accounts: { + work: { + created: '2026-01-01T00:00:00.000Z', + last_used: null, + context_mode: 'shared', + context_group: 'team-alpha', + continuity_mode: 'extreme', + }, + }, + profiles: {}, + cliproxy: { oauth_accounts: {}, providers: [], variants: {} }, + }); + + expect(response.status).toBe(400); + const payload = (await response.json()) as { error: string }; + expect(payload.error).toContain('continuity_mode'); + }); + it('rejects invalid shared context_group names', async () => { const response = await putJson(baseUrl, '/api/config', { version: 8, @@ -162,6 +203,7 @@ describe('web-server config-routes account context validation', () => { last_used: null, context_mode: 'shared', context_group: 'Sprint-A', + continuity_mode: 'deeper', }; const response = await putJson(baseUrl, '/api/config', config); @@ -171,6 +213,7 @@ describe('web-server config-routes account context validation', () => { const savedConfig = loadUnifiedConfig(); expect(savedConfig?.accounts.work.context_group).toBe('sprint-a'); + expect(savedConfig?.accounts.work.continuity_mode).toBe('deeper'); }); it('returns alreadyMigrated when migration is not needed', async () => { diff --git a/ui/src/components/account/accounts-table.tsx b/ui/src/components/account/accounts-table.tsx index 7c49ba5d..d1b7232c 100644 --- a/ui/src/components/account/accounts-table.tsx +++ b/ui/src/components/account/accounts-table.tsx @@ -90,7 +90,7 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) { }, { id: 'context', - header: 'Context', + header: 'History Sync', size: 170, cell: ({ row }) => { if (row.original.type === 'cliproxy') { @@ -100,7 +100,19 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) { const mode = row.original.context_mode || 'isolated'; if (mode === 'shared') { const group = row.original.context_group || 'default'; - return shared ({group}); + if (row.original.continuity_mode === 'deeper') { + return shared ({group}, deeper); + } + + if (row.original.continuity_inferred) { + return ( + + shared ({group}, standard legacy) + + ); + } + + return shared ({group}, standard); } if (row.original.context_inferred) { diff --git a/ui/src/components/account/create-auth-profile-dialog.tsx b/ui/src/components/account/create-auth-profile-dialog.tsx index 9a04b4cd..1c6cd3ec 100644 --- a/ui/src/components/account/create-auth-profile-dialog.tsx +++ b/ui/src/components/account/create-auth-profile-dialog.tsx @@ -28,6 +28,7 @@ export function CreateAuthProfileDialog({ open, onClose }: CreateAuthProfileDial const [profileName, setProfileName] = useState(''); const [shareContext, setShareContext] = useState(false); const [contextGroup, setContextGroup] = useState(''); + const [deeperContinuity, setDeeperContinuity] = useState(false); const [copied, setCopied] = useState(false); // Validate profile name: alphanumeric, dash, underscore only @@ -47,6 +48,7 @@ export function CreateAuthProfileDialog({ open, onClose }: CreateAuthProfileDial ? `--context-group ${normalizedGroup}` : '--share-context' : '', + shareContext && deeperContinuity ? '--deeper-continuity' : '', ] .filter(Boolean) .join(' ') @@ -63,6 +65,7 @@ export function CreateAuthProfileDialog({ open, onClose }: CreateAuthProfileDial setProfileName(''); setShareContext(false); setContextGroup(''); + setDeeperContinuity(false); setCopied(false); onClose(); }; @@ -74,7 +77,7 @@ export function CreateAuthProfileDialog({ open, onClose }: CreateAuthProfileDial Create New Account Auth profiles require Claude CLI login. Run the command below in your terminal. You can - edit context mode/group later from the Accounts table. + edit sync mode, group, and continuity depth later from the Accounts table. @@ -104,13 +107,13 @@ export function CreateAuthProfileDialog({ open, onClose }: CreateAuthProfileDial onCheckedChange={(checked) => setShareContext(checked === true)} /> {shareContext && (
- + Leave empty to use the default shared group. Spaces are normalized to dashes.

+
+ setDeeperContinuity(checked === true)} + /> + +
+

+ Adds sync for session-env, file-history,{' '} + shell-snapshots, and todos. Credentials stay isolated. +

{contextGroup.trim().length > 0 && !isValidContextGroup && (

Group must start with a letter and use only letters, numbers, dashes, or @@ -158,6 +175,10 @@ export function CreateAuthProfileDialog({ open, onClose }: CreateAuthProfileDial

  • Complete the Claude login in your browser
  • Return here and refresh to see the new account
  • +

    + Prefer pooled Claude OAuth routing instead? Use CLIProxy Claude pool from the Accounts + page action button. +

    diff --git a/ui/src/components/account/edit-account-context-dialog.tsx b/ui/src/components/account/edit-account-context-dialog.tsx index cd6230c2..4edf98ad 100644 --- a/ui/src/components/account/edit-account-context-dialog.tsx +++ b/ui/src/components/account/edit-account-context-dialog.tsx @@ -21,6 +21,7 @@ import type { Account } from '@/lib/api-client'; import { useUpdateAccountContext } from '@/hooks/use-accounts'; type ContextMode = 'isolated' | 'shared'; +type ContinuityMode = 'standard' | 'deeper'; const MAX_CONTEXT_GROUP_LENGTH = 64; const CONTEXT_GROUP_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/; @@ -36,6 +37,9 @@ export function EditAccountContextDialog({ account, onClose }: EditAccountContex account.context_mode === 'shared' ? 'shared' : 'isolated' ); const [group, setGroup] = useState(account.context_group || 'default'); + const [continuityMode, setContinuityMode] = useState( + account.continuity_mode === 'deeper' ? 'deeper' : 'standard' + ); const normalizedGroup = useMemo(() => group.trim().toLowerCase().replace(/\s+/g, '-'), [group]); const isSharedGroupValid = @@ -54,6 +58,7 @@ export function EditAccountContextDialog({ account, onClose }: EditAccountContex name: account.name, context_mode: mode, context_group: mode === 'shared' ? normalizedGroup : undefined, + continuity_mode: mode === 'shared' ? continuityMode : undefined, }, { onSuccess: () => { @@ -73,34 +78,36 @@ export function EditAccountContextDialog({ account, onClose }: EditAccountContex - Edit Context Mode + Edit History Sync - Configure how "{account.name}" shares project workspace context with other accounts. + Configure how "{account.name}" shares history and continuity with other + ccs auth + accounts.
    - +

    {mode === 'shared' - ? 'Shared mode reuses workspace context for accounts in the same group.' - : 'Isolated mode keeps this account workspace context separate.'} + ? 'Shared mode reuses workspace context for accounts in the same history sync group.' + : 'Isolated mode keeps this account fully separate from other ccs auth accounts.'}

    {mode === 'shared' && (
    - + )} + {mode === 'shared' && ( +
    + + +

    + {continuityMode === 'deeper' + ? 'Advanced mode also syncs session-env, file-history, shell-snapshots, and todos.' + : 'Standard mode syncs project workspace context only.'} +

    +
    + )} +

    - Credentials stay isolated per account. Only project workspace context is shared. + Credentials and `.anthropic` remain isolated per account in all modes.

    diff --git a/ui/src/hooks/use-accounts.ts b/ui/src/hooks/use-accounts.ts index 82684e3e..8caff09c 100644 --- a/ui/src/hooks/use-accounts.ts +++ b/ui/src/hooks/use-accounts.ts @@ -13,7 +13,10 @@ export interface AuthAccountsView { default: string | null; cliproxyCount: number; legacyContextCount: number; + legacyContinuityCount: number; sharedCount: number; + sharedStandardCount: number; + deeperSharedCount: number; isolatedCount: number; } @@ -27,8 +30,18 @@ export function useAccounts() { const sharedCount = authAccounts.filter( (account) => account.context_mode === 'shared' ).length; + const deeperSharedCount = authAccounts.filter( + (account) => account.context_mode === 'shared' && account.continuity_mode === 'deeper' + ).length; + const sharedStandardCount = Math.max(sharedCount - deeperSharedCount, 0); const isolatedCount = authAccounts.length - sharedCount; const legacyContextCount = authAccounts.filter((account) => account.context_inferred).length; + const legacyContinuityCount = authAccounts.filter( + (account) => + account.context_mode === 'shared' && + account.continuity_mode !== 'deeper' && + account.continuity_inferred + ).length; const defaultAccount = authAccounts.some((account) => account.name === data.default) ? data.default : null; @@ -38,7 +51,10 @@ export function useAccounts() { default: defaultAccount, cliproxyCount, legacyContextCount, + legacyContinuityCount, sharedCount, + sharedStandardCount, + deeperSharedCount, isolatedCount, }; }, @@ -98,16 +114,20 @@ export function useUpdateAccountContext() { name, context_mode, context_group, + continuity_mode, }: { name: string; context_mode: 'isolated' | 'shared'; context_group?: string; - }) => api.accounts.updateContext(name, { context_mode, context_group }), + continuity_mode?: 'standard' | 'deeper'; + }) => api.accounts.updateContext(name, { context_mode, context_group, continuity_mode }), onSuccess: (_data, vars) => { queryClient.invalidateQueries({ queryKey: ['accounts'] }); const contextSummary = vars.context_mode === 'shared' - ? `shared (${(vars.context_group || 'default').trim().toLowerCase().replace(/\s+/g, '-')})` + ? vars.continuity_mode === 'deeper' + ? `shared (${(vars.context_group || 'default').trim().toLowerCase().replace(/\s+/g, '-')}, deeper continuity)` + : `shared (${(vars.context_group || 'default').trim().toLowerCase().replace(/\s+/g, '-')}, standard)` : 'isolated'; toast.success(`Updated "${vars.name}" context to ${contextSummary}`); }, diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index f45600d6..5f581f2f 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -455,7 +455,9 @@ export interface Account { last_used?: string | null; context_mode?: 'isolated' | 'shared'; context_group?: string; + continuity_mode?: 'standard' | 'deeper'; context_inferred?: boolean; + continuity_inferred?: boolean; provider?: string; displayName?: string; } @@ -463,6 +465,7 @@ export interface Account { export interface UpdateAccountContext { context_mode: 'isolated' | 'shared'; context_group?: string; + continuity_mode?: 'standard' | 'deeper'; } // Unified config types diff --git a/ui/src/pages/accounts.tsx b/ui/src/pages/accounts.tsx index 9bde14fe..74d322a0 100644 --- a/ui/src/pages/accounts.tsx +++ b/ui/src/pages/accounts.tsx @@ -5,7 +5,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { ArrowRight, Link2, Plus, Unlink, Users, Zap } from 'lucide-react'; +import { ArrowRight, Link2, Plus, Unlink, Users, Waves, Zap } from 'lucide-react'; import { AccountsTable } from '@/components/account/accounts-table'; import { CreateAuthProfileDialog } from '@/components/account/create-auth-profile-dialog'; import { Button } from '@/components/ui/button'; @@ -21,7 +21,10 @@ export function AccountsPage() { const authAccounts = data?.accounts || []; const cliproxyCount = data?.cliproxyCount || 0; const legacyContextCount = data?.legacyContextCount || 0; + const legacyContinuityCount = data?.legacyContinuityCount || 0; const sharedCount = data?.sharedCount || 0; + const sharedStandardCount = data?.sharedStandardCount || 0; + const deeperSharedCount = data?.deeperSharedCount || 0; const isolatedCount = data?.isolatedCount || 0; return ( @@ -29,13 +32,13 @@ export function AccountsPage() {
    - CCS Auth Accounts + ccs auth Continuity

    Accounts

    - This page is dedicated to{' '} - ccs auth accounts. Choose - isolated mode to keep context separate, or shared mode to link context across selected - accounts. + This page manages + ccs auth + accounts only. Choose isolated, shared-standard, or shared-deeper continuity per + account.

    @@ -44,17 +47,48 @@ export function AccountsPage() { Create Account - {cliproxyCount > 0 && ( - - )} +
    -
    +
    + + + + Lane A: ccs auth continuity + + + + Profile isolation + opt-in sync + + + + Use this when you want per-account control over isolated vs shared history behavior. + + + + + + + Lane B: CLIProxy Claude pool + + + + OAuth pool and lower manual switching + + + + Use CLIProxy when you want pooled Claude OAuth accounts and easier account routing + behavior. + + +
    + +
    Total Auth Accounts @@ -68,11 +102,23 @@ export function AccountsPage() { - Shared (Linked) + Shared Standard - {sharedCount} + {sharedStandardCount} + + + + + + + + Shared Deeper + + + + {deeperSharedCount} @@ -80,7 +126,7 @@ export function AccountsPage() { - Isolated (Separate) + Isolated @@ -93,11 +139,10 @@ export function AccountsPage() { {cliproxyCount > 0 && ( - OAuth accounts are managed elsewhere + CLIProxy accounts are intentionally excluded from this table - This screen hides {cliproxyCount} CLIProxy OAuth account - {cliproxyCount > 1 ? 's' : ''}. Use CLIProxy Plus to manage Gemini, - Codex, Antigravity, and other OAuth providers. + {cliproxyCount} CLIProxy OAuth account{cliproxyCount > 1 ? 's are' : ' is'} available. + Manage them in CLIProxy Plus to enable account pool usage. )} @@ -105,21 +150,33 @@ export function AccountsPage() { {legacyContextCount > 0 && ( - Legacy accounts need context review + Legacy accounts need first-time sync mode review {legacyContextCount} account{legacyContextCount > 1 ? 's were' : ' was'} onboarded - before context controls and currently default to isolated mode. Use the pencil action in - the table to explicitly choose isolated or shared. + before explicit context controls. Use the pencil action to confirm isolated or shared + behavior. + + + )} + + {legacyContinuityCount > 0 && ( + + + Shared legacy accounts default to standard continuity + + {legacyContinuityCount} shared account + {legacyContinuityCount > 1 ? 's are' : ' is'} currently on legacy standard depth. Edit + and switch to deeper continuity only when you intentionally want broader history sync. )} - CCS Auth Accounts + ccs auth Accounts - New onboarding: Create Account. - Existing accounts: use the pencil action to control linkage flexibility per account. + Shared total: {sharedCount}. Create accounts here, then tune per-account sync mode and + continuity depth from the table. From 12c7a218b709cd1a93709b6f36c6afa0afdb2697 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Feb 2026 20:31:35 +0700 Subject: [PATCH 04/11] feat(accounts-ui): redesign accounts dashboard to column layout --- ui/src/pages/accounts.tsx | 476 ++++++++++++++++++++++++++------------ 1 file changed, 329 insertions(+), 147 deletions(-) diff --git a/ui/src/pages/accounts.tsx b/ui/src/pages/accounts.tsx index 74d322a0..2e36ffe7 100644 --- a/ui/src/pages/accounts.tsx +++ b/ui/src/pages/accounts.tsx @@ -5,14 +5,87 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { ArrowRight, Link2, Plus, Unlink, Users, Waves, Zap } from 'lucide-react'; +import { AlertTriangle, ArrowRight, Link2, Plus, Unlink, Users, Waves, Zap } from 'lucide-react'; import { AccountsTable } from '@/components/account/accounts-table'; import { CreateAuthProfileDialog } from '@/components/account/create-auth-profile-dialog'; +import { CopyButton } from '@/components/ui/copy-button'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { ScrollArea } from '@/components/ui/scroll-area'; import { useAccounts } from '@/hooks/use-accounts'; +import { cn } from '@/lib/utils'; +import type { LucideIcon } from 'lucide-react'; + +type MetricTone = 'default' | 'shared' | 'deeper' | 'isolated'; + +function MetricTile({ + label, + value, + icon: Icon, + tone = 'default', +}: { + label: string; + value: number; + icon: LucideIcon; + tone?: MetricTone; +}) { + const toneClasses: Record = { + default: { + border: 'border-border', + icon: 'text-primary', + }, + shared: { + border: 'border-emerald-300/60 dark:border-emerald-900/40', + icon: 'text-emerald-700 dark:text-emerald-400', + }, + deeper: { + border: 'border-indigo-300/60 dark:border-indigo-900/40', + icon: 'text-indigo-700 dark:text-indigo-400', + }, + isolated: { + border: 'border-blue-300/60 dark:border-blue-900/40', + icon: 'text-blue-700 dark:text-blue-400', + }, + }; + + return ( +
    +
    + {label} + +
    +

    {value}

    +
    + ); +} + +function StrategyCard({ + title, + description, + variant, +}: { + title: string; + description: string; + variant: 'auth' | 'pool'; +}) { + const variantClasses = + variant === 'auth' + ? 'border-emerald-300/70 bg-emerald-50/40 dark:border-emerald-900/40 dark:bg-emerald-900/10' + : 'border-blue-300/70 bg-blue-50/40 dark:border-blue-900/40 dark:bg-blue-900/10'; + const titleClasses = + variant === 'auth' + ? 'text-emerald-800 dark:text-emerald-300' + : 'text-blue-800 dark:text-blue-300'; + + return ( +
    +

    {title}

    +

    {description}

    +
    + ); +} export function AccountsPage() { const navigate = useNavigate(); @@ -26,169 +99,278 @@ export function AccountsPage() { const sharedStandardCount = data?.sharedStandardCount || 0; const deeperSharedCount = data?.deeperSharedCount || 0; const isolatedCount = data?.isolatedCount || 0; + const hasLegacyFollowUp = legacyContextCount > 0 || legacyContinuityCount > 0; return ( -
    -
    -
    -
    - ccs auth Continuity -

    Accounts

    -

    - This page manages - ccs auth - accounts only. Choose isolated, shared-standard, or shared-deeper continuity per - account. -

    + <> +
    + {/* Left rail */} +
    +
    +
    +
    + +

    Accounts

    +
    +

    + Dedicated + ccs auth + continuity controls. +

    +
    + +
    + + +
    -
    - - + +
    +
    +

    + Snapshot +

    +
    + + + + +
    +
    + +
    +

    + Strategy Lanes +

    + + +
    + + {hasLegacyFollowUp && ( +
    +

    + Migration Follow-up +

    +
    +
    + +
    + {legacyContextCount > 0 && ( +

    + {legacyContextCount} account + {legacyContextCount > 1 ? 's still need' : ' still needs'} first-time + mode confirmation. +

    + )} + {legacyContinuityCount > 0 && ( +

    + {legacyContinuityCount} shared account + {legacyContinuityCount > 1 ? 's remain' : ' remains'} on standard legacy + continuity depth. +

    + )} +
    +
    +
    +
    + )} +
    +
    + +
    +
    + Standard shared + {sharedStandardCount} +
    +
    + CLIProxy hidden + {cliproxyCount} +
    +
    +
    + + {/* Main workspace */} +
    + {/* Table column */} +
    +
    +
    + ccs auth Workspace + History Sync Controls +
    +

    Auth Accounts

    +

    + This table is intentionally scoped to + ccs auth + accounts. Edit each account for isolated, shared-standard, or shared-deeper + continuity behavior. +

    +
    + +
    + {cliproxyCount > 0 && ( + + + CLIProxy pool accounts are managed in their own page + + {cliproxyCount} OAuth account{cliproxyCount > 1 ? 's are' : ' is'} available in + CLIProxy. This table only covers local + ccs auth + profiles. + + + )} + + + + Account Matrix + + Shared total: {sharedCount}. Update sync behavior from the pencil action in each + row. + + + + {isLoading ? ( +
    Loading accounts...
    + ) : ( + + )} +
    +
    +
    +
    + + {/* Right guidance column */} +
    +
    +

    Continuity Guide

    +

    + Choose the lightest mode that solves your workflow. +

    +
    + + +
    + + + Shared Standard + Project workspace sync only. + + + Best default when users need continuity but want minimal coupling. + + + + + + Shared Deeper (Advanced) + + Adds session-env, file-history,{' '} + shell-snapshots, todos. + + + + Use only when cross-account continuity is worth stronger coupling. + + + + + + Quick Commands + Copy and run in terminal. + + +
    + + ccs auth create work --context-group sprint-a --deeper-continuity + + +
    +
    + ccs cliproxy auth claude + +
    +
    +
    +
    +
    -
    - - - - Lane A: ccs auth continuity - - - - Profile isolation + opt-in sync - - - - Use this when you want per-account control over isolated vs shared history behavior. - - - - - - - Lane B: CLIProxy Claude pool - - - - OAuth pool and lower manual switching - - - - Use CLIProxy when you want pooled Claude OAuth accounts and easier account routing - behavior. - - -
    - -
    + {/* Mobile fallback */} +
    - - Total Auth Accounts - - - {authAccounts.length} - + + Accounts + + Manage + ccs auth + continuity per account. + + + + + - - - - Shared Standard - - - - {sharedStandardCount} - - - +
    + + + + +
    - - - - Shared Deeper - - - - {deeperSharedCount} - - - - - - - - Isolated - - - - {isolatedCount} - + + + Account Matrix + + {isLoading ? ( +
    Loading accounts...
    + ) : ( + + )} +
    - {cliproxyCount > 0 && ( - - - CLIProxy accounts are intentionally excluded from this table - - {cliproxyCount} CLIProxy OAuth account{cliproxyCount > 1 ? 's are' : ' is'} available. - Manage them in CLIProxy Plus to enable account pool usage. - - - )} - - {legacyContextCount > 0 && ( - - - Legacy accounts need first-time sync mode review - - {legacyContextCount} account{legacyContextCount > 1 ? 's were' : ' was'} onboarded - before explicit context controls. Use the pencil action to confirm isolated or shared - behavior. - - - )} - - {legacyContinuityCount > 0 && ( - - - Shared legacy accounts default to standard continuity - - {legacyContinuityCount} shared account - {legacyContinuityCount > 1 ? 's are' : ' is'} currently on legacy standard depth. Edit - and switch to deeper continuity only when you intentionally want broader history sync. - - - )} - - - - ccs auth Accounts - - Shared total: {sharedCount}. Create accounts here, then tune per-account sync mode and - continuity depth from the table. - - - - {isLoading ? ( -
    Loading accounts...
    - ) : ( - - )} -
    -
    - setCreateDialogOpen(false)} /> -
    + ); } From a974efb8b11e02d26234d8cd9303ae9aef420d67 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Feb 2026 20:49:23 +0700 Subject: [PATCH 05/11] fix(accounts-ui): add actionable continuity controls and claude deep-link --- ui/src/components/account/accounts-table.tsx | 71 ++++- ui/src/hooks/use-accounts.ts | 41 +++ ui/src/pages/accounts.tsx | 285 ++++++++++++++----- ui/src/pages/cliproxy.tsx | 30 +- 4 files changed, 350 insertions(+), 77 deletions(-) diff --git a/ui/src/components/account/accounts-table.tsx b/ui/src/components/account/accounts-table.tsx index d1b7232c..757ff701 100644 --- a/ui/src/components/account/accounts-table.tsx +++ b/ui/src/components/account/accounts-table.tsx @@ -24,12 +24,13 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; -import { Check, Pencil, Trash2, RotateCcw } from 'lucide-react'; +import { Check, CheckCheck, Link2, Pencil, RotateCcw, Trash2, Unlink } from 'lucide-react'; import { EditAccountContextDialog } from '@/components/account/edit-account-context-dialog'; import { useSetDefaultAccount, useDeleteAccount, useResetDefaultAccount, + useUpdateAccountContext, } from '@/hooks/use-accounts'; import type { Account } from '@/lib/api-client'; @@ -42,6 +43,7 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) { const setDefaultMutation = useSetDefaultAccount(); const deleteMutation = useDeleteAccount(); const resetDefaultMutation = useResetDefaultAccount(); + const updateContextMutation = useUpdateAccountContext(); const [deleteTarget, setDeleteTarget] = useState(null); const [contextTarget, setContextTarget] = useState(null); @@ -130,8 +132,14 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) { size: 220, cell: ({ row }) => { const isDefault = row.original.name === defaultAccount; - const isPending = setDefaultMutation.isPending || deleteMutation.isPending; + const isPending = + setDefaultMutation.isPending || + deleteMutation.isPending || + updateContextMutation.isPending; const isCliproxy = row.original.type === 'cliproxy'; + const isShared = row.original.context_mode === 'shared'; + const hasLegacyInference = + row.original.context_inferred || row.original.continuity_inferred; return (
    @@ -147,6 +155,63 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) { )} + {!isCliproxy && ( + + )} + {!isCliproxy && hasLegacyInference && ( + + )} +
    @@ -176,7 +210,7 @@ export function AccountsPage() {

    Migration Follow-up

    -
    +
    @@ -196,6 +230,18 @@ export function AccountsPage() { )}
    + +
    )} @@ -222,19 +268,29 @@ export function AccountsPage() {
    ccs auth Workspace History Sync Controls +

    Auth Accounts

    This table is intentionally scoped to ccs auth - accounts. Edit each account for isolated, shared-standard, or shared-deeper - continuity behavior. + accounts. Use + Link/ + Unlink + for quick policy changes and pencil edit for advanced group/deeper settings.

    -
    +
    {cliproxyCount > 0 && ( - + CLIProxy pool accounts are managed in their own page @@ -246,15 +302,15 @@ export function AccountsPage() { )} - + Account Matrix - Shared total: {sharedCount}. Update sync behavior from the pencil action in each - row. + Shared total: {sharedCount}. Actions now include quick link/unlink plus legacy + confirmation. - + {isLoading ? (
    Loading accounts...
    ) : ( @@ -265,64 +321,137 @@ export function AccountsPage() {
    - {/* Right guidance column */} -
    -
    -

    Continuity Guide

    -

    - Choose the lightest mode that solves your workflow. -

    -
    - - -
    - - - Shared Standard - Project workspace sync only. - - - Best default when users need continuity but want minimal coupling. - - - - - - Shared Deeper (Advanced) - - Adds session-env, file-history,{' '} - shell-snapshots, todos. - - - - Use only when cross-account continuity is worth stronger coupling. - - - - - - Quick Commands - Copy and run in terminal. - - -
    - - ccs auth create work --context-group sprint-a --deeper-continuity - - -
    -
    - ccs cliproxy auth claude - -
    -
    -
    + {/* Right action center */} + {showGuideRail ? ( +
    +
    +
    +

    Action Center

    +

    + High-value actions for pool auth and legacy cleanup. +

    +
    +
    - -
    + + +
    + + + Immediate Actions + + + + + + + + + + + + + + + + + +
    +

    Shared Standard

    +

    + Project workspace sync only. Best default for most teams. +

    +
    +
    +

    Shared Deeper

    +

    + Adds session-env, file-history,{' '} + shell-snapshots, todos. +

    +
    +
    +

    Isolated

    +

    No link. Best for strict separation.

    +
    +
    +
    +
    +
    + + + + Quick Commands + Copy and run in terminal. + + +
    + + ccs auth create work --context-group sprint-a --deeper-continuity + + +
    +
    + ccs cliproxy auth claude + +
    +
    +
    +
    +
    +
    + ) : ( +
    + +
    + )}
    @@ -342,10 +471,24 @@ export function AccountsPage() { Create Account - + + diff --git a/ui/src/pages/cliproxy.tsx b/ui/src/pages/cliproxy.tsx index 5f6229b7..345f0fd0 100644 --- a/ui/src/pages/cliproxy.tsx +++ b/ui/src/pages/cliproxy.tsx @@ -4,7 +4,7 @@ * Right panel: Provider Editor with split-view (settings + code editor) */ -import { useState, useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -32,6 +32,7 @@ import { } from '@/hooks/use-cliproxy'; import type { AuthStatus, Variant } from '@/lib/api-client'; import { MODEL_CATALOGS } from '@/lib/model-catalogs'; +import { getProviderDisplayName, isValidProvider } from '@/lib/provider-config'; import { cn } from '@/lib/utils'; // Sidebar provider item @@ -198,9 +199,14 @@ export function CliproxyPage() { const deleteMutation = useDeleteVariant(); // Selection state: either a provider or a variant - // Initialize from localStorage if available + // Initialize from URL provider deep-link, fallback to localStorage. const [selectedProvider, setSelectedProviderState] = useState(() => { if (typeof window !== 'undefined') { + const query = new URLSearchParams(window.location.search); + const queryProvider = query.get('provider')?.trim().toLowerCase(); + if (queryProvider && isValidProvider(queryProvider)) { + return queryProvider; + } return localStorage.getItem('cliproxy-selected-provider'); } return null; @@ -211,7 +217,25 @@ export function CliproxyPage() { provider: string; displayName: string; isFirstAccount: boolean; - } | null>(null); + } | null>(() => { + if (typeof window === 'undefined') { + return null; + } + + const query = new URLSearchParams(window.location.search); + const queryProvider = query.get('provider')?.trim().toLowerCase(); + const action = query.get('action'); + + if (action !== 'auth' || !queryProvider || !isValidProvider(queryProvider)) { + return null; + } + + return { + provider: queryProvider, + displayName: getProviderDisplayName(queryProvider), + isFirstAccount: false, + }; + }); const providers = useMemo(() => authData?.authStatus || [], [authData?.authStatus]); const isRemoteMode = authData?.source === 'remote'; From 6cfc8d4a45fcaf80f413dd1b397e5f7f46f889d0 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Feb 2026 20:59:16 +0700 Subject: [PATCH 06/11] feat(accounts-ui): add history sync learning visualization --- .../account/history-sync-learning-map.tsx | 163 ++++++++++++++++++ ui/src/pages/accounts.tsx | 37 ++-- 2 files changed, 182 insertions(+), 18 deletions(-) create mode 100644 ui/src/components/account/history-sync-learning-map.tsx diff --git a/ui/src/components/account/history-sync-learning-map.tsx b/ui/src/components/account/history-sync-learning-map.tsx new file mode 100644 index 00000000..b63b53a2 --- /dev/null +++ b/ui/src/components/account/history-sync-learning-map.tsx @@ -0,0 +1,163 @@ +import { + ArrowRight, + ArrowRightLeft, + Layers3, + Link2, + Unlink, + Waves, + type LucideIcon, +} from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { cn } from '@/lib/utils'; + +interface HistorySyncLearningMapProps { + isolatedCount: number; + sharedStandardCount: number; + deeperSharedCount: number; + sharedGroups: string[]; + legacyTargetCount: number; +} + +type StageTone = 'isolated' | 'shared' | 'deeper'; + +function StageCard({ + title, + count, + icon: Icon, + tone, + description, +}: { + title: string; + count: number; + icon: LucideIcon; + tone: StageTone; + description: string; +}) { + const toneClasses: Record = { + isolated: { + card: 'border-blue-300/60 bg-blue-50/40 dark:border-blue-900/40 dark:bg-blue-900/10', + icon: 'text-blue-700 dark:text-blue-400', + count: 'text-blue-700 dark:text-blue-400', + }, + shared: { + card: 'border-emerald-300/60 bg-emerald-50/40 dark:border-emerald-900/40 dark:bg-emerald-900/10', + icon: 'text-emerald-700 dark:text-emerald-400', + count: 'text-emerald-700 dark:text-emerald-400', + }, + deeper: { + card: 'border-indigo-300/60 bg-indigo-50/40 dark:border-indigo-900/40 dark:bg-indigo-900/10', + icon: 'text-indigo-700 dark:text-indigo-400', + count: 'text-indigo-700 dark:text-indigo-400', + }, + }; + + return ( +
    +
    +

    {title}

    + +
    +

    + {count} +

    +

    {description}

    +
    + ); +} + +export function HistorySyncLearningMap({ + isolatedCount, + sharedStandardCount, + deeperSharedCount, + sharedGroups, + legacyTargetCount, +}: HistorySyncLearningMapProps) { + const groupsToShow = sharedGroups.length > 0 ? sharedGroups : ['default']; + + return ( + + +
    + How History Sync Works + Learning Map +
    + + Accounts can move between modes at any time. Link/unlink is instant; Edit gives full + control of group and continuity depth. + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    +
    + +

    Mode Switch Actions

    +
    +
    + Link: Isolated -> Shared + Unlink: Shared -> Isolated + Edit: Group + Deeper +
    +
    + +
    +
    + +

    History Sync Group

    +
    +

    + Accounts in the same group share the same project context bucket. Group names are + user-defined lanes, with default as fallback. +

    +
    + {groupsToShow.map((group) => ( + + {group} + + ))} +
    +
    +
    + + {legacyTargetCount > 0 && ( +
    + {legacyTargetCount} legacy account + {legacyTargetCount > 1 ? 's still need' : ' still needs'} explicit confirmation. Use{' '} + Confirm Legacy Policies in Action Center. +
    + )} +
    +
    + ); +} diff --git a/ui/src/pages/accounts.tsx b/ui/src/pages/accounts.tsx index ab1d2e02..0f1b603a 100644 --- a/ui/src/pages/accounts.tsx +++ b/ui/src/pages/accounts.tsx @@ -19,6 +19,7 @@ import { } from 'lucide-react'; import { AccountsTable } from '@/components/account/accounts-table'; import { CreateAuthProfileDialog } from '@/components/account/create-auth-profile-dialog'; +import { HistorySyncLearningMap } from '@/components/account/history-sync-learning-map'; import { CopyButton } from '@/components/ui/copy-button'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -115,6 +116,13 @@ export function AccountsPage() { const sharedStandardCount = data?.sharedStandardCount || 0; const deeperSharedCount = data?.deeperSharedCount || 0; const isolatedCount = data?.isolatedCount || 0; + const sharedGroups = Array.from( + new Set( + authAccounts + .filter((account) => account.context_mode === 'shared') + .map((account) => account.context_group || 'default') + ) + ).sort((a, b) => a.localeCompare(b)); const legacyTargets = authAccounts.filter( (account) => account.context_inferred || account.continuity_inferred @@ -149,25 +157,10 @@ export function AccountsPage() { Create Account - -
    +

    + Pool auth actions live in Action Center to avoid duplicate controls. +

    @@ -302,6 +295,14 @@ export function AccountsPage() { )} + + Account Matrix From 286180e4653a1b2a467f2fd87ed1a192545a9496 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Feb 2026 21:06:55 +0700 Subject: [PATCH 07/11] fix(accounts-ui): condense sync guidance and remove redundant controls --- ui/src/components/account/accounts-table.tsx | 41 +--- .../account/history-sync-learning-map.tsx | 178 +++++++++--------- ui/src/pages/accounts.tsx | 161 ++-------------- 3 files changed, 116 insertions(+), 264 deletions(-) diff --git a/ui/src/components/account/accounts-table.tsx b/ui/src/components/account/accounts-table.tsx index 757ff701..37abf24e 100644 --- a/ui/src/components/account/accounts-table.tsx +++ b/ui/src/components/account/accounts-table.tsx @@ -24,7 +24,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; -import { Check, CheckCheck, Link2, Pencil, RotateCcw, Trash2, Unlink } from 'lucide-react'; +import { Check, CheckCheck, Pencil, RotateCcw, Trash2 } from 'lucide-react'; import { EditAccountContextDialog } from '@/components/account/edit-account-context-dialog'; import { useSetDefaultAccount, @@ -137,51 +137,22 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) { deleteMutation.isPending || updateContextMutation.isPending; const isCliproxy = row.original.type === 'cliproxy'; - const isShared = row.original.context_mode === 'shared'; const hasLegacyInference = row.original.context_inferred || row.original.continuity_inferred; return (
    - {!isCliproxy && ( - - )} {!isCliproxy && ( )} {!isCliproxy && hasLegacyInference && ( @@ -270,7 +241,7 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) { created: 'w-[150px]', last_used: 'w-[150px]', context: 'w-[170px]', - actions: 'w-[340px]', + actions: 'w-[290px]', }[header.id] || 'w-auto'; return ( diff --git a/ui/src/components/account/history-sync-learning-map.tsx b/ui/src/components/account/history-sync-learning-map.tsx index b63b53a2..09ad40b0 100644 --- a/ui/src/components/account/history-sync-learning-map.tsx +++ b/ui/src/components/account/history-sync-learning-map.tsx @@ -1,6 +1,8 @@ +import { useState } from 'react'; import { ArrowRight, ArrowRightLeft, + ChevronDown, Layers3, Link2, Unlink, @@ -8,7 +10,9 @@ import { type LucideIcon, } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { cn } from '@/lib/utils'; interface HistorySyncLearningMapProps { @@ -17,51 +21,49 @@ interface HistorySyncLearningMapProps { deeperSharedCount: number; sharedGroups: string[]; legacyTargetCount: number; + cliproxyCount: number; } type StageTone = 'isolated' | 'shared' | 'deeper'; -function StageCard({ +function StageTile({ title, count, icon: Icon, tone, - description, }: { title: string; count: number; icon: LucideIcon; tone: StageTone; - description: string; }) { - const toneClasses: Record = { + const toneClasses: Record = { isolated: { - card: 'border-blue-300/60 bg-blue-50/40 dark:border-blue-900/40 dark:bg-blue-900/10', + border: 'border-blue-300/60 bg-blue-50/40 dark:border-blue-900/40 dark:bg-blue-900/10', icon: 'text-blue-700 dark:text-blue-400', count: 'text-blue-700 dark:text-blue-400', }, shared: { - card: 'border-emerald-300/60 bg-emerald-50/40 dark:border-emerald-900/40 dark:bg-emerald-900/10', + border: + 'border-emerald-300/60 bg-emerald-50/40 dark:border-emerald-900/40 dark:bg-emerald-900/10', icon: 'text-emerald-700 dark:text-emerald-400', count: 'text-emerald-700 dark:text-emerald-400', }, deeper: { - card: 'border-indigo-300/60 bg-indigo-50/40 dark:border-indigo-900/40 dark:bg-indigo-900/10', + border: + 'border-indigo-300/60 bg-indigo-50/40 dark:border-indigo-900/40 dark:bg-indigo-900/10', icon: 'text-indigo-700 dark:text-indigo-400', count: 'text-indigo-700 dark:text-indigo-400', }, }; return ( -
    +
    -

    {title}

    - +

    {title}

    +
    -

    - {count} -

    -

    {description}

    +

    {count}

    ); } @@ -72,91 +74,93 @@ export function HistorySyncLearningMap({ deeperSharedCount, sharedGroups, legacyTargetCount, + cliproxyCount, }: HistorySyncLearningMapProps) { + const [open, setOpen] = useState(false); const groupsToShow = sharedGroups.length > 0 ? sharedGroups : ['default']; return ( - +
    - How History Sync Works +
    + How History Sync Works + + Isolated -> Shared -> Deeper. Use Sync per row for all changes. + +
    Learning Map
    - - Accounts can move between modes at any time. Link/unlink is instant; Edit gives full - control of group and continuity depth. -
    - -
    - -
    - -
    - -
    - -
    - -
    -
    -
    -
    - -

    Mode Switch Actions

    -
    -
    - Link: Isolated -> Shared - Unlink: Shared -> Isolated - Edit: Group + Deeper -
    -
    - -
    -
    - -

    History Sync Group

    -
    -

    - Accounts in the same group share the same project context bucket. Group names are - user-defined lanes, with default as fallback. -

    -
    - {groupsToShow.map((group) => ( - - {group} - - ))} -
    -
    -
    - - {legacyTargetCount > 0 && ( -
    - {legacyTargetCount} legacy account - {legacyTargetCount > 1 ? 's still need' : ' still needs'} explicit confirmation. Use{' '} - Confirm Legacy Policies in Action Center. + + {cliproxyCount > 0 && ( +
    + {cliproxyCount} CLIProxy Claude pool account{cliproxyCount > 1 ? 's are' : ' is'} + managed in Action Center / CLIProxy page.
    )} + +
    + +
    + +
    + +
    + +
    + +
    + + + + + + +
    +
    +
    + +

    Mode Switch

    +
    +

    + Sync dialog lets users move between isolated/shared and choose deeper continuity. +

    +
    + +
    +
    + +

    History Sync Group

    +
    +

    + Same group means shared project context lane. Default fallback is{' '} + default. +

    +
    + {groupsToShow.map((group) => ( + + {group} + + ))} +
    +
    +
    + + {legacyTargetCount > 0 && ( +
    + {legacyTargetCount} legacy account + {legacyTargetCount > 1 ? 's still need' : ' still needs'} explicit confirmation. +
    + )} +
    +
    ); diff --git a/ui/src/pages/accounts.tsx b/ui/src/pages/accounts.tsx index 0f1b603a..9e00cd5e 100644 --- a/ui/src/pages/accounts.tsx +++ b/ui/src/pages/accounts.tsx @@ -10,11 +10,8 @@ import { ArrowRight, ChevronDown, ChevronLeft, - Link2, Plus, - Unlink, Users, - Waves, Zap, } from 'lucide-react'; import { AccountsTable } from '@/components/account/accounts-table'; @@ -23,82 +20,11 @@ import { HistorySyncLearningMap } from '@/components/account/history-sync-learni import { CopyButton } from '@/components/ui/copy-button'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useAccounts, useConfirmLegacyAccountPolicies } from '@/hooks/use-accounts'; import { cn } from '@/lib/utils'; -import type { LucideIcon } from 'lucide-react'; - -type MetricTone = 'default' | 'shared' | 'deeper' | 'isolated'; - -function MetricTile({ - label, - value, - icon: Icon, - tone = 'default', -}: { - label: string; - value: number; - icon: LucideIcon; - tone?: MetricTone; -}) { - const toneClasses: Record = { - default: { - border: 'border-border', - icon: 'text-primary', - }, - shared: { - border: 'border-emerald-300/60 dark:border-emerald-900/40', - icon: 'text-emerald-700 dark:text-emerald-400', - }, - deeper: { - border: 'border-indigo-300/60 dark:border-indigo-900/40', - icon: 'text-indigo-700 dark:text-indigo-400', - }, - isolated: { - border: 'border-blue-300/60 dark:border-blue-900/40', - icon: 'text-blue-700 dark:text-blue-400', - }, - }; - - return ( -
    -
    - {label} - -
    -

    {value}

    -
    - ); -} - -function StrategyCard({ - title, - description, - variant, -}: { - title: string; - description: string; - variant: 'auth' | 'pool'; -}) { - const variantClasses = - variant === 'auth' - ? 'border-emerald-300/70 bg-emerald-50/40 dark:border-emerald-900/40 dark:bg-emerald-900/10' - : 'border-blue-300/70 bg-blue-50/40 dark:border-blue-900/40 dark:bg-blue-900/10'; - const titleClasses = - variant === 'auth' - ? 'text-emerald-800 dark:text-emerald-300' - : 'text-blue-800 dark:text-blue-300'; - - return ( -
    -

    {title}

    -

    {description}

    -
    - ); -} export function AccountsPage() { const navigate = useNavigate(); @@ -164,40 +90,7 @@ export function AccountsPage() {
    -
    -
    -

    - Snapshot -

    -
    - - - - -
    -
    - -
    -

    - Strategy Lanes -

    - - -
    - +
    {hasLegacyFollowUp && (

    @@ -238,19 +131,14 @@ export function AccountsPage() {

    )} + + {!hasLegacyFollowUp && ( +
    + No legacy follow-up pending. Manage pool auth from Action Center. +
    + )}
    - -
    -
    - Standard shared - {sharedStandardCount} -
    -
    - CLIProxy hidden - {cliproxyCount} -
    -
    {/* Main workspace */} @@ -275,39 +163,26 @@ export function AccountsPage() { This table is intentionally scoped to ccs auth accounts. Use - Link/ - Unlink - for quick policy changes and pencil edit for advanced group/deeper settings. + Sync + for mode/group/depth changes.

    - {cliproxyCount > 0 && ( - - - CLIProxy pool accounts are managed in their own page - - {cliproxyCount} OAuth account{cliproxyCount > 1 ? 's are' : ' is'} available in - CLIProxy. This table only covers local - ccs auth - profiles. - - - )} - Account Matrix - Shared total: {sharedCount}. Actions now include quick link/unlink plus legacy + Shared total: {sharedCount}. Actions include Sync settings and legacy confirmation. @@ -493,12 +368,14 @@ export function AccountsPage() { -
    - - - - -
    + From 1708128a6201de8758a6632e732bf9fb5e50fff3 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Feb 2026 21:09:06 +0700 Subject: [PATCH 08/11] fix(accounts-ui): soften details trigger hover state --- ui/src/components/account/history-sync-learning-map.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ui/src/components/account/history-sync-learning-map.tsx b/ui/src/components/account/history-sync-learning-map.tsx index 09ad40b0..7c7b2f94 100644 --- a/ui/src/components/account/history-sync-learning-map.tsx +++ b/ui/src/components/account/history-sync-learning-map.tsx @@ -115,10 +115,11 @@ export function HistorySyncLearningMap({ - From cfdad81beef1fbd5051e3daf0ff9ba8240eb66bb Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Feb 2026 21:11:35 +0700 Subject: [PATCH 09/11] fix(accounts-ui): move action center content into left column --- ui/src/pages/accounts.tsx | 390 ++++++++++++++++---------------------- 1 file changed, 162 insertions(+), 228 deletions(-) diff --git a/ui/src/pages/accounts.tsx b/ui/src/pages/accounts.tsx index 9e00cd5e..6b368253 100644 --- a/ui/src/pages/accounts.tsx +++ b/ui/src/pages/accounts.tsx @@ -5,15 +5,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { - AlertTriangle, - ArrowRight, - ChevronDown, - ChevronLeft, - Plus, - Users, - Zap, -} from 'lucide-react'; +import { AlertTriangle, ArrowRight, ChevronDown, Plus, Users, Zap } from 'lucide-react'; import { AccountsTable } from '@/components/account/accounts-table'; import { CreateAuthProfileDialog } from '@/components/account/create-auth-profile-dialog'; import { HistorySyncLearningMap } from '@/components/account/history-sync-learning-map'; @@ -31,7 +23,6 @@ export function AccountsPage() { const { data, isLoading } = useAccounts(); const confirmLegacyMutation = useConfirmLegacyAccountPolicies(); const [createDialogOpen, setCreateDialogOpen] = useState(false); - const [showGuideRail, setShowGuideRail] = useState(true); const [guideOpen, setGuideOpen] = useState(false); const authAccounts = data?.accounts || []; @@ -63,34 +54,69 @@ export function AccountsPage() { return ( <>
    - {/* Left rail */} -
    -
    -
    -
    - -

    Accounts

    -
    -

    - Dedicated - ccs auth - continuity controls. -

    + {/* Left action column */} +
    +
    +
    + +

    Accounts

    + + Action Center +
    - -
    - -
    -

    - Pool auth actions live in Action Center to avoid duplicate controls. +

    + Manage + ccs auth + accounts and pool onboarding from one panel.

    -
    +
    + + + Immediate Actions + + + + + + + + + {hasLegacyFollowUp && (

    @@ -116,218 +142,126 @@ export function AccountsPage() { )}

    - -
    )} {!hasLegacyFollowUp && (
    - No legacy follow-up pending. Manage pool auth from Action Center. + No legacy follow-up pending.
    )} + + + + + + + + + + +
    +

    Shared Standard

    +

    + Project workspace sync only. Best default for most teams. +

    +
    +
    +

    Shared Deeper

    +

    + Adds session-env, file-history,{' '} + shell-snapshots, todos. +

    +
    +
    +

    Isolated

    +

    No link. Best for strict separation.

    +
    +
    +
    +
    +
    + + + + Quick Commands + Copy and run in terminal. + + +
    + + ccs auth create work --context-group sprint-a --deeper-continuity + + +
    +
    + ccs cliproxy auth claude + +
    +
    +
    {/* Main workspace */} -
    - {/* Table column */} -
    -
    -
    - ccs auth Workspace - History Sync Controls - -
    -

    Auth Accounts

    -

    - This table is intentionally scoped to - ccs auth - accounts. Use - Sync - for mode/group/depth changes. -

    -
    - -
    - - - - - Account Matrix - - Shared total: {sharedCount}. Actions include Sync settings and legacy - confirmation. - - - - {isLoading ? ( -
    Loading accounts...
    - ) : ( - - )} -
    -
    +
    +
    +
    + ccs auth Workspace + History Sync Controls
    +

    Auth Accounts

    +

    + This table is intentionally scoped to + ccs auth + accounts. Use + Sync + for mode/group/depth changes. +

    - {/* Right action center */} - {showGuideRail ? ( -
    -
    -
    -

    Action Center

    -

    - High-value actions for pool auth and legacy cleanup. -

    -
    - -
    +
    + - -
    - - - Immediate Actions - - - - - - - - - - - - - - - - - -
    -

    Shared Standard

    -

    - Project workspace sync only. Best default for most teams. -

    -
    -
    -

    Shared Deeper

    -

    - Adds session-env, file-history,{' '} - shell-snapshots, todos. -

    -
    -
    -

    Isolated

    -

    No link. Best for strict separation.

    -
    -
    -
    -
    -
    - - - - Quick Commands - Copy and run in terminal. - - -
    - - ccs auth create work --context-group sprint-a --deeper-continuity - - -
    -
    - ccs cliproxy auth claude - -
    -
    -
    -
    -
    -
    - ) : ( -
    - -
    - )} + + + Account Matrix + + Shared total: {sharedCount}. Actions include Sync settings and legacy + confirmation. + + + + {isLoading ? ( +
    Loading accounts...
    + ) : ( + + )} +
    +
    +
    From 7fccb1843d85efe3f01198aabf71ba71d35321ca Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Feb 2026 21:15:05 +0700 Subject: [PATCH 10/11] fix(accounts-ui): simplify left action column hierarchy --- ui/src/pages/accounts.tsx | 245 +++++++++++++++++++------------------- 1 file changed, 125 insertions(+), 120 deletions(-) diff --git a/ui/src/pages/accounts.tsx b/ui/src/pages/accounts.tsx index 6b368253..dc0813b1 100644 --- a/ui/src/pages/accounts.tsx +++ b/ui/src/pages/accounts.tsx @@ -75,142 +75,147 @@ export function AccountsPage() {
    - Immediate Actions + Action Center + + Primary actions plus migration status in one place. + - - - - - + +
    +

    + Primary Actions +

    + + + +
    + +
    +

    + Legacy Migration +

    + {hasLegacyFollowUp ? ( +
    +
    + +
    + {legacyContextCount > 0 && ( +

    + {legacyContextCount} account + {legacyContextCount > 1 ? 's still need' : ' still needs'}{' '} + first-time mode confirmation. +

    + )} + {legacyContinuityCount > 0 && ( +

    + {legacyContinuityCount} shared account + {legacyContinuityCount > 1 ? 's remain' : ' remains'} on standard + legacy continuity depth. +

    + )} +
    +
    + +
    + ) : ( +
    + No legacy follow-up pending. +
    + )} +
    - {hasLegacyFollowUp && ( -
    -

    - Migration Follow-up -

    -
    -
    - -
    - {legacyContextCount > 0 && ( -

    - {legacyContextCount} account - {legacyContextCount > 1 ? 's still need' : ' still needs'} first-time - mode confirmation. -

    - )} - {legacyContinuityCount > 0 && ( -

    - {legacyContinuityCount} shared account - {legacyContinuityCount > 1 ? 's remain' : ' remains'} on standard legacy - continuity depth. -

    - )} -
    -
    -
    -
    - )} - - {!hasLegacyFollowUp && ( -
    - No legacy follow-up pending. -
    - )} - - - - + + + Guide & Commands + Continuity modes and CLI shortcuts. + + + - - - -
    -

    Shared Standard

    -

    - Project workspace sync only. Best default for most teams. -

    + +
    +
    +

    Shared Standard

    +

    + Project workspace sync only. Best default for most teams. +

    +
    +
    +

    Shared Deeper

    +

    + Adds session-env, file-history,{' '} + shell-snapshots, todos. +

    +
    +
    +

    Isolated

    +

    No link. Best for strict separation.

    +
    -
    -

    Shared Deeper

    -

    - Adds session-env, file-history,{' '} - shell-snapshots, todos. -

    -
    -
    -

    Isolated

    -

    No link. Best for strict separation.

    -
    - -
    - - + + - - - Quick Commands - Copy and run in terminal. - - -
    - - ccs auth create work --context-group sprint-a --deeper-continuity - - -
    -
    - ccs cliproxy auth claude - +
    +

    Quick Commands

    +
    + + ccs auth create work --context-group sprint-a --deeper-continuity + + +
    +
    + ccs cliproxy auth claude + +
    From 5996f9df7a06064ee01aa6fa032d266a2c6edfba Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Feb 2026 21:20:35 +0700 Subject: [PATCH 11/11] fix(accounts-ui): remove extra left-column wrapper layers --- ui/src/pages/accounts.tsx | 242 ++++++++++++++++++-------------------- 1 file changed, 115 insertions(+), 127 deletions(-) diff --git a/ui/src/pages/accounts.tsx b/ui/src/pages/accounts.tsx index dc0813b1..b0c5efe1 100644 --- a/ui/src/pages/accounts.tsx +++ b/ui/src/pages/accounts.tsx @@ -60,9 +60,6 @@ export function AccountsPage() {

    Accounts

    - - Action Center -

    Manage @@ -73,149 +70,140 @@ export function AccountsPage() {

    - - - Action Center - - Primary actions plus migration status in one place. - - - -
    -

    - Primary Actions -

    +
    +

    + Primary Actions +

    + + + +
    + + {hasLegacyFollowUp ? ( +
    +

    + Migration Follow-up +

    +
    +
    + +
    + {legacyContextCount > 0 && ( +

    + {legacyContextCount} account + {legacyContextCount > 1 ? 's still need' : ' still needs'} first-time + mode confirmation. +

    + )} + {legacyContinuityCount > 0 && ( +

    + {legacyContinuityCount} shared account + {legacyContinuityCount > 1 ? 's remain' : ' remains'} on standard legacy + continuity depth. +

    + )} +
    +
    - -
    +
    + ) : ( +
    + No legacy follow-up pending. +
    + )} -
    -

    - Legacy Migration -

    - {hasLegacyFollowUp ? ( -
    -
    - -
    - {legacyContextCount > 0 && ( -

    - {legacyContextCount} account - {legacyContextCount > 1 ? 's still need' : ' still needs'}{' '} - first-time mode confirmation. -

    - )} - {legacyContinuityCount > 0 && ( -

    - {legacyContinuityCount} shared account - {legacyContinuityCount > 1 ? 's remain' : ' remains'} on standard - legacy continuity depth. -

    - )} -
    -
    - -
    - ) : ( -
    - No legacy follow-up pending. -
    - )} -
    - - - - - - Guide & Commands - Continuity modes and CLI shortcuts. - - - + + + - -
    -
    -

    Shared Standard

    -

    - Project workspace sync only. Best default for most teams. -

    -
    -
    -

    Shared Deeper

    -

    - Adds session-env, file-history,{' '} - shell-snapshots, todos. -

    -
    -
    -

    Isolated

    -

    No link. Best for strict separation.

    -
    + + + +
    +

    Shared Standard

    +

    + Project workspace sync only. Best default for most teams. +

    -
    - +
    +

    Shared Deeper

    +

    + Adds session-env, file-history,{' '} + shell-snapshots, todos. +

    +
    +
    +

    Isolated

    +

    No link. Best for strict separation.

    +
    + + + + -
    -

    Quick Commands

    -
    - - ccs auth create work --context-group sprint-a --deeper-continuity - - -
    -
    - ccs cliproxy auth claude - -
    + + + Quick Commands + Copy and run in terminal. + + +
    + + ccs auth create work --context-group sprint-a --deeper-continuity + + +
    +
    + ccs cliproxy auth claude +