mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
feat(auth): add resume lane diagnostics and recovery
- add runtime-aware resume lane diagnostics and auth backup flows - warn when account resume uses a different plain ccs continuity lane - surface lane mismatch guidance in the accounts dashboard, docs, and tests
This commit is contained in:
@@ -522,6 +522,29 @@ accounts:
|
||||
|
||||
Shared context with `standard` depth links project workspace data. `deeper` depth links additional continuity artifacts. Credentials remain isolated per account.
|
||||
|
||||
Resume is lane-scoped:
|
||||
|
||||
- plain `ccs -r` resumes the lane that plain `ccs` currently uses
|
||||
- `ccs <account> -r` resumes that account's lane only
|
||||
- those lanes can differ, even when an account is `shared + deeper`
|
||||
|
||||
If you do most of your work with plain `ccs` and want future resumes to line up with an auth account:
|
||||
|
||||
```bash
|
||||
ccs auth default work
|
||||
```
|
||||
|
||||
If you need to protect local continuity files before changing account sync settings:
|
||||
|
||||
```bash
|
||||
ccs auth backup work
|
||||
ccs auth backup default
|
||||
```
|
||||
|
||||
- `ccs auth backup work` backs up the local continuity lane for that auth account
|
||||
- `ccs auth backup default` backs up the lane plain `ccs` would use right now
|
||||
- this backs up local continuity artifacts only; Claude-hosted resume behavior still depends on upstream state
|
||||
|
||||
#### Cross-Profile Continuity Inheritance (Claude Target)
|
||||
|
||||
You can map non-account profiles (API, CLIProxy, Copilot, or `default`) to reuse continuity artifacts from an account profile:
|
||||
@@ -532,6 +555,7 @@ continuity:
|
||||
glm: pro
|
||||
gemini: pro
|
||||
copilot: pro
|
||||
default: pro
|
||||
```
|
||||
|
||||
With this config, `ccs glm`, `ccs gemini`, and `ccs copilot` run with `pro`'s `CLAUDE_CONFIG_DIR` continuity context while keeping each profile's own provider credentials/settings.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Session Sharing Technical Analysis
|
||||
|
||||
Last Updated: 2026-02-26
|
||||
Last Updated: 2026-04-04
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -69,6 +69,30 @@ Behavior:
|
||||
- Reuses `CLAUDE_CONFIG_DIR` from mapped account profile after normal account context policy resolution
|
||||
- Invalid/missing mapped accounts are skipped safely
|
||||
|
||||
### Resume Lane Note
|
||||
|
||||
Resume follows the active `CLAUDE_CONFIG_DIR`, not just the continuity group:
|
||||
|
||||
- plain `ccs -r` resumes the lane plain `ccs` is using right now
|
||||
- `ccs <account> -r` resumes only that account lane
|
||||
- those two commands can point at different continuity inventories
|
||||
|
||||
That means `shared + deeper` on an account does **not** automatically make old plain-`ccs` resume history appear inside `ccs <account> -r`.
|
||||
|
||||
If you want future plain `ccs` sessions to use an account lane, either:
|
||||
|
||||
```bash
|
||||
ccs auth default work
|
||||
```
|
||||
|
||||
or map the default profile explicitly:
|
||||
|
||||
```yaml
|
||||
continuity:
|
||||
inherit_from_account:
|
||||
default: work
|
||||
```
|
||||
|
||||
## User Workflows
|
||||
|
||||
### New account with shared context
|
||||
@@ -88,6 +112,19 @@ ccs auth create backup2 --context-group sprint-a --deeper-continuity
|
||||
|
||||
No account recreation required for this workflow.
|
||||
|
||||
### Backup Before Changing Sync
|
||||
|
||||
CCS can back up local continuity artifacts before you change settings:
|
||||
|
||||
```bash
|
||||
ccs auth backup work
|
||||
ccs auth backup default
|
||||
```
|
||||
|
||||
- `ccs auth backup work` backs up the selected account lane
|
||||
- `ccs auth backup default` backs up the lane plain `ccs` would use right now
|
||||
- this is a local continuity backup, not a guaranteed export of all upstream Claude-hosted resume state
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- Shared context is local filesystem sharing. It does not bypass remote provider permission models.
|
||||
|
||||
@@ -21,6 +21,7 @@ import packageJson from '../../package.json';
|
||||
import {
|
||||
type CommandContext,
|
||||
handleCreate,
|
||||
handleBackup,
|
||||
handleList,
|
||||
handleShow,
|
||||
handleRemove,
|
||||
@@ -68,6 +69,9 @@ class AuthCommands {
|
||||
console.log('');
|
||||
console.log(subheader('Commands'));
|
||||
console.log(` ${color('create <profile>', 'command')} Create new profile and login`);
|
||||
console.log(
|
||||
` ${color('backup <profile>', 'command')} Backup local continuity for an account`
|
||||
);
|
||||
console.log(` ${color('list', 'command')} List all saved profiles`);
|
||||
console.log(` ${color('show <profile>', 'command')} Show profile details`);
|
||||
console.log(` ${color('remove <profile>', 'command')} Remove saved profile`);
|
||||
@@ -97,6 +101,12 @@ class AuthCommands {
|
||||
console.log(` ${dim('# Set work as default')}`);
|
||||
console.log(` ${color('ccs auth default work', 'command')}`);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Backup the local continuity lane for an account or plain ccs')}`);
|
||||
console.log(` ${color('ccs auth backup work', 'command')}`);
|
||||
console.log(
|
||||
` ${color('ccs auth backup default', 'command')} ${dim('# backup plain ccs lane')}`
|
||||
);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Restore original CCS behavior')}`);
|
||||
console.log(` ${color('ccs auth reset-default', 'command')}`);
|
||||
console.log('');
|
||||
@@ -162,6 +172,10 @@ class AuthCommands {
|
||||
return handleCreate(this.getContext(), args);
|
||||
}
|
||||
|
||||
async handleBackup(args: string[]): Promise<void> {
|
||||
return handleBackup(this.getContext(), args);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all profiles - delegates to list-command.ts
|
||||
*/
|
||||
@@ -227,6 +241,10 @@ class AuthCommands {
|
||||
await this.handleList(commandArgs);
|
||||
break;
|
||||
|
||||
case 'backup':
|
||||
await this.handleBackup(commandArgs);
|
||||
break;
|
||||
|
||||
case 'show':
|
||||
await this.handleShow(commandArgs);
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { initUI, color, dim, fail, info, ok } from '../../utils/ui';
|
||||
import { exitWithError } from '../../errors';
|
||||
import { ExitCode } from '../../errors/exit-codes';
|
||||
import {
|
||||
createTimestampStamp,
|
||||
getAuthBackupRoot,
|
||||
getContinuityArtifactNames,
|
||||
resolveRuntimePlainCcsResumeLane,
|
||||
} from '../resume-lane-diagnostics';
|
||||
import { isAccountContextMetadata, resolveAccountContextPolicy } from '../account-context';
|
||||
import { CommandContext, parseArgs } from './types';
|
||||
|
||||
interface BackupManifest {
|
||||
target: string;
|
||||
source_config_dir: string;
|
||||
created_at: string;
|
||||
copied: string[];
|
||||
skipped: string[];
|
||||
}
|
||||
|
||||
function copyDirectoryIfPresent(sourcePath: string, targetPath: string): boolean {
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fs.cpSync(sourcePath, targetPath, {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
force: false,
|
||||
errorOnExist: false,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function handleBackup(ctx: CommandContext, args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
const { profileName, json } = parseArgs(args);
|
||||
|
||||
if (!profileName) {
|
||||
console.log(fail('Profile name is required'));
|
||||
console.log('');
|
||||
console.log(`Usage: ${color('ccs auth backup <profile|default> [--json]', 'command')}`);
|
||||
exitWithError('Profile name is required', ExitCode.PROFILE_ERROR);
|
||||
}
|
||||
|
||||
try {
|
||||
let sourceConfigDir = '';
|
||||
let backupLabel = profileName;
|
||||
let artifactNames: string[] = [];
|
||||
|
||||
if (profileName === 'default') {
|
||||
const lane = await resolveRuntimePlainCcsResumeLane();
|
||||
sourceConfigDir = lane.configDir;
|
||||
backupLabel = lane.accountName ? `default-${lane.accountName}` : 'default';
|
||||
artifactNames = getContinuityArtifactNames('default');
|
||||
} else {
|
||||
const profiles = ctx.registry.getAllProfilesMerged();
|
||||
const profile = profiles[profileName];
|
||||
if (!profile || profile.type !== 'account') {
|
||||
exitWithError(
|
||||
`Backup supports auth accounts or "default". Not found: ${profileName}`,
|
||||
ExitCode.PROFILE_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
const contextPolicy = resolveAccountContextPolicy(
|
||||
isAccountContextMetadata(profile) ? profile : undefined
|
||||
);
|
||||
sourceConfigDir = await ctx.instanceMgr.ensureInstance(profileName, contextPolicy, {
|
||||
bare: profile.bare === true,
|
||||
});
|
||||
artifactNames = getContinuityArtifactNames('account');
|
||||
}
|
||||
|
||||
const backupRoot = path.join(getAuthBackupRoot(), backupLabel, createTimestampStamp());
|
||||
fs.mkdirSync(backupRoot, { recursive: true, mode: 0o700 });
|
||||
|
||||
const copied: string[] = [];
|
||||
const skipped: string[] = [];
|
||||
|
||||
for (const artifactName of artifactNames) {
|
||||
const sourcePath = path.join(sourceConfigDir, artifactName);
|
||||
const targetPath = path.join(backupRoot, artifactName);
|
||||
if (copyDirectoryIfPresent(sourcePath, targetPath)) {
|
||||
copied.push(artifactName);
|
||||
} else {
|
||||
skipped.push(artifactName);
|
||||
}
|
||||
}
|
||||
|
||||
const manifest: BackupManifest = {
|
||||
target: profileName,
|
||||
source_config_dir: sourceConfigDir,
|
||||
created_at: new Date().toISOString(),
|
||||
copied,
|
||||
skipped,
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(backupRoot, 'manifest.json'),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
{
|
||||
encoding: 'utf8',
|
||||
mode: 0o600,
|
||||
}
|
||||
);
|
||||
|
||||
if (json) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
target: profileName,
|
||||
backup_path: backupRoot,
|
||||
copied,
|
||||
skipped,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(ok(`Continuity backup created: ${backupRoot}`));
|
||||
console.log('');
|
||||
console.log(info(`Source lane: ${sourceConfigDir}`));
|
||||
console.log(` ${dim(`Copied: ${copied.length > 0 ? copied.join(', ') : 'none'}`)}`);
|
||||
if (skipped.length > 0) {
|
||||
console.log(` ${dim(`Skipped: ${skipped.join(', ')}`)}`);
|
||||
}
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
exitWithError(
|
||||
`Failed to create continuity backup: ${(error as Error).message}`,
|
||||
ExitCode.GENERAL_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ export {
|
||||
|
||||
// Command handlers
|
||||
export { handleCreate } from './create-command';
|
||||
export { handleBackup } from './backup-command';
|
||||
export { handleList } from './list-command';
|
||||
export { handleShow } from './show-command';
|
||||
export { handleRemove } from './remove-command';
|
||||
|
||||
@@ -100,6 +100,19 @@ function resolveMappedAccount(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveConfiguredContinuitySourceAccount(
|
||||
profileName: string,
|
||||
profileType: ProfileType
|
||||
): string | undefined {
|
||||
const inheritFromAccount = getContinuityInheritanceMap();
|
||||
return (
|
||||
resolveMappedAccount(profileName, profileType, inheritFromAccount) ??
|
||||
(!isUnifiedMode()
|
||||
? resolveMappedAccount(profileName, profileType, loadLegacyContinuityInheritanceMap())
|
||||
: undefined)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve optional cross-profile continuity inheritance.
|
||||
*
|
||||
@@ -115,16 +128,10 @@ export async function resolveProfileContinuityInheritance(
|
||||
return {};
|
||||
}
|
||||
|
||||
const inheritFromAccount = getContinuityInheritanceMap();
|
||||
const sourceAccount =
|
||||
resolveMappedAccount(input.profileName, input.profileType, inheritFromAccount) ??
|
||||
(!isUnifiedMode()
|
||||
? resolveMappedAccount(
|
||||
input.profileName,
|
||||
input.profileType,
|
||||
loadLegacyContinuityInheritanceMap()
|
||||
)
|
||||
: undefined);
|
||||
const sourceAccount = resolveConfiguredContinuitySourceAccount(
|
||||
input.profileName,
|
||||
input.profileType
|
||||
);
|
||||
if (!sourceAccount) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -462,6 +462,14 @@ class ProfileDetector {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Public wrapper for diagnostics and tooling that need to inspect how
|
||||
* plain `ccs` resolves without duplicating the default-profile logic.
|
||||
*/
|
||||
resolveDefaultProfileResult(): ProfileDetectionResult {
|
||||
return this.resolveDefaultProfile();
|
||||
}
|
||||
|
||||
/**
|
||||
* List available profiles (for error messages)
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { getDefaultClaudeConfigDir } from '../utils/claude-config-path';
|
||||
import { getCcsDir } from '../utils/config-manager';
|
||||
import InstanceManager from '../management/instance-manager';
|
||||
import ProfileDetector from './profile-detector';
|
||||
import { resolveConfiguredContinuitySourceAccount } from './profile-continuity-inheritance';
|
||||
import type { ProfileType } from '../types/profile';
|
||||
|
||||
export type ResumeLaneKind =
|
||||
| 'native'
|
||||
| 'account-default'
|
||||
| 'account-inherited'
|
||||
| 'profile-default'
|
||||
| 'ambient';
|
||||
|
||||
export interface ResumeLaneSummary {
|
||||
kind: ResumeLaneKind;
|
||||
label: string;
|
||||
configDir: string;
|
||||
accountName?: string;
|
||||
profileName?: string;
|
||||
projectCount: number;
|
||||
}
|
||||
|
||||
export interface ResumeFlagIntent {
|
||||
implicit: boolean;
|
||||
explicitSessionId?: string;
|
||||
}
|
||||
|
||||
function countTopLevelProjectDirs(projectsDir: string): number {
|
||||
try {
|
||||
return fs
|
||||
.readdirSync(projectsDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory()).length;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveNativeLaneSummary(
|
||||
kind: ResumeLaneKind = 'native',
|
||||
profileName?: string
|
||||
): ResumeLaneSummary {
|
||||
const configDir = getDefaultClaudeConfigDir();
|
||||
const label =
|
||||
kind === 'profile-default' && profileName
|
||||
? `profile "${profileName}" via native Claude lane`
|
||||
: 'native Claude lane';
|
||||
|
||||
return {
|
||||
kind,
|
||||
label,
|
||||
configDir,
|
||||
profileName,
|
||||
projectCount: countTopLevelProjectDirs(path.join(configDir, 'projects')),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAccountLaneSummary(
|
||||
kind: Extract<ResumeLaneKind, 'account-default' | 'account-inherited'>,
|
||||
accountName: string
|
||||
): ResumeLaneSummary {
|
||||
const instanceMgr = new InstanceManager();
|
||||
const configDir = instanceMgr.getInstancePath(accountName);
|
||||
|
||||
return {
|
||||
kind,
|
||||
label:
|
||||
kind === 'account-inherited'
|
||||
? `plain ccs inherits from account "${accountName}"`
|
||||
: `plain ccs defaults to account "${accountName}"`,
|
||||
configDir,
|
||||
accountName,
|
||||
projectCount: countTopLevelProjectDirs(path.join(configDir, 'projects')),
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveConfiguredPlainCcsResumeLane(): Promise<ResumeLaneSummary> {
|
||||
const detector = new ProfileDetector();
|
||||
const defaultProfile = detector.resolveDefaultProfileResult();
|
||||
|
||||
if (defaultProfile.type === 'account') {
|
||||
return resolveAccountLaneSummary('account-default', defaultProfile.name);
|
||||
}
|
||||
|
||||
const inheritedAccount = resolveConfiguredContinuitySourceAccount(
|
||||
defaultProfile.name,
|
||||
defaultProfile.type
|
||||
);
|
||||
if (inheritedAccount) {
|
||||
return resolveAccountLaneSummary('account-inherited', inheritedAccount);
|
||||
}
|
||||
|
||||
if (defaultProfile.type !== 'default') {
|
||||
return resolveNativeLaneSummary('profile-default', defaultProfile.name);
|
||||
}
|
||||
|
||||
return resolveNativeLaneSummary();
|
||||
}
|
||||
|
||||
export async function resolveRuntimePlainCcsResumeLane(): Promise<ResumeLaneSummary> {
|
||||
if (process.env.CLAUDE_CONFIG_DIR) {
|
||||
const configDir = path.resolve(process.env.CLAUDE_CONFIG_DIR);
|
||||
return {
|
||||
kind: 'ambient',
|
||||
label: 'current shell CLAUDE_CONFIG_DIR',
|
||||
configDir,
|
||||
projectCount: countTopLevelProjectDirs(path.join(configDir, 'projects')),
|
||||
};
|
||||
}
|
||||
|
||||
return resolveConfiguredPlainCcsResumeLane();
|
||||
}
|
||||
|
||||
export function parseResumeFlagIntent(args: string[]): ResumeFlagIntent | null {
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === '-r') {
|
||||
return { implicit: true };
|
||||
}
|
||||
|
||||
if (arg === '--resume') {
|
||||
const next = args[index + 1];
|
||||
if (next && !next.startsWith('-')) {
|
||||
return { implicit: false, explicitSessionId: next };
|
||||
}
|
||||
return { implicit: true };
|
||||
}
|
||||
|
||||
if (arg.startsWith('--resume=')) {
|
||||
const sessionId = arg.slice('--resume='.length).trim();
|
||||
return sessionId ? { implicit: false, explicitSessionId: sessionId } : { implicit: true };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getAuthBackupRoot(): string {
|
||||
return path.join(getCcsDir(), 'backups', 'auth-continuity');
|
||||
}
|
||||
|
||||
export function createTimestampStamp(date = new Date()): string {
|
||||
const yyyy = date.getFullYear();
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(date.getDate()).padStart(2, '0');
|
||||
const hh = String(date.getHours()).padStart(2, '0');
|
||||
const min = String(date.getMinutes()).padStart(2, '0');
|
||||
const ss = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${yyyy}${mm}${dd}_${hh}${min}${ss}`;
|
||||
}
|
||||
|
||||
export function getContinuityArtifactNames(profileType: ProfileType): string[] {
|
||||
const sharedItems = ['projects'];
|
||||
const deeperItems = ['session-env', 'file-history', 'shell-snapshots', 'todos'];
|
||||
|
||||
if (profileType === 'default' || profileType === 'account') {
|
||||
return [...sharedItems, ...deeperItems];
|
||||
}
|
||||
|
||||
return sharedItems;
|
||||
}
|
||||
+43
@@ -1,6 +1,7 @@
|
||||
import './utils/fetch-proxy-setup';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { detectClaudeCli } from './utils/claude-detector';
|
||||
import {
|
||||
getSettingsPath,
|
||||
@@ -64,6 +65,10 @@ import { tryHandleRootCommand } from './commands/root-command-router';
|
||||
// Import extracted utility functions
|
||||
import { execClaude } from './utils/shell-executor';
|
||||
import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation';
|
||||
import {
|
||||
parseResumeFlagIntent,
|
||||
resolveRuntimePlainCcsResumeLane,
|
||||
} from './auth/resume-lane-diagnostics';
|
||||
|
||||
// Import target adapter system
|
||||
import {
|
||||
@@ -259,6 +264,43 @@ function shouldPassthroughNativeCodexFlagCommand(args: string[]): boolean {
|
||||
return getNativeCodexPassthroughArgs(args) !== null;
|
||||
}
|
||||
|
||||
async function maybeWarnAboutResumeLaneMismatch(
|
||||
profileName: string,
|
||||
accountConfigDir: string,
|
||||
args: string[]
|
||||
): Promise<void> {
|
||||
const resumeIntent = parseResumeFlagIntent(args);
|
||||
if (!resumeIntent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const plainLane = await resolveRuntimePlainCcsResumeLane();
|
||||
if (path.resolve(plainLane.configDir) === path.resolve(accountConfigDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(
|
||||
warn(
|
||||
`Resume for account "${profileName}" will search that account lane, not the current plain ccs lane.`
|
||||
)
|
||||
);
|
||||
console.error(info(` Account lane: ${accountConfigDir}`));
|
||||
console.error(info(` Plain ccs lane: ${plainLane.label} (${plainLane.configDir})`));
|
||||
if (resumeIntent.explicitSessionId) {
|
||||
console.error(
|
||||
info(
|
||||
` This explicit session ID may have been created in a different lane, so Claude may not find it here.`
|
||||
)
|
||||
);
|
||||
}
|
||||
console.error(info(` Recover the original lane first: ccs -r`));
|
||||
console.error(info(` Back it up before changing setup: ccs auth backup default`));
|
||||
console.error(
|
||||
info(` For future work, align plain ccs with this account: ccs auth default ${profileName}`)
|
||||
);
|
||||
console.error('');
|
||||
}
|
||||
|
||||
function getNativeCodexPassthroughArgs(args: string[]): string[] | null {
|
||||
const targetArgs = stripTargetFlag(args);
|
||||
if (resolveTargetType(args) !== 'codex' || targetArgs.length === 0) {
|
||||
@@ -1269,6 +1311,7 @@ async function main(): Promise<void> {
|
||||
CCS_WEBSEARCH_SKIP: '1',
|
||||
CCS_IMAGE_ANALYSIS_SKIP: '1',
|
||||
};
|
||||
await maybeWarnAboutResumeLaneMismatch(profileInfo.name, instancePath, remainingArgs);
|
||||
const launchArgs = resolveNativeClaudeLaunchArgs(remainingArgs, 'account', instancePath);
|
||||
execClaude(claudeCli, launchArgs, envVars);
|
||||
} else {
|
||||
|
||||
@@ -215,6 +215,7 @@ export const ROOT_COMMAND_FLAGS = [
|
||||
] as const;
|
||||
export const AUTH_SUBCOMMANDS = [
|
||||
'create',
|
||||
'backup',
|
||||
'list',
|
||||
'show',
|
||||
'remove',
|
||||
|
||||
@@ -94,6 +94,8 @@ function getSuggestionsForCommand(tokensBeforeCurrent: string[]): CompletionSugg
|
||||
return completeSubcommands(ROOT_HELP_TOPICS.map((topic) => topic.name));
|
||||
case 'auth':
|
||||
if (!subcommand) return completeSubcommands(AUTH_SUBCOMMANDS);
|
||||
if (subcommand === 'backup')
|
||||
return completeSubcommands(['default', ...getProfileNames('accounts')], ['--json']);
|
||||
if (subcommand === 'show')
|
||||
return completeSubcommands(getProfileNames('accounts'), ['--json']);
|
||||
if (subcommand === 'remove')
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
type MergedAccountEntry,
|
||||
} from './account-route-helpers';
|
||||
import type { AccountConfig } from '../../config/unified-config-types';
|
||||
import { resolveConfiguredPlainCcsResumeLane } from '../../auth/resume-lane-diagnostics';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -59,7 +60,7 @@ function hasAuthAccount(name: string): boolean {
|
||||
/**
|
||||
* GET /api/accounts - List accounts from both profiles.json and config.yaml
|
||||
*/
|
||||
router.get('/', (_req: Request, res: Response): void => {
|
||||
router.get('/', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const registry = createProfileRegistry();
|
||||
|
||||
@@ -147,8 +148,19 @@ router.get('/', (_req: Request, res: Response): void => {
|
||||
|
||||
// Get default from unified config first, fallback to legacy
|
||||
const defaultProfile = registry.getDefaultUnified() ?? registry.getDefaultProfile() ?? null;
|
||||
const plainCcsLane = await resolveConfiguredPlainCcsResumeLane();
|
||||
|
||||
res.json({ accounts, default: defaultProfile });
|
||||
res.json({
|
||||
accounts,
|
||||
default: defaultProfile,
|
||||
plain_ccs_lane: {
|
||||
kind: plainCcsLane.kind,
|
||||
label: plainCcsLane.label,
|
||||
account_name: plainCcsLane.accountName ?? null,
|
||||
profile_name: plainCcsLane.profileName ?? null,
|
||||
project_count: plainCcsLane.projectCount,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import ProfileRegistry from '../../../src/auth/profile-registry';
|
||||
import { InstanceManager } from '../../../src/management/instance-manager';
|
||||
import { handleBackup } from '../../../src/auth/commands/backup-command';
|
||||
|
||||
describe('auth backup command', () => {
|
||||
let tempHome = '';
|
||||
let originalCcsHome: string | undefined;
|
||||
let originalUnified: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-auth-backup-'));
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
originalUnified = process.env.CCS_UNIFIED_CONFIG;
|
||||
process.env.CCS_HOME = tempHome;
|
||||
process.env.CCS_UNIFIED_CONFIG = '1';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
|
||||
else delete process.env.CCS_HOME;
|
||||
|
||||
if (originalUnified !== undefined) process.env.CCS_UNIFIED_CONFIG = originalUnified;
|
||||
else delete process.env.CCS_UNIFIED_CONFIG;
|
||||
|
||||
if (tempHome && fs.existsSync(tempHome)) {
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function writeConfig(): void {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(ccsDir, 'config.yaml'),
|
||||
[
|
||||
'version: 12',
|
||||
'accounts:',
|
||||
' work:',
|
||||
' created: "2026-04-04T00:00:00.000Z"',
|
||||
' context_mode: shared',
|
||||
' context_group: default',
|
||||
' continuity_mode: deeper',
|
||||
'profiles: {}',
|
||||
'cliproxy:',
|
||||
' oauth_accounts: {}',
|
||||
' providers: {}',
|
||||
' variants: {}',
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
}
|
||||
|
||||
it('creates a JSON backup for an auth account continuity lane', async () => {
|
||||
writeConfig();
|
||||
const instanceMgr = new InstanceManager();
|
||||
const registry = new ProfileRegistry();
|
||||
const instancePath = await instanceMgr.ensureInstance('work', {
|
||||
mode: 'shared',
|
||||
group: 'default',
|
||||
continuityMode: 'deeper',
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.join(instancePath, 'projects', 'demo-project'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(instancePath, 'projects', 'demo-project', 'history.jsonl'),
|
||||
'{}\n',
|
||||
'utf8'
|
||||
);
|
||||
fs.writeFileSync(path.join(instancePath, 'todos', 'todo.md'), '- keep this\n', 'utf8');
|
||||
|
||||
const consoleSpy = spyOn(console, 'log').mockImplementation(() => {});
|
||||
try {
|
||||
await handleBackup(
|
||||
{
|
||||
registry,
|
||||
instanceMgr,
|
||||
version: 'test',
|
||||
},
|
||||
['work', '--json']
|
||||
);
|
||||
} finally {
|
||||
consoleSpy.mockRestore();
|
||||
}
|
||||
|
||||
const backupBase = path.join(tempHome, '.ccs', 'backups', 'auth-continuity', 'work');
|
||||
const [latestBackupDir] = fs.readdirSync(backupBase).sort().reverse();
|
||||
expect(latestBackupDir).toBeTruthy();
|
||||
const backupPath = path.join(backupBase, latestBackupDir as string);
|
||||
expect(fs.existsSync(path.join(backupPath, 'manifest.json'))).toBe(true);
|
||||
expect(
|
||||
fs.existsSync(path.join(backupPath, 'projects', 'demo-project', 'history.jsonl'))
|
||||
).toBe(true);
|
||||
expect(fs.existsSync(path.join(backupPath, 'todos', 'todo.md'))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
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 {
|
||||
parseResumeFlagIntent,
|
||||
resolveConfiguredPlainCcsResumeLane,
|
||||
} from '../../../src/auth/resume-lane-diagnostics';
|
||||
|
||||
describe('resume lane diagnostics', () => {
|
||||
let tempHome = '';
|
||||
let originalCcsHome: string | undefined;
|
||||
let originalUnified: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-resume-lane-'));
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
originalUnified = process.env.CCS_UNIFIED_CONFIG;
|
||||
process.env.CCS_HOME = tempHome;
|
||||
process.env.CCS_UNIFIED_CONFIG = '1';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
|
||||
else delete process.env.CCS_HOME;
|
||||
|
||||
if (originalUnified !== undefined) process.env.CCS_UNIFIED_CONFIG = originalUnified;
|
||||
else delete process.env.CCS_UNIFIED_CONFIG;
|
||||
|
||||
if (tempHome && fs.existsSync(tempHome)) {
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function writeConfig(lines: string[]): void {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(ccsDir, 'config.yaml'), `${lines.join('\n')}\n`, 'utf8');
|
||||
}
|
||||
|
||||
it('parses implicit and explicit resume flags', () => {
|
||||
expect(parseResumeFlagIntent(['-r'])).toEqual({ implicit: true });
|
||||
expect(parseResumeFlagIntent(['--resume'])).toEqual({ implicit: true });
|
||||
expect(parseResumeFlagIntent(['--resume', 'abc123'])).toEqual({
|
||||
implicit: false,
|
||||
explicitSessionId: 'abc123',
|
||||
});
|
||||
expect(parseResumeFlagIntent(['--resume=xyz'])).toEqual({
|
||||
implicit: false,
|
||||
explicitSessionId: 'xyz',
|
||||
});
|
||||
expect(parseResumeFlagIntent(['hello'])).toBeNull();
|
||||
});
|
||||
|
||||
it('defaults to the native lane when no account/default mapping exists', async () => {
|
||||
writeConfig([
|
||||
'version: 12',
|
||||
'accounts: {}',
|
||||
'profiles: {}',
|
||||
'cliproxy:',
|
||||
' oauth_accounts: {}',
|
||||
' providers: {}',
|
||||
' variants: {}',
|
||||
]);
|
||||
|
||||
const lane = await resolveConfiguredPlainCcsResumeLane();
|
||||
expect(lane.kind).toBe('native');
|
||||
expect(lane.configDir).toBe(path.join(tempHome, '.claude'));
|
||||
});
|
||||
|
||||
it('resolves the plain ccs lane to a default account when configured', async () => {
|
||||
writeConfig([
|
||||
'version: 12',
|
||||
'default: work',
|
||||
'accounts:',
|
||||
' work:',
|
||||
' created: "2026-04-04T00:00:00.000Z"',
|
||||
' context_mode: shared',
|
||||
' context_group: default',
|
||||
' continuity_mode: deeper',
|
||||
'profiles: {}',
|
||||
'cliproxy:',
|
||||
' oauth_accounts: {}',
|
||||
' providers: {}',
|
||||
' variants: {}',
|
||||
]);
|
||||
|
||||
const lane = await resolveConfiguredPlainCcsResumeLane();
|
||||
expect(lane.kind).toBe('account-default');
|
||||
expect(lane.accountName).toBe('work');
|
||||
expect(lane.configDir).toBe(path.join(tempHome, '.ccs', 'instances', 'work'));
|
||||
});
|
||||
|
||||
it('resolves the plain ccs lane to inherited account continuity when configured', async () => {
|
||||
writeConfig([
|
||||
'version: 12',
|
||||
'accounts:',
|
||||
' work:',
|
||||
' created: "2026-04-04T00:00:00.000Z"',
|
||||
' context_mode: shared',
|
||||
' context_group: default',
|
||||
' continuity_mode: deeper',
|
||||
'profiles: {}',
|
||||
'continuity:',
|
||||
' inherit_from_account:',
|
||||
' default: work',
|
||||
'cliproxy:',
|
||||
' oauth_accounts: {}',
|
||||
' providers: {}',
|
||||
' variants: {}',
|
||||
]);
|
||||
|
||||
const lane = await resolveConfiguredPlainCcsResumeLane();
|
||||
expect(lane.kind).toBe('account-inherited');
|
||||
expect(lane.accountName).toBe('work');
|
||||
expect(lane.configDir).toBe(path.join(tempHome, '.ccs', 'instances', 'work'));
|
||||
});
|
||||
|
||||
it('resolves a non-account default profile through continuity inheritance', async () => {
|
||||
writeConfig([
|
||||
'version: 12',
|
||||
'default: glm',
|
||||
'accounts:',
|
||||
' work:',
|
||||
' created: "2026-04-04T00:00:00.000Z"',
|
||||
'profiles:',
|
||||
' glm:',
|
||||
' type: api',
|
||||
' settings: ~/.ccs/glm.settings.json',
|
||||
'continuity:',
|
||||
' inherit_from_account:',
|
||||
' glm: work',
|
||||
'cliproxy:',
|
||||
' oauth_accounts: {}',
|
||||
' providers: {}',
|
||||
' variants: {}',
|
||||
]);
|
||||
|
||||
const lane = await resolveConfiguredPlainCcsResumeLane();
|
||||
expect(lane.kind).toBe('account-inherited');
|
||||
expect(lane.accountName).toBe('work');
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,9 @@ function suggestionValues(
|
||||
current = '',
|
||||
shell: 'bash' | 'fish' | 'powershell' | 'zsh' = 'bash'
|
||||
): string[] {
|
||||
return getCompletionSuggestions({ tokensBeforeCurrent, current, shell }).map((entry) => entry.value);
|
||||
return getCompletionSuggestions({ tokensBeforeCurrent, current, shell }).map(
|
||||
(entry) => entry.value
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -100,7 +102,9 @@ describe('completion backend', () => {
|
||||
|
||||
test('suggests lifecycle subcommands for api', () => {
|
||||
const values = suggestionValues(['api']);
|
||||
expect(values).toEqual(expect.arrayContaining(['create', 'discover', 'copy', 'export', 'import', 'remove']));
|
||||
expect(values).toEqual(
|
||||
expect.arrayContaining(['create', 'discover', 'copy', 'export', 'import', 'remove'])
|
||||
);
|
||||
});
|
||||
|
||||
test('suggests account profiles for auth show', () => {
|
||||
@@ -109,6 +113,13 @@ describe('completion backend', () => {
|
||||
expect(values).toContain('--json');
|
||||
});
|
||||
|
||||
test('suggests default lane and account profiles for auth backup', () => {
|
||||
const values = suggestionValues(['auth', 'backup']);
|
||||
expect(values).toContain('default');
|
||||
expect(values).toContain('work');
|
||||
expect(values).toContain('--json');
|
||||
});
|
||||
|
||||
test('suggests cliproxy variants for variant-scoped commands', () => {
|
||||
const values = suggestionValues(['cliproxy', 'edit']);
|
||||
expect(values).toContain('my-codex');
|
||||
@@ -116,7 +127,9 @@ describe('completion backend', () => {
|
||||
|
||||
test('suggests env format values after the format flag', () => {
|
||||
const values = suggestionValues(['env', '--format']);
|
||||
expect(values).toEqual(expect.arrayContaining(['openai', 'anthropic', 'raw', 'claude-extension']));
|
||||
expect(values).toEqual(
|
||||
expect.arrayContaining(['openai', 'anthropic', 'raw', 'claude-extension'])
|
||||
);
|
||||
});
|
||||
|
||||
test('includes live doctor and cliproxy flags from the shared catalog', () => {
|
||||
|
||||
@@ -164,6 +164,104 @@ describe('web-server account-routes context normalization', () => {
|
||||
expect(work?.continuity_inferred).toBe(true);
|
||||
});
|
||||
|
||||
it('reports the plain ccs lane as native when no account default or inheritance exists', async () => {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(ccsDir, 'config.yaml'),
|
||||
[
|
||||
'version: 12',
|
||||
'accounts:',
|
||||
' work:',
|
||||
' created: "2026-02-01T00:00:00.000Z"',
|
||||
'profiles: {}',
|
||||
'cliproxy:',
|
||||
' oauth_accounts: {}',
|
||||
' providers: {}',
|
||||
' variants: {}',
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const payload = await getJson<{
|
||||
plain_ccs_lane?: {
|
||||
kind: string;
|
||||
account_name?: string | null;
|
||||
};
|
||||
}>(baseUrl, '/api/accounts');
|
||||
|
||||
expect(payload.plain_ccs_lane?.kind).toBe('native');
|
||||
expect(payload.plain_ccs_lane?.account_name).toBeNull();
|
||||
});
|
||||
|
||||
it('reports the plain ccs lane as an account when default profile is an auth account', async () => {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(ccsDir, 'config.yaml'),
|
||||
[
|
||||
'version: 12',
|
||||
'default: work',
|
||||
'accounts:',
|
||||
' work:',
|
||||
' created: "2026-02-01T00:00:00.000Z"',
|
||||
' context_mode: shared',
|
||||
' context_group: default',
|
||||
' continuity_mode: deeper',
|
||||
'profiles: {}',
|
||||
'cliproxy:',
|
||||
' oauth_accounts: {}',
|
||||
' providers: {}',
|
||||
' variants: {}',
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const payload = await getJson<{
|
||||
plain_ccs_lane?: {
|
||||
kind: string;
|
||||
account_name?: string | null;
|
||||
};
|
||||
}>(baseUrl, '/api/accounts');
|
||||
|
||||
expect(payload.plain_ccs_lane?.kind).toBe('account-default');
|
||||
expect(payload.plain_ccs_lane?.account_name).toBe('work');
|
||||
});
|
||||
|
||||
it('does not initialize an account instance as a side effect of listing accounts', async () => {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(ccsDir, 'config.yaml'),
|
||||
[
|
||||
'version: 12',
|
||||
'default: work',
|
||||
'accounts:',
|
||||
' work:',
|
||||
' created: "2026-02-01T00:00:00.000Z"',
|
||||
' context_mode: shared',
|
||||
' context_group: default',
|
||||
' continuity_mode: deeper',
|
||||
'profiles: {}',
|
||||
'cliproxy:',
|
||||
' oauth_accounts: {}',
|
||||
' providers: {}',
|
||||
' variants: {}',
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const instancePath = path.join(ccsDir, 'instances', 'work');
|
||||
expect(fs.existsSync(instancePath)).toBe(false);
|
||||
|
||||
await getJson(baseUrl, '/api/accounts');
|
||||
|
||||
expect(fs.existsSync(instancePath)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not delete metadata when instance deletion fails', async () => {
|
||||
const ccsDir = path.join(tempHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
|
||||
@@ -35,14 +35,21 @@ import {
|
||||
useUpdateAccountContext,
|
||||
} from '@/hooks/use-accounts';
|
||||
import type { AuthAccountRow, SharedGroupSummary } from '@/lib/account-continuity';
|
||||
import type { PlainCcsLane } from '@/lib/api-client';
|
||||
|
||||
interface AccountsTableProps {
|
||||
data: AuthAccountRow[];
|
||||
defaultAccount: string | null;
|
||||
groupSummaries: SharedGroupSummary[];
|
||||
plainCcsLane: PlainCcsLane | null;
|
||||
}
|
||||
|
||||
export function AccountsTable({ data, defaultAccount, groupSummaries }: AccountsTableProps) {
|
||||
export function AccountsTable({
|
||||
data,
|
||||
defaultAccount,
|
||||
groupSummaries,
|
||||
plainCcsLane,
|
||||
}: AccountsTableProps) {
|
||||
const { t } = useTranslation();
|
||||
const setDefaultMutation = useSetDefaultAccount();
|
||||
const deleteMutation = useDeleteAccount();
|
||||
@@ -114,7 +121,7 @@ export function AccountsTable({ data, defaultAccount, groupSummaries }: Accounts
|
||||
variant="outline"
|
||||
className={`font-mono text-[10px] uppercase px-1.5 py-0 border ${isDeeper ? 'text-indigo-700 border-indigo-300/60 bg-indigo-50/50 dark:text-indigo-300 dark:border-indigo-900/40 dark:bg-indigo-900/20' : 'text-emerald-700 border-emerald-300/60 bg-emerald-50/50 dark:text-emerald-300 dark:border-emerald-900/40 dark:bg-emerald-900/20'}`}
|
||||
>
|
||||
{isDeeper ? 'Deeper' : 'Shared'}
|
||||
{isDeeper ? t('accountsTable.badges.deeper') : t('accountsTable.badges.shared')}
|
||||
</Badge>
|
||||
<span className="text-xs font-semibold text-foreground/80">{group}</span>
|
||||
</div>
|
||||
@@ -136,7 +143,7 @@ export function AccountsTable({ data, defaultAccount, groupSummaries }: Accounts
|
||||
variant="outline"
|
||||
className="text-amber-700 border-amber-300/60 bg-amber-50/50 dark:text-amber-400 dark:border-amber-900/40 dark:bg-amber-900/20 font-mono text-[10px] uppercase px-1.5 py-0"
|
||||
>
|
||||
Legacy
|
||||
{t('accountsTable.badges.legacy')}
|
||||
</Badge>
|
||||
<p className="text-[10px] text-amber-700/80 dark:text-amber-400/80 whitespace-nowrap">
|
||||
{t('accountsTable.legacyReview')}
|
||||
@@ -151,7 +158,7 @@ export function AccountsTable({ data, defaultAccount, groupSummaries }: Accounts
|
||||
variant="secondary"
|
||||
className="font-mono text-[10px] uppercase px-1.5 py-0 text-muted-foreground bg-muted/60 border-transparent shadow-none"
|
||||
>
|
||||
Isolated
|
||||
{t('accountsTable.badges.isolated')}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
@@ -321,6 +328,7 @@ export function AccountsTable({ data, defaultAccount, groupSummaries }: Accounts
|
||||
<EditAccountContextDialog
|
||||
account={contextTarget}
|
||||
groupSummaries={groupSummaries}
|
||||
plainCcsLane={plainCcsLane}
|
||||
onClose={() => setContextTarget(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -12,10 +12,13 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { CopyButton } from '@/components/ui/copy-button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import type { PlainCcsLane } from '@/lib/api-client';
|
||||
|
||||
interface ContinuityOverviewProps {
|
||||
totalAccounts: number;
|
||||
primaryAccountName?: string | null;
|
||||
isolatedCount: number;
|
||||
sharedStandardCount: number;
|
||||
deeperSharedCount: number;
|
||||
@@ -27,6 +30,7 @@ interface ContinuityOverviewProps {
|
||||
deeperReadyGroups: string[];
|
||||
legacyTargetCount: number;
|
||||
cliproxyCount: number;
|
||||
plainCcsLane: PlainCcsLane | null;
|
||||
}
|
||||
|
||||
type ReadinessState =
|
||||
@@ -45,6 +49,7 @@ const InfoTooltip = ({ titleKey, descKey }: { titleKey: string; descKey: string
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={t(titleKey)}
|
||||
className="h-5 w-5 rounded-full hover:bg-muted text-muted-foreground/70 transition-colors"
|
||||
>
|
||||
<Info className="h-3 w-3" />
|
||||
@@ -64,6 +69,7 @@ const InfoTooltip = ({ titleKey, descKey }: { titleKey: string; descKey: string
|
||||
|
||||
export function ContinuityOverview({
|
||||
totalAccounts,
|
||||
primaryAccountName,
|
||||
isolatedCount,
|
||||
sharedStandardCount,
|
||||
deeperSharedCount,
|
||||
@@ -74,8 +80,26 @@ export function ContinuityOverview({
|
||||
deeperReadyGroups,
|
||||
legacyTargetCount,
|
||||
cliproxyCount,
|
||||
plainCcsLane,
|
||||
}: ContinuityOverviewProps) {
|
||||
const { t } = useTranslation();
|
||||
const laneLabel = plainCcsLane
|
||||
? plainCcsLane.kind === 'native'
|
||||
? t('continuityOverview.lane.native')
|
||||
: plainCcsLane.kind === 'account-default'
|
||||
? t('continuityOverview.lane.accountDefault', {
|
||||
name: plainCcsLane.account_name || '',
|
||||
})
|
||||
: plainCcsLane.kind === 'account-inherited'
|
||||
? t('continuityOverview.lane.accountInherited', {
|
||||
name: plainCcsLane.account_name || '',
|
||||
})
|
||||
: plainCcsLane.kind === 'profile-default'
|
||||
? t('continuityOverview.lane.profileDefault', {
|
||||
name: plainCcsLane.profile_name || 'default',
|
||||
})
|
||||
: plainCcsLane.label
|
||||
: '';
|
||||
|
||||
const readiness: ReadinessState =
|
||||
totalAccounts < 2
|
||||
@@ -110,6 +134,11 @@ export function ContinuityOverview({
|
||||
};
|
||||
|
||||
const currentIcon = iconMap[readiness];
|
||||
const plainLaneUsesPrimaryAccount =
|
||||
!!primaryAccountName && plainCcsLane?.account_name === primaryAccountName;
|
||||
const showPlainLaneRecovery =
|
||||
totalAccounts > 0 && !!plainCcsLane && (!primaryAccountName || !plainLaneUsesPrimaryAccount);
|
||||
const showSharedRecoverySteps = totalAccounts > 1 && readiness !== 'ready';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -167,7 +196,7 @@ export function ContinuityOverview({
|
||||
variant="secondary"
|
||||
className="font-mono text-[11px] px-2 bg-muted/50 text-muted-foreground border-transparent"
|
||||
>
|
||||
Recommend: {highlightGroup}
|
||||
{t('continuityOverview.recommendBadge', { group: highlightGroup })}
|
||||
</Badge>
|
||||
)}
|
||||
{hasMixedState && (
|
||||
@@ -175,13 +204,70 @@ export function ContinuityOverview({
|
||||
variant="secondary"
|
||||
className="font-mono text-[11px] px-2 bg-muted/50 text-muted-foreground border-transparent"
|
||||
>
|
||||
Partial sync ({highlightGroup})
|
||||
{t('continuityOverview.partialBadge', { group: highlightGroup })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{(showPlainLaneRecovery || showSharedRecoverySteps) && (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="p-5 space-y-4">
|
||||
{showPlainLaneRecovery && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{t('continuityOverview.plainLaneTitle')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('continuityOverview.plainLaneDescription', {
|
||||
lane: laneLabel || 'plain ccs',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="rounded-md border bg-background px-2 py-2 font-mono text-[11px] flex items-start gap-2">
|
||||
<span className="flex-1 break-all">ccs -r</span>
|
||||
<CopyButton value="ccs -r" size="icon" />
|
||||
</div>
|
||||
<div className="rounded-md border bg-background px-2 py-2 font-mono text-[11px] flex items-start gap-2">
|
||||
<span className="flex-1 break-all">ccs auth backup default</span>
|
||||
<CopyButton value="ccs auth backup default" size="icon" />
|
||||
</div>
|
||||
{primaryAccountName ? (
|
||||
<div className="rounded-md border bg-background px-2 py-2 font-mono text-[11px] flex items-start gap-2">
|
||||
<span className="flex-1 break-all">
|
||||
{`ccs auth default ${primaryAccountName}`}
|
||||
</span>
|
||||
<CopyButton value={`ccs auth default ${primaryAccountName}`} size="icon" />
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('continuityOverview.setDefaultHint')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showSharedRecoverySteps && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{t('continuityReadiness.stepsTitle')}
|
||||
</p>
|
||||
<ol className="space-y-1 pl-5 text-sm text-muted-foreground">
|
||||
<li>{t('continuityReadiness.steps.syncBoth')}</li>
|
||||
<li>{t('continuityReadiness.steps.sameGroup', { group: highlightGroup })}</li>
|
||||
<li>{t('continuityReadiness.steps.enableDeeper')}</li>
|
||||
<li>{t('continuityReadiness.steps.resumeOriginal')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Horizontal Progression Chain */}
|
||||
<div className="flex flex-col md:flex-row items-center gap-3">
|
||||
<div className="flex-1 w-full flex items-center justify-between p-3.5 rounded-xl border border-blue-300/40 bg-blue-50/50 dark:border-blue-900/30 dark:bg-blue-900/10 shadow-sm transition-colors hover:bg-blue-100/40 dark:hover:bg-blue-900/20">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo, useState, type KeyboardEvent } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -14,7 +14,9 @@ import { Label } from '@/components/ui/label';
|
||||
import { Unlink, Link2, Waves, ShieldAlert, Info } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { AuthAccountRow, SharedGroupSummary } from '@/lib/account-continuity';
|
||||
import type { PlainCcsLane } from '@/lib/api-client';
|
||||
import { useUpdateAccountContext } from '@/hooks/use-accounts';
|
||||
import { CopyButton } from '@/components/ui/copy-button';
|
||||
|
||||
type ContextMode = 'isolated' | 'shared';
|
||||
type ContinuityMode = 'standard' | 'deeper';
|
||||
@@ -25,12 +27,14 @@ const CONTEXT_GROUP_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
||||
interface EditAccountContextDialogProps {
|
||||
account: AuthAccountRow;
|
||||
groupSummaries: SharedGroupSummary[];
|
||||
plainCcsLane: PlainCcsLane | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function EditAccountContextDialog({
|
||||
account,
|
||||
groupSummaries,
|
||||
plainCcsLane,
|
||||
onClose,
|
||||
}: EditAccountContextDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -73,6 +77,43 @@ export function EditAccountContextDialog({
|
||||
0
|
||||
)
|
||||
: 0;
|
||||
const plainLaneUsesThisAccount = plainCcsLane?.account_name === account.name;
|
||||
const showPlainLaneMismatch = !!plainCcsLane && !plainLaneUsesThisAccount;
|
||||
const defaultCommand = `ccs auth default ${account.name}`;
|
||||
const accountBackupCommand = `ccs auth backup ${account.name}`;
|
||||
const handleRadioKeyDown = <T extends string>(
|
||||
event: KeyboardEvent<HTMLButtonElement>,
|
||||
current: T,
|
||||
values: readonly T[],
|
||||
setValue: (value: T) => void
|
||||
) => {
|
||||
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const currentIndex = values.indexOf(current);
|
||||
const direction = event.key === 'ArrowLeft' || event.key === 'ArrowUp' ? -1 : 1;
|
||||
const nextIndex = (currentIndex + direction + values.length) % values.length;
|
||||
setValue(values[nextIndex] as T);
|
||||
};
|
||||
const laneLabel = plainCcsLane
|
||||
? plainCcsLane.kind === 'native'
|
||||
? t('continuityOverview.lane.native')
|
||||
: plainCcsLane.kind === 'account-default'
|
||||
? t('continuityOverview.lane.accountDefault', {
|
||||
name: plainCcsLane.account_name || '',
|
||||
})
|
||||
: plainCcsLane.kind === 'account-inherited'
|
||||
? t('continuityOverview.lane.accountInherited', {
|
||||
name: plainCcsLane.account_name || '',
|
||||
})
|
||||
: plainCcsLane.kind === 'profile-default'
|
||||
? t('continuityOverview.lane.profileDefault', {
|
||||
name: plainCcsLane.profile_name || 'default',
|
||||
})
|
||||
: plainCcsLane.label
|
||||
: '';
|
||||
|
||||
const handleSave = () => {
|
||||
if (!canSubmit) {
|
||||
@@ -124,7 +165,11 @@ export function EditAccountContextDialog({
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={mode === 'isolated'}
|
||||
tabIndex={mode === 'isolated' ? 0 : -1}
|
||||
onClick={() => setMode('isolated')}
|
||||
onKeyDown={(event) =>
|
||||
handleRadioKeyDown(event, mode, ['isolated', 'shared'] as const, setMode)
|
||||
}
|
||||
className={`flex-1 flex justify-center items-center gap-2 px-3 py-1.5 rounded-[10px] text-sm font-medium transition-all duration-200 overflow-hidden ${
|
||||
mode === 'isolated'
|
||||
? 'bg-background text-blue-600 dark:text-blue-400 shadow-sm ring-1 ring-border/50'
|
||||
@@ -138,7 +183,11 @@ export function EditAccountContextDialog({
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={mode === 'shared'}
|
||||
tabIndex={mode === 'shared' ? 0 : -1}
|
||||
onClick={() => setMode('shared')}
|
||||
onKeyDown={(event) =>
|
||||
handleRadioKeyDown(event, mode, ['isolated', 'shared'] as const, setMode)
|
||||
}
|
||||
className={`flex-1 flex justify-center items-center gap-2 px-3 py-1.5 rounded-[10px] text-sm font-medium transition-all duration-200 overflow-hidden ${
|
||||
mode === 'shared'
|
||||
? 'bg-background text-emerald-600 dark:text-emerald-400 shadow-sm ring-1 ring-border/50'
|
||||
@@ -191,7 +240,16 @@ export function EditAccountContextDialog({
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={continuityMode === 'standard'}
|
||||
tabIndex={continuityMode === 'standard' ? 0 : -1}
|
||||
onClick={() => setContinuityMode('standard')}
|
||||
onKeyDown={(event) =>
|
||||
handleRadioKeyDown(
|
||||
event,
|
||||
continuityMode,
|
||||
['standard', 'deeper'] as const,
|
||||
setContinuityMode
|
||||
)
|
||||
}
|
||||
className={`flex-1 flex justify-center items-center gap-2 px-3 py-1.5 rounded-[10px] text-sm font-medium transition-all duration-200 overflow-hidden ${
|
||||
continuityMode === 'standard'
|
||||
? 'bg-background text-emerald-600 dark:text-emerald-400 shadow-sm ring-1 ring-border/50'
|
||||
@@ -205,7 +263,16 @@ export function EditAccountContextDialog({
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={continuityMode === 'deeper'}
|
||||
tabIndex={continuityMode === 'deeper' ? 0 : -1}
|
||||
onClick={() => setContinuityMode('deeper')}
|
||||
onKeyDown={(event) =>
|
||||
handleRadioKeyDown(
|
||||
event,
|
||||
continuityMode,
|
||||
['standard', 'deeper'] as const,
|
||||
setContinuityMode
|
||||
)
|
||||
}
|
||||
className={`flex-1 flex justify-center items-center gap-2 px-3 py-1.5 rounded-[10px] text-sm font-medium transition-all duration-200 overflow-hidden ${
|
||||
continuityMode === 'deeper'
|
||||
? 'bg-background text-indigo-600 dark:text-indigo-400 shadow-sm ring-1 ring-border/50'
|
||||
@@ -228,6 +295,40 @@ export function EditAccountContextDialog({
|
||||
{t('editAccountContext.credentialsIsolated')}
|
||||
</p>
|
||||
|
||||
{showPlainLaneMismatch && (
|
||||
<div className="rounded-[14px] border border-amber-200 bg-amber-50/50 p-4 text-xs shadow-sm dark:border-amber-900/40 dark:bg-amber-900/10">
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium text-foreground">
|
||||
{t('continuityOverview.plainLaneTitle')}
|
||||
</p>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
{t('continuityOverview.plainLaneDescription', {
|
||||
lane: laneLabel || 'plain ccs',
|
||||
name: account.name,
|
||||
})}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<div className="rounded-md border bg-background px-2 py-2 font-mono text-[11px] flex items-start gap-2">
|
||||
<span className="flex-1 break-all">ccs -r</span>
|
||||
<CopyButton value="ccs -r" size="icon" />
|
||||
</div>
|
||||
<div className="rounded-md border bg-background px-2 py-2 font-mono text-[11px] flex items-start gap-2">
|
||||
<span className="flex-1 break-all">ccs auth backup default</span>
|
||||
<CopyButton value="ccs auth backup default" size="icon" />
|
||||
</div>
|
||||
<div className="rounded-md border bg-background px-2 py-2 font-mono text-[11px] flex items-start gap-2">
|
||||
<span className="flex-1 break-all">{accountBackupCommand}</span>
|
||||
<CopyButton value={accountBackupCommand} size="icon" />
|
||||
</div>
|
||||
<div className="rounded-md border bg-background px-2 py-2 font-mono text-[11px] flex items-start gap-2">
|
||||
<span className="flex-1 break-all">{defaultCommand}</span>
|
||||
<CopyButton value={defaultCommand} size="icon" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`rounded-[14px] border p-4 text-xs shadow-sm transition-colors ${mode === 'isolated' ? 'bg-blue-50/50 border-blue-200 dark:bg-blue-900/10 dark:border-blue-800/40' : 'bg-muted/40 border-border/60'}`}
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '@/lib/api-client';
|
||||
import type { Account } from '@/lib/api-client';
|
||||
import type { Account, PlainCcsLane } from '@/lib/api-client';
|
||||
import {
|
||||
summarizeAuthAccountContinuity,
|
||||
type AuthAccountRow,
|
||||
@@ -30,6 +30,7 @@ export interface AuthAccountsView {
|
||||
deeperReadyGroups: string[];
|
||||
sharedGroups: string[];
|
||||
groupSummaries: SharedGroupSummary[];
|
||||
plainCcsLane: PlainCcsLane | null;
|
||||
}
|
||||
|
||||
export function useAccounts() {
|
||||
@@ -61,6 +62,7 @@ export function useAccounts() {
|
||||
deeperReadyGroups: continuity.deeperReadyGroups,
|
||||
sharedGroups: continuity.sharedGroups,
|
||||
groupSummaries: continuity.groupSummaries,
|
||||
plainCcsLane: data.plain_ccs_lane ?? null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -733,6 +733,14 @@ export interface Account {
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
export interface PlainCcsLane {
|
||||
kind: 'native' | 'account-default' | 'account-inherited' | 'profile-default' | 'ambient';
|
||||
label: string;
|
||||
account_name?: string | null;
|
||||
profile_name?: string | null;
|
||||
project_count: number;
|
||||
}
|
||||
|
||||
export interface UpdateAccountContext {
|
||||
context_mode: 'isolated' | 'shared';
|
||||
context_group?: string;
|
||||
@@ -1120,7 +1128,10 @@ export const api = {
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
list: () => request<{ accounts: Account[]; default: string | null }>('/accounts'),
|
||||
list: () =>
|
||||
request<{ accounts: Account[]; default: string | null; plain_ccs_lane?: PlainCcsLane }>(
|
||||
'/accounts'
|
||||
),
|
||||
setDefault: (name: string) =>
|
||||
request('/accounts/default', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -540,6 +540,21 @@ const resources = {
|
||||
'If the original conversation is important, keep resuming it from the original account until the second account is ready.',
|
||||
},
|
||||
},
|
||||
continuityOverview: {
|
||||
plainLaneTitle: 'Plain ccs is using a different resume lane',
|
||||
plainLaneDescription:
|
||||
'Plain ccs currently resumes from {{lane}}, not this account lane. Recover the original lane first before changing sync settings.',
|
||||
setDefaultHint:
|
||||
'Use the Set Default action on the account you want plain ccs to follow in future sessions.',
|
||||
recommendBadge: 'Recommend {{group}}',
|
||||
partialBadge: 'Partial sync {{group}}',
|
||||
lane: {
|
||||
native: 'native Claude (~/.claude)',
|
||||
accountDefault: 'account {{name}}',
|
||||
accountInherited: 'account {{name}} via continuity inheritance',
|
||||
profileDefault: 'default profile {{name}} via native Claude lane',
|
||||
},
|
||||
},
|
||||
accountsTable: {
|
||||
name: 'Name',
|
||||
type: 'Type',
|
||||
@@ -573,6 +588,12 @@ const resources = {
|
||||
sameGroupPeerCount_other: '{{count}} same-group peers',
|
||||
legacyReview: 'Review and confirm this inferred default.',
|
||||
noHandoff: 'Cross-account resume stays in the original account.',
|
||||
badges: {
|
||||
shared: 'Shared',
|
||||
deeper: 'Deeper',
|
||||
legacy: 'Legacy',
|
||||
isolated: 'Isolated',
|
||||
},
|
||||
},
|
||||
addAccountDialog: {
|
||||
title: 'Add {{displayName}} Account',
|
||||
@@ -1820,6 +1841,20 @@ const resources = {
|
||||
resumeOriginal: '在第二个账号配置完成之前,重要会话仍应从原账号继续恢复。',
|
||||
},
|
||||
},
|
||||
continuityOverview: {
|
||||
plainLaneTitle: '普通 ccs 正在使用不同的续接通道',
|
||||
plainLaneDescription:
|
||||
'普通 ccs 当前会从 {{lane}} 续接,而不是这个账号通道。请先恢复原通道中的会话,再修改同步设置。',
|
||||
setDefaultHint: '如果希望以后普通 ccs 跟随某个账号,请使用该账号行上的“设为默认”。',
|
||||
recommendBadge: '建议 {{group}}',
|
||||
partialBadge: '部分同步 {{group}}',
|
||||
lane: {
|
||||
native: '原生 Claude(~/.claude)',
|
||||
accountDefault: '账号 {{name}}',
|
||||
accountInherited: '通过连续性继承使用账号 {{name}}',
|
||||
profileDefault: '默认配置 {{name}}(仍走原生 Claude 通道)',
|
||||
},
|
||||
},
|
||||
accountsTable: {
|
||||
name: '名称',
|
||||
type: '类型',
|
||||
@@ -1853,6 +1888,12 @@ const resources = {
|
||||
sameGroupPeerCount_other: '{{count}} 个同组账号',
|
||||
legacyReview: '请检查并确认这个推断出的默认状态。',
|
||||
noHandoff: '跨账号续接仍会回到原账号。',
|
||||
badges: {
|
||||
shared: '共享',
|
||||
deeper: '更深',
|
||||
legacy: '旧策略',
|
||||
isolated: '隔离',
|
||||
},
|
||||
},
|
||||
addAccountDialog: {
|
||||
title: '添加 {{displayName}} 账号',
|
||||
@@ -3118,6 +3159,21 @@ const resources = {
|
||||
'Cho đến khi tài khoản thứ hai sẵn sàng, các cuộc trò chuyện quan trọng vẫn nên được resume từ tài khoản gốc.',
|
||||
},
|
||||
},
|
||||
continuityOverview: {
|
||||
plainLaneTitle: 'Plain ccs đang dùng một lane resume khác',
|
||||
plainLaneDescription:
|
||||
'Plain ccs hiện resume từ {{lane}}, không phải lane của tài khoản này. Hãy khôi phục lane gốc trước khi đổi cài đặt sync.',
|
||||
setDefaultHint:
|
||||
'Nếu muốn plain ccs theo một tài khoản trong các phiên sau, hãy dùng hành động Set Default trên dòng tài khoản đó.',
|
||||
recommendBadge: 'Đề xuất {{group}}',
|
||||
partialBadge: 'Đồng bộ một phần {{group}}',
|
||||
lane: {
|
||||
native: 'Claude gốc (~/.claude)',
|
||||
accountDefault: 'tài khoản {{name}}',
|
||||
accountInherited: 'tài khoản {{name}} qua continuity inheritance',
|
||||
profileDefault: 'profile mặc định {{name}} qua lane Claude gốc',
|
||||
},
|
||||
},
|
||||
accountsTable: {
|
||||
name: 'Tên',
|
||||
type: 'Kiểu',
|
||||
@@ -3152,6 +3208,12 @@ const resources = {
|
||||
sameGroupPeerCount_other: '{{count}} tài khoản cùng nhóm',
|
||||
legacyReview: 'Hãy xem lại và xác nhận trạng thái suy ra này.',
|
||||
noHandoff: 'Resume xuyên tài khoản vẫn sẽ quay về tài khoản gốc.',
|
||||
badges: {
|
||||
shared: 'Chia sẻ',
|
||||
deeper: 'Sâu hơn',
|
||||
legacy: 'Kế thừa',
|
||||
isolated: 'Tách biệt',
|
||||
},
|
||||
},
|
||||
addAccountDialog: {
|
||||
title: 'Thêm tài khoản {{displayName}}',
|
||||
@@ -4454,6 +4516,21 @@ const resources = {
|
||||
'2 つ目のアカウントが整うまでは、重要な会話は元のアカウントから再開し続けてください。',
|
||||
},
|
||||
},
|
||||
continuityOverview: {
|
||||
plainLaneTitle: '通常の ccs は別の再開レーンを使っています',
|
||||
plainLaneDescription:
|
||||
'通常の ccs は現在 {{lane}} から再開しており、このアカウントのレーンではありません。同期設定を変える前に元のレーンを先に復旧してください。',
|
||||
setDefaultHint:
|
||||
'今後の通常の ccs を特定のアカウントに合わせたい場合は、その行の「既定に設定」を使ってください。',
|
||||
recommendBadge: '推奨 {{group}}',
|
||||
partialBadge: '部分同期 {{group}}',
|
||||
lane: {
|
||||
native: 'ネイティブ Claude(~/.claude)',
|
||||
accountDefault: 'アカウント {{name}}',
|
||||
accountInherited: '継続性継承によるアカウント {{name}}',
|
||||
profileDefault: '既定プロファイル {{name}}(ネイティブ Claude レーン)',
|
||||
},
|
||||
},
|
||||
accountsTable: {
|
||||
name: '名前',
|
||||
type: '種別',
|
||||
@@ -4488,6 +4565,12 @@ const resources = {
|
||||
sameGroupPeerCount_other: '{{count}} 件の同グループアカウント',
|
||||
legacyReview: 'この推定された既定状態を確認して明示的に確定してください。',
|
||||
noHandoff: 'アカウント間再開は元のアカウントに残ります。',
|
||||
badges: {
|
||||
shared: '共有',
|
||||
deeper: '拡張',
|
||||
legacy: 'レガシー',
|
||||
isolated: '分離',
|
||||
},
|
||||
},
|
||||
addAccountDialog: {
|
||||
title: '{{displayName}} アカウントを追加',
|
||||
|
||||
@@ -39,6 +39,7 @@ export function AccountsPage() {
|
||||
const deeperReadyGroups = data?.deeperReadyGroups || [];
|
||||
const sharedGroups = data?.sharedGroups || [];
|
||||
const groupSummaries = data?.groupSummaries || [];
|
||||
const plainCcsLane = data?.plainCcsLane || null;
|
||||
|
||||
const legacyTargets = authAccounts.filter(
|
||||
(account) => account.context_inferred || account.continuity_inferred
|
||||
@@ -189,6 +190,7 @@ export function AccountsPage() {
|
||||
<div className="flex-1 min-h-0 p-5 space-y-4 overflow-y-auto">
|
||||
<ContinuityOverview
|
||||
totalAccounts={authAccounts.length}
|
||||
primaryAccountName={authAccounts.length === 1 ? authAccounts[0]?.name : null}
|
||||
isolatedCount={isolatedCount}
|
||||
sharedStandardCount={sharedStandardCount}
|
||||
deeperSharedCount={deeperSharedCount}
|
||||
@@ -200,6 +202,7 @@ export function AccountsPage() {
|
||||
deeperReadyGroups={deeperReadyGroups}
|
||||
legacyTargetCount={legacyTargetCount}
|
||||
cliproxyCount={cliproxyCount}
|
||||
plainCcsLane={plainCcsLane}
|
||||
/>
|
||||
|
||||
<Card className="flex flex-col">
|
||||
@@ -217,6 +220,7 @@ export function AccountsPage() {
|
||||
data={authAccounts}
|
||||
defaultAccount={data?.default ?? null}
|
||||
groupSummaries={groupSummaries}
|
||||
plainCcsLane={plainCcsLane}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -264,6 +268,7 @@ export function AccountsPage() {
|
||||
|
||||
<ContinuityOverview
|
||||
totalAccounts={authAccounts.length}
|
||||
primaryAccountName={authAccounts.length === 1 ? authAccounts[0]?.name : null}
|
||||
isolatedCount={isolatedCount}
|
||||
sharedStandardCount={sharedStandardCount}
|
||||
deeperSharedCount={deeperSharedCount}
|
||||
@@ -275,6 +280,7 @@ export function AccountsPage() {
|
||||
deeperReadyGroups={deeperReadyGroups}
|
||||
legacyTargetCount={legacyTargetCount}
|
||||
cliproxyCount={cliproxyCount}
|
||||
plainCcsLane={plainCcsLane}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
@@ -289,6 +295,7 @@ export function AccountsPage() {
|
||||
data={authAccounts}
|
||||
defaultAccount={data?.default ?? null}
|
||||
groupSummaries={groupSummaries}
|
||||
plainCcsLane={plainCcsLane}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user