From be9effcce34a7545943d195b3a5ce5369083e98a Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Tue, 5 May 2026 13:33:04 -0400 Subject: [PATCH] feat: clarify account history sync route (#1187) * feat: clarify account history sync route * fix: clarify shared context command examples * fix: report missing bare profile settings --- docs/dashboard-auth-cli.md | 23 ++- docs/session-sharing-technical-analysis.md | 48 +++++- src/auth/account-profile-diagnostics.ts | 140 +++++++++++++++++ src/auth/auth-commands.ts | 32 ++-- src/auth/commands/create-command.ts | 12 +- src/auth/commands/show-command.ts | 44 ++++++ src/auth/commands/types.ts | 20 +++ tests/unit/auth-help-route-guidance.test.ts | 21 +++ .../auth/account-profile-diagnostics.test.ts | 102 +++++++++++++ .../account/account-route-guide-card.tsx | 126 +++++++++++++++ ui/src/lib/i18n.ts | 143 ++++++++++++++++++ ui/src/pages/accounts.tsx | 20 ++- .../account/account-route-guide-card.test.tsx | 53 +++++++ 13 files changed, 767 insertions(+), 17 deletions(-) create mode 100644 src/auth/account-profile-diagnostics.ts create mode 100644 tests/unit/auth-help-route-guidance.test.ts create mode 100644 tests/unit/auth/account-profile-diagnostics.test.ts create mode 100644 ui/src/components/account/account-route-guide-card.tsx create mode 100644 ui/tests/unit/components/account/account-route-guide-card.test.tsx diff --git a/docs/dashboard-auth-cli.md b/docs/dashboard-auth-cli.md index f8c83fc5..b2c2a33d 100644 --- a/docs/dashboard-auth-cli.md +++ b/docs/dashboard-auth-cli.md @@ -1,6 +1,6 @@ # Dashboard Authentication CLI -Last Updated: 2026-04-06 +Last Updated: 2026-05-05 CLI commands for managing CCS dashboard authentication. @@ -27,7 +27,16 @@ Dashboard auth and account context metadata are separate: - `dashboard_auth`: protects dashboard access with username/password - `accounts..context_mode/context_group`: controls isolated vs shared account context -Account context is isolation-first: +Account context is isolation-first. The recommended two-account route is: + +```bash +ccs auth create work +ccs auth create personal +ccs work +ccs personal +``` + +Only enable history sync when both accounts should share local continuity while tokens stay separate: | Mode | Default | Requirement | |------|---------|-------------| @@ -39,6 +48,16 @@ Shared continuity depth: - `standard` (default): shares project workspace context only - `deeper` (advanced opt-in): also syncs `session-env`, `file-history`, `shell-snapshots`, `todos` +`ccs auth show ` reports credential isolation, settings sync state, history lane, and whether plain `ccs` currently uses the same resume lane. + +Non-bare account profiles share the basic Claude `settings.json` with native Claude: + +```text +~/.ccs/instances//settings.json -> ~/.ccs/shared/settings.json -> ~/.claude/settings.json +``` + +This keeps ordinary Claude settings in sync without copying account tokens. Local history is separate: if users want future plain `ccs` and `ccs ck` sessions to resume from the same account lane, run `ccs auth default ck` after backing up the current native lane with `ccs auth backup default`. + `context_group` normalization and validation: - trim + lowercase + collapse internal whitespace to `-` diff --git a/docs/session-sharing-technical-analysis.md b/docs/session-sharing-technical-analysis.md index 551f6b42..a160cccb 100644 --- a/docs/session-sharing-technical-analysis.md +++ b/docs/session-sharing-technical-analysis.md @@ -1,6 +1,6 @@ # Session Sharing Technical Analysis -Last Updated: 2026-04-04 +Last Updated: 2026-05-05 ## Summary @@ -12,6 +12,38 @@ This is implemented as a context policy per account: - `shared` + `standard` (default): account workspace context is linked to a shared context group - `shared` + `deeper` (advanced opt-in): account also shares continuity artifacts +## Recommended Two-Account Route + +Use `ccs auth` account profiles when you want two real Claude accounts and want to choose which one runs each session: + +```bash +ccs auth create work +ccs auth create personal + +ccs work +ccs personal +``` + +This keeps usage and credentials isolated. Each account owns its own Claude config directory, login state, and `.anthropic` credentials. + +Basic Claude settings are shared for non-bare account profiles: + +```text +~/.ccs/instances//settings.json + -> ~/.ccs/shared/settings.json + -> ~/.claude/settings.json +``` + +This is for ordinary Claude Code settings, hooks, commands, skills, agents, and plugins. It is not token sharing. `ccs auth show ` reports the current `Settings`, `History`, and `Plain ccs` lanes so users can see whether settings and resume history are aligned. + +Only opt in to shared history when both accounts should see the same local continuity: + +```bash +ccs auth create work2 --share-context --context-group daily --deeper-continuity +``` + +For existing accounts, use Dashboard -> Accounts -> Sync on both accounts, set both to `shared`, and use the same `History Sync Group`. Use `deeper` only when users expect stronger local handoff beyond project context. + ## 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. @@ -93,14 +125,24 @@ continuity: default: work ``` +Example with an existing `ck` account: + +```bash +ccs auth show ck +ccs auth backup default +ccs auth default ck +``` + +`ccs auth default ck` makes future plain `ccs` sessions use the `ck` account lane, so future `ccs` and `ccs ck` resume from the same local inventory. It does not automatically import old native `~/.claude/projects` history into `ck`; keep using `ccs -r` for the old native lane until you intentionally migrate that local history. + ## User Workflows ### New account with shared context ```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 +ccs auth create backup --share-context --context-group sprint-a +ccs auth create backup2 --share-context --context-group sprint-a --deeper-continuity ``` ### Existing account diff --git a/src/auth/account-profile-diagnostics.ts b/src/auth/account-profile-diagnostics.ts new file mode 100644 index 00000000..9ae8d13d --- /dev/null +++ b/src/auth/account-profile-diagnostics.ts @@ -0,0 +1,140 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { DEFAULT_ACCOUNT_CONTEXT_GROUP, type AccountContextPolicy } from './account-context'; +import { getCcsDir } from '../config/config-loader-facade'; +import { getDefaultClaudeConfigDir } from '../utils/claude-config-path'; + +export type SettingsSyncState = 'shared' | 'profile-local' | 'missing' | 'unknown'; + +export interface AccountSettingsSyncSummary { + state: SettingsSyncState; + profile_settings_path: string; + shared_settings_path: string; + root_settings_path: string; + description: string; +} + +export interface AccountHistorySummary { + project_count: number; + session_count: number; + projects_path: string; + projects_shared: boolean; + deeper_artifacts_shared: boolean; +} + +function safeRealpath(targetPath: string): string | null { + try { + return fs.realpathSync(targetPath); + } catch { + return null; + } +} + +function countTopLevelDirectories(targetPath: string): number { + try { + return fs + .readdirSync(targetPath, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()).length; + } catch { + return 0; + } +} + +function countJsonFiles(targetPath: string): number { + try { + return fs.readdirSync(targetPath).filter((entry) => entry.endsWith('.json')).length; + } catch { + return 0; + } +} + +function resolvesTo(targetPath: string, expectedPath: string): boolean { + const realTarget = safeRealpath(targetPath); + const realExpected = safeRealpath(expectedPath); + return !!realTarget && !!realExpected && realTarget === realExpected; +} + +export function describeSettingsSync( + instancePath: string, + options: { bare?: boolean } = {} +): AccountSettingsSyncSummary { + const rootSettingsPath = path.join(getDefaultClaudeConfigDir(), 'settings.json'); + const sharedSettingsPath = path.join(getCcsDir(), 'shared', 'settings.json'); + const profileSettingsPath = path.join(instancePath, 'settings.json'); + + if (options.bare) { + if (!fs.existsSync(profileSettingsPath)) { + return { + state: 'missing', + profile_settings_path: profileSettingsPath, + shared_settings_path: sharedSettingsPath, + root_settings_path: rootSettingsPath, + description: 'missing (bare profile; no local settings.json yet)', + }; + } + + return { + state: 'profile-local', + profile_settings_path: profileSettingsPath, + shared_settings_path: sharedSettingsPath, + root_settings_path: rootSettingsPath, + description: 'profile-local (bare profile; not linked to ~/.claude/settings.json)', + }; + } + + if (!fs.existsSync(profileSettingsPath)) { + return { + state: 'missing', + profile_settings_path: profileSettingsPath, + shared_settings_path: sharedSettingsPath, + root_settings_path: rootSettingsPath, + description: 'missing (run or repair this profile to recreate settings link)', + }; + } + + if (resolvesTo(profileSettingsPath, rootSettingsPath)) { + return { + state: 'shared', + profile_settings_path: profileSettingsPath, + shared_settings_path: sharedSettingsPath, + root_settings_path: rootSettingsPath, + description: 'shared with ~/.claude/settings.json', + }; + } + + return { + state: 'unknown', + profile_settings_path: profileSettingsPath, + shared_settings_path: sharedSettingsPath, + root_settings_path: rootSettingsPath, + description: 'not linked to ~/.claude/settings.json', + }; +} + +export function summarizeAccountHistory( + instancePath: string, + policy: AccountContextPolicy +): AccountHistorySummary { + const projectsPath = path.join(instancePath, 'projects'); + const sessionEnvPath = path.join(instancePath, 'session-env'); + const group = policy.group || DEFAULT_ACCOUNT_CONTEXT_GROUP; + const sharedGroupRoot = path.join(getCcsDir(), 'shared', 'context-groups', group); + const sharedProjectsPath = path.join(sharedGroupRoot, 'projects'); + const deeperArtifacts = ['session-env', 'file-history', 'shell-snapshots', 'todos']; + + return { + project_count: countTopLevelDirectories(projectsPath), + session_count: countJsonFiles(sessionEnvPath), + projects_path: projectsPath, + projects_shared: policy.mode === 'shared' && resolvesTo(projectsPath, sharedProjectsPath), + deeper_artifacts_shared: + policy.mode === 'shared' && + policy.continuityMode === 'deeper' && + deeperArtifacts.every((artifact) => + resolvesTo( + path.join(instancePath, artifact), + path.join(sharedGroupRoot, 'continuity', artifact) + ) + ), + }; +} diff --git a/src/auth/auth-commands.ts b/src/auth/auth-commands.ts index 6f757e1a..aef02f4c 100644 --- a/src/auth/auth-commands.ts +++ b/src/auth/auth-commands.ts @@ -81,18 +81,25 @@ class AuthCommands { ); console.log(''); console.log(subheader('Examples')); - console.log(` ${dim('# Create & login to work profile')}`); + console.log(` ${dim('# Create two isolated accounts and choose one explicitly at runtime')}`); console.log(` ${color('ccs auth create work', 'command')}`); + console.log(` ${color('ccs auth create personal', 'command')}`); + console.log(` ${color('ccs work "review code"', 'command')}`); + console.log(` ${color('ccs personal "write tests"', 'command')}`); console.log(''); - console.log(` ${dim('# Create account with shared project context (default group)')}`); + console.log( + ` ${dim('# Optional: share local project history while credentials stay isolated')}` + ); console.log(` ${color('ccs auth create work2 --share-context', 'command')}`); console.log(''); console.log(` ${dim('# Share context only within a specific group')}`); - console.log(` ${color('ccs auth create backup --context-group sprint-a', 'command')}`); + console.log( + ` ${color('ccs auth create backup --share-context --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')}` + ` ${color('ccs auth create backup --share-context --context-group sprint-a --deeper-continuity', 'command')}` ); console.log(''); console.log(` ${dim('# Create clean profile without shared commands/skills/agents')}`); @@ -113,9 +120,6 @@ class AuthCommands { console.log(` ${dim('# List all profiles')}`); console.log(` ${color('ccs auth list', 'command')}`); console.log(''); - console.log(` ${dim('# Use work profile')}`); - console.log(` ${color('ccs work "review code"', 'command')}`); - console.log(''); console.log(subheader('Options')); console.log( ` ${color('--force', 'command')} Allow overwriting existing profile (create)` @@ -147,14 +151,24 @@ class AuthCommands { ` By default, ${color('ccs', 'command')} uses Claude CLI defaults from ~/.claude/` ); console.log( - ` Use ${color('ccs auth default ', 'command')} to change the default profile.` + ` Recommended two-account route: create ${color('work', 'command')} and ${color('personal', 'command')}, then run the profile you want.` ); console.log( - ` Account profiles stay isolated unless you opt in with ${color('--share-context', 'command')}.` + ` Use ${color('ccs auth default ', 'command')} to change the default profile.` + ); + console.log(` Account logins, tokens, and .anthropic stay isolated for every profile.`); + console.log( + ` Non-bare account profiles share basic ${color('settings.json', 'path')} with ${color('~/.claude/settings.json', 'path')}; ${color('ccs auth show ', 'command')} shows the link state.` + ); + console.log( + ` History sync is opt-in: both accounts need shared mode and the same ${color('context_group', 'path')}.` ); console.log( ` ${color('--deeper-continuity', 'command')} requires shared mode and syncs session-env/file-history/todos/shell-snapshots.` ); + console.log( + ` To make future plain ${color('ccs', 'command')} resume with an account, set ${color('ccs auth default ', 'command')}; back up the current native lane first with ${color('ccs auth backup default', 'command')}.` + ); 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 f071e001..df45122d 100644 --- a/src/auth/commands/create-command.ts +++ b/src/auth/commands/create-command.ts @@ -280,7 +280,9 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise `Profile: ${profileName}\n` + `Instance: ${instancePath}\n` + `Type: account\n` + - `Context: ${formatAccountContextPolicy(contextPolicy)}` + + `Context: ${formatAccountContextPolicy(contextPolicy)}\n` + + `Tokens: isolated per account\n` + + `Settings: ${effectiveBare ? 'profile-local (bare)' : 'shared with ~/.claude/settings.json'}` + (effectiveBare ? '\nMode: bare (no shared symlinks)' : ''), 'Profile Created' ) @@ -289,6 +291,14 @@ export async function handleCreate(ctx: CommandContext, args: string[]): Promise console.log(header('Usage')); console.log(` ${color(`ccs ${profileName} "your prompt here"`, 'command')}`); console.log(''); + console.log( + 'To keep two accounts separate, create another account and run either profile by name:' + ); + console.log(` ${color('ccs auth create personal', 'command')}`); + console.log( + ` ${color(`ccs ${profileName}`, 'command')} / ${color('ccs personal', 'command')}` + ); + console.log(''); console.log( warnBox( `Running the command below will SWITCH your default\n` + diff --git a/src/auth/commands/show-command.ts b/src/auth/commands/show-command.ts index 63aab594..54ca56de 100644 --- a/src/auth/commands/show-command.ts +++ b/src/auth/commands/show-command.ts @@ -8,10 +8,18 @@ import * as fs from 'fs'; import * as path from 'path'; import { initUI, header, color, fail, table } from '../../utils/ui'; import { resolveAccountContextPolicy, formatAccountContextPolicy } from '../account-context'; +import { describeSettingsSync, summarizeAccountHistory } from '../account-profile-diagnostics'; +import { resolveConfiguredPlainCcsResumeLane } from '../resume-lane-diagnostics'; import { exitWithError } from '../../errors'; import { ExitCode } from '../../errors/exit-codes'; import { CommandContext, ProfileOutput, parseArgs } from './types'; +function formatHistorySummary(history: ReturnType): string { + const scope = history.projects_shared ? 'shared projects' : 'profile-local projects'; + const deeper = history.deeper_artifacts_shared ? ', deeper artifacts shared' : ''; + return `${scope}: ${history.project_count} project(s), ${history.session_count} session env file(s)${deeper}`; +} + /** * Handle the show command */ @@ -37,6 +45,11 @@ export async function handleShow(ctx: CommandContext, args: string[]): Promise null); + const plainCcsUsesThisAccount = + !!plainCcsLane && path.resolve(plainCcsLane.configDir) === path.resolve(instancePath); // Count sessions let sessionCount = 0; @@ -63,6 +76,24 @@ export async function handleShow(ctx: CommandContext, args: string[]): Promise { + it('keeps the help copy explicit about account choice and credential isolation', () => { + const source = fs.readFileSync( + path.join(process.cwd(), 'src', 'auth', 'auth-commands.ts'), + 'utf8' + ); + + expect(source).toContain('Create two isolated accounts and choose one explicitly at runtime'); + expect(source).toContain('ccs auth create work'); + expect(source).toContain('ccs auth create personal'); + expect(source).toContain('Account logins, tokens, and .anthropic stay isolated'); + expect(source).toContain('settings.json'); + expect(source).toContain('ccs auth show '); + expect(source).toContain('History sync is opt-in'); + expect(source).toContain('ccs auth backup default'); + }); +}); diff --git a/tests/unit/auth/account-profile-diagnostics.test.ts b/tests/unit/auth/account-profile-diagnostics.test.ts new file mode 100644 index 00000000..63eda681 --- /dev/null +++ b/tests/unit/auth/account-profile-diagnostics.test.ts @@ -0,0 +1,102 @@ +import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { + describeSettingsSync, + summarizeAccountHistory, +} from '../../../src/auth/account-profile-diagnostics'; + +describe('account profile diagnostics', () => { + let tempHome = ''; + let originalCcsHome: string | undefined; + + beforeEach(() => { + tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-account-diagnostics-')); + originalCcsHome = process.env.CCS_HOME; + process.env.CCS_HOME = tempHome; + }); + + afterEach(() => { + if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome; + else delete process.env.CCS_HOME; + + if (tempHome && fs.existsSync(tempHome)) { + fs.rmSync(tempHome, { recursive: true, force: true }); + } + }); + + function seedSharedSettings(instancePath: string): void { + const claudeDir = path.join(tempHome, '.claude'); + const sharedDir = path.join(tempHome, '.ccs', 'shared'); + fs.mkdirSync(claudeDir, { recursive: true }); + fs.mkdirSync(sharedDir, { recursive: true }); + fs.mkdirSync(instancePath, { recursive: true }); + fs.writeFileSync(path.join(claudeDir, 'settings.json'), '{}\n'); + fs.symlinkSync(path.join(claudeDir, 'settings.json'), path.join(sharedDir, 'settings.json')); + fs.symlinkSync(path.join(sharedDir, 'settings.json'), path.join(instancePath, 'settings.json')); + } + + it('reports non-bare account settings as shared with root Claude settings', () => { + const instancePath = path.join(tempHome, '.ccs', 'instances', 'ck'); + seedSharedSettings(instancePath); + + const summary = describeSettingsSync(instancePath); + + expect(summary.state).toBe('shared'); + expect(summary.description).toBe('shared with ~/.claude/settings.json'); + expect(summary.root_settings_path).toBe(path.join(tempHome, '.claude', 'settings.json')); + }); + + it('reports bare account settings as profile-local', () => { + const instancePath = path.join(tempHome, '.ccs', 'instances', 'bare'); + fs.mkdirSync(instancePath, { recursive: true }); + fs.writeFileSync(path.join(instancePath, 'settings.json'), '{}\n'); + + const summary = describeSettingsSync(instancePath, { bare: true }); + + expect(summary.state).toBe('profile-local'); + expect(summary.description).toContain('bare profile'); + }); + + it('reports bare account settings as missing when the local file is absent', () => { + const instancePath = path.join(tempHome, '.ccs', 'instances', 'bare'); + + const summary = describeSettingsSync(instancePath, { bare: true }); + + expect(summary.state).toBe('missing'); + expect(summary.description).toContain('bare profile'); + expect(summary.profile_settings_path).toBe(path.join(instancePath, 'settings.json')); + }); + + it('summarizes shared project and deeper continuity lane state', () => { + const instancePath = path.join(tempHome, '.ccs', 'instances', 'ck'); + const groupRoot = path.join(tempHome, '.ccs', 'shared', 'context-groups', 'default'); + const projectsPath = path.join(groupRoot, 'projects'); + const sessionEnvPath = path.join(groupRoot, 'continuity', 'session-env'); + fs.mkdirSync(projectsPath, { recursive: true }); + fs.mkdirSync(sessionEnvPath, { recursive: true }); + fs.mkdirSync(path.join(projectsPath, 'project-a')); + fs.mkdirSync(path.join(projectsPath, 'project-b')); + fs.writeFileSync(path.join(sessionEnvPath, 'session-a.json'), '{}\n'); + fs.mkdirSync(instancePath, { recursive: true }); + fs.symlinkSync(projectsPath, path.join(instancePath, 'projects'), 'dir'); + + for (const artifact of ['session-env', 'file-history', 'shell-snapshots', 'todos']) { + const sharedArtifactPath = path.join(groupRoot, 'continuity', artifact); + fs.mkdirSync(sharedArtifactPath, { recursive: true }); + fs.symlinkSync(sharedArtifactPath, path.join(instancePath, artifact), 'dir'); + } + + const summary = summarizeAccountHistory(instancePath, { + mode: 'shared', + group: 'default', + continuityMode: 'deeper', + }); + + expect(summary.project_count).toBe(2); + expect(summary.session_count).toBe(1); + expect(summary.projects_shared).toBe(true); + expect(summary.deeper_artifacts_shared).toBe(true); + }); +}); diff --git a/ui/src/components/account/account-route-guide-card.tsx b/ui/src/components/account/account-route-guide-card.tsx new file mode 100644 index 00000000..ccba55ae --- /dev/null +++ b/ui/src/components/account/account-route-guide-card.tsx @@ -0,0 +1,126 @@ +import { Link2, Settings2, ShieldCheck, Terminal, UserRoundCheck, Waves } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { CopyButton } from '@/components/ui/copy-button'; + +interface AccountRouteGuideCardProps { + totalAccounts: number; + isolatedCount: number; + sharedPeerGroups: string[]; + deeperReadyGroups: string[]; +} + +type RouteStatus = 'empty' | 'isolated' | 'shared' | 'deeper' | 'mixed'; + +export function AccountRouteGuideCard({ + totalAccounts, + isolatedCount, + sharedPeerGroups, + deeperReadyGroups, +}: AccountRouteGuideCardProps) { + const { t } = useTranslation(); + const recommendedGroup = deeperReadyGroups[0] || sharedPeerGroups[0] || 'daily'; + const status: RouteStatus = + totalAccounts < 2 + ? 'empty' + : deeperReadyGroups.length > 0 + ? 'deeper' + : sharedPeerGroups.length > 0 + ? 'shared' + : totalAccounts >= 2 && isolatedCount === totalAccounts + ? 'isolated' + : totalAccounts === 0 + ? 'empty' + : 'mixed'; + const syncCommand = `ccs auth create work2 --share-context --context-group ${recommendedGroup} --deeper-continuity`; + + return ( + + +
+
+ {t('accountRouteGuide.title')} + {t('accountRouteGuide.description')} +
+ + {t(`accountRouteGuide.status.${status}`)} + +
+
+ +
+
+
+ + {t('accountRouteGuide.cards.isolated.title')} +
+

+ {t('accountRouteGuide.cards.isolated.desc')} +

+
+
+
+ + {t('accountRouteGuide.cards.select.title')} +
+

+ {t('accountRouteGuide.cards.select.desc')} +

+
+
+
+ + {t('accountRouteGuide.cards.settings.title')} +
+

+ {t('accountRouteGuide.cards.settings.desc')} +

+
+
+
+ + {t('accountRouteGuide.cards.sync.title')} +
+

+ {t('accountRouteGuide.cards.sync.desc')} +

+
+
+ +
+
+
+ + {t('accountRouteGuide.commands.isolated')} +
+ {['ccs auth create work', 'ccs auth create personal', 'ccs work', 'ccs personal'].map( + (command) => ( +
+ {command} + +
+ ) + )} +
+
+
+ + {t('accountRouteGuide.commands.sync')} +
+
+ {syncCommand} + +
+

+ {t('accountRouteGuide.commands.syncDesc', { group: recommendedGroup })} +

+
+
+
+
+ ); +} diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts index 7c25d987..c0a05780 100644 --- a/ui/src/lib/i18n.ts +++ b/ui/src/lib/i18n.ts @@ -253,6 +253,42 @@ const resources = { copied: 'Copied!', copyCommand: 'Copy Command', }, + accountRouteGuide: { + title: 'Recommended two-account route', + description: + 'Use separate account profiles for token isolation. Turn on history sync only when both accounts should see the same local continuity.', + status: { + empty: 'create accounts', + isolated: 'isolated ready', + shared: 'shared project context', + deeper: 'deeper sync ready', + mixed: 'mixed setup', + }, + cards: { + isolated: { + title: 'Tokens stay separate', + desc: 'Each ccs auth account owns its own Claude config, login, and credentials. Sync settings never copy tokens.', + }, + select: { + title: 'You choose the account', + desc: 'Run ccs work or ccs personal for explicit usage. Set Default only when plain ccs should follow one account.', + }, + settings: { + title: 'Settings follow root', + desc: 'Non-bare accounts link settings.json through CCS shared state to ~/.claude/settings.json. ccs auth show confirms it.', + }, + sync: { + title: 'History sync is opt-in', + desc: 'Both accounts need Shared mode and the same group. Deeper sync adds extra local continuity artifacts.', + }, + }, + commands: { + isolated: 'Isolated account usage', + sync: 'Optional shared history', + syncDesc: + 'Use the same group, such as "{{group}}", on both accounts when you want them to share local history.', + }, + }, cliproxyModelCategory: { google: 'Google (Gemini)', openai: 'OpenAI (GPT)', @@ -2819,6 +2855,41 @@ const resources = { copied: '已复制!', copyCommand: '复制命令', }, + accountRouteGuide: { + title: '推荐的双账号路线', + description: + '使用独立 account profile 保持 token 隔离。只有当两个账号应该看到同一份本地连续性时,才开启历史同步。', + status: { + empty: '创建账号', + isolated: '隔离已就绪', + shared: '共享项目上下文', + deeper: '更深同步已就绪', + mixed: '混合设置', + }, + cards: { + isolated: { + title: 'Token 仍然隔离', + desc: '每个 ccs auth 账号都有自己的 Claude 配置、登录和凭据。同步设置不会复制 token。', + }, + select: { + title: '你选择使用哪个账号', + desc: '运行 ccs work 或 ccs personal 来明确选择账号。只有当普通 ccs 应跟随某个账号时才设置默认。', + }, + settings: { + title: '设置跟随根目录', + desc: '非 bare 账号会通过 CCS shared state 将 settings.json 链接到 ~/.claude/settings.json。ccs auth show 会确认状态。', + }, + sync: { + title: '历史同步需要主动开启', + desc: '两个账号都需要使用 Shared 模式和同一分组。更深同步会加入额外的本地连续性文件。', + }, + }, + commands: { + isolated: '隔离账号用法', + sync: '可选共享历史', + syncDesc: '当希望两个账号共享本地历史时,在两个账号上使用同一分组,例如 "{{group}}"。', + }, + }, cliproxyModelCategory: { google: 'Google(Gemini)', openai: 'OpenAI(GPT)', @@ -5282,6 +5353,42 @@ const resources = { copied: 'Đã sao chép!', copyCommand: 'Sao chép lệnh', }, + accountRouteGuide: { + title: 'Lộ trình hai tài khoản khuyến nghị', + description: + 'Dùng account profile riêng để giữ token tách biệt. Chỉ bật history sync khi cả hai tài khoản cần thấy cùng continuity cục bộ.', + status: { + empty: 'tạo tài khoản', + isolated: 'đã sẵn sàng tách biệt', + shared: 'chia sẻ context project', + deeper: 'deeper sync sẵn sàng', + mixed: 'thiết lập hỗn hợp', + }, + cards: { + isolated: { + title: 'Token vẫn tách biệt', + desc: 'Mỗi tài khoản ccs auth có Claude config, login và credential riêng. Cài đặt sync không bao giờ copy token.', + }, + select: { + title: 'Bạn chọn tài khoản cần dùng', + desc: 'Chạy ccs work hoặc ccs personal để chọn rõ tài khoản. Chỉ đặt Default khi plain ccs nên đi theo một tài khoản.', + }, + settings: { + title: 'Settings đi theo root', + desc: 'Tài khoản không bare link settings.json qua CCS shared state tới ~/.claude/settings.json. ccs auth show sẽ xác nhận.', + }, + sync: { + title: 'History sync là tùy chọn', + desc: 'Cả hai tài khoản cần Shared mode và cùng group. Deeper sync thêm các artifact continuity cục bộ.', + }, + }, + commands: { + isolated: 'Dùng tài khoản tách biệt', + sync: 'Chia sẻ lịch sử tùy chọn', + syncDesc: + 'Dùng cùng group, ví dụ "{{group}}", trên cả hai tài khoản khi muốn chia sẻ lịch sử cục bộ.', + }, + }, cliproxyModelCategory: { google: 'Google (Gemini)', openai: 'OpenAI (GPT)', @@ -7848,6 +7955,42 @@ const resources = { copied: 'コピーしました!', copyCommand: 'コマンドをコピー', }, + accountRouteGuide: { + title: '推奨される 2 アカウント構成', + description: + 'トークンを分離するため、別々の account profile を使います。両方のアカウントで同じローカル継続性を見たい場合だけ履歴同期を有効にします。', + status: { + empty: 'アカウントを作成', + isolated: '分離準備済み', + shared: 'プロジェクト文脈を共有', + deeper: '拡張同期準備済み', + mixed: '混在設定', + }, + cards: { + isolated: { + title: 'トークンは分離されたまま', + desc: '各 ccs auth アカウントは専用の Claude 設定、ログイン、認証情報を持ちます。同期設定でトークンはコピーされません。', + }, + select: { + title: '使うアカウントを選ぶ', + desc: 'ccs work または ccs personal を実行して明示的に選びます。通常の ccs を特定アカウントに合わせたい場合だけ既定を設定します。', + }, + settings: { + title: '設定はルートに追従', + desc: '非 bare アカウントは CCS shared state 経由で settings.json を ~/.claude/settings.json にリンクします。ccs auth show で確認できます。', + }, + sync: { + title: '履歴同期は任意', + desc: '両方のアカウントで共有モードと同じグループが必要です。拡張同期では追加のローカル継続性ファイルも共有します。', + }, + }, + commands: { + isolated: '分離アカウントの使用', + sync: '任意の共有履歴', + syncDesc: + 'ローカル履歴を共有したい場合は、両方のアカウントで "{{group}}" など同じグループを使ってください。', + }, + }, cliproxyModelCategory: { google: 'Google (Gemini)', openai: 'OpenAI (GPT)', diff --git a/ui/src/pages/accounts.tsx b/ui/src/pages/accounts.tsx index 2f86ad2e..2e729551 100644 --- a/ui/src/pages/accounts.tsx +++ b/ui/src/pages/accounts.tsx @@ -7,6 +7,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { AlertTriangle, ArrowRight, Plus, Users, Zap } from 'lucide-react'; import { AccountsTable } from '@/components/account/accounts-table'; +import { AccountRouteGuideCard } from '@/components/account/account-route-guide-card'; import { ContinuityOverview } from '@/components/account/continuity-overview'; import { CreateAuthProfileDialog } from '@/components/account/create-auth-profile-dialog'; import { CopyButton } from '@/components/ui/copy-button'; @@ -151,10 +152,11 @@ export function AccountsPage() {
- ccs auth create work --context-group sprint-a --deeper-continuity + ccs auth create work --share-context --context-group sprint-a + --deeper-continuity
@@ -205,6 +207,13 @@ export function AccountsPage() { plainCcsLane={plainCcsLane} /> + + {t('accountsPage.accountMatrix')} @@ -283,6 +292,13 @@ export function AccountsPage() { plainCcsLane={plainCcsLane} /> + + {t('accountsPage.accountMatrix')} diff --git a/ui/tests/unit/components/account/account-route-guide-card.test.tsx b/ui/tests/unit/components/account/account-route-guide-card.test.tsx new file mode 100644 index 00000000..aa83201a --- /dev/null +++ b/ui/tests/unit/components/account/account-route-guide-card.test.tsx @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest'; +import i18n from '@/lib/i18n'; +import { AccountRouteGuideCard } from '@/components/account/account-route-guide-card'; +import { render, screen } from '@tests/setup/test-utils'; + +describe('AccountRouteGuideCard', () => { + it('explains explicit two-account usage while tokens stay isolated', async () => { + await i18n.changeLanguage('en'); + + render( + + ); + + expect(screen.getByText('Recommended two-account route')).toBeInTheDocument(); + expect(screen.getByText('isolated ready')).toBeInTheDocument(); + expect(screen.getByText('Tokens stay separate')).toBeInTheDocument(); + expect(screen.getByText('Settings follow root')).toBeInTheDocument(); + expect(screen.getByText('ccs auth create work')).toBeInTheDocument(); + expect(screen.getByText('ccs auth create personal')).toBeInTheDocument(); + expect(screen.getByText('ccs work')).toBeInTheDocument(); + expect(screen.getByText('ccs personal')).toBeInTheDocument(); + }); + + it('uses the active deeper-ready group in optional sync guidance', async () => { + await i18n.changeLanguage('en'); + + render( + + ); + + expect(screen.getByText('deeper sync ready')).toBeInTheDocument(); + expect( + screen.getByText( + 'ccs auth create work2 --share-context --context-group sprint-a --deeper-continuity' + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + 'Use the same group, such as "sprint-a", on both accounts when you want them to share local history.' + ) + ).toBeInTheDocument(); + }); +});