mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 06:18:55 +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.
|
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)
|
#### 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:
|
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
|
glm: pro
|
||||||
gemini: pro
|
gemini: pro
|
||||||
copilot: 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.
|
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
|
# Session Sharing Technical Analysis
|
||||||
|
|
||||||
Last Updated: 2026-02-26
|
Last Updated: 2026-04-04
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
@@ -69,6 +69,30 @@ Behavior:
|
|||||||
- Reuses `CLAUDE_CONFIG_DIR` from mapped account profile after normal account context policy resolution
|
- Reuses `CLAUDE_CONFIG_DIR` from mapped account profile after normal account context policy resolution
|
||||||
- Invalid/missing mapped accounts are skipped safely
|
- 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
|
## User Workflows
|
||||||
|
|
||||||
### New account with shared context
|
### 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.
|
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
|
## Current Limitations
|
||||||
|
|
||||||
- Shared context is local filesystem sharing. It does not bypass remote provider permission models.
|
- Shared context is local filesystem sharing. It does not bypass remote provider permission models.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import packageJson from '../../package.json';
|
|||||||
import {
|
import {
|
||||||
type CommandContext,
|
type CommandContext,
|
||||||
handleCreate,
|
handleCreate,
|
||||||
|
handleBackup,
|
||||||
handleList,
|
handleList,
|
||||||
handleShow,
|
handleShow,
|
||||||
handleRemove,
|
handleRemove,
|
||||||
@@ -68,6 +69,9 @@ class AuthCommands {
|
|||||||
console.log('');
|
console.log('');
|
||||||
console.log(subheader('Commands'));
|
console.log(subheader('Commands'));
|
||||||
console.log(` ${color('create <profile>', 'command')} Create new profile and login`);
|
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('list', 'command')} List all saved profiles`);
|
||||||
console.log(` ${color('show <profile>', 'command')} Show profile details`);
|
console.log(` ${color('show <profile>', 'command')} Show profile details`);
|
||||||
console.log(` ${color('remove <profile>', 'command')} Remove saved profile`);
|
console.log(` ${color('remove <profile>', 'command')} Remove saved profile`);
|
||||||
@@ -97,6 +101,12 @@ class AuthCommands {
|
|||||||
console.log(` ${dim('# Set work as default')}`);
|
console.log(` ${dim('# Set work as default')}`);
|
||||||
console.log(` ${color('ccs auth default work', 'command')}`);
|
console.log(` ${color('ccs auth default work', 'command')}`);
|
||||||
console.log('');
|
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(` ${dim('# Restore original CCS behavior')}`);
|
||||||
console.log(` ${color('ccs auth reset-default', 'command')}`);
|
console.log(` ${color('ccs auth reset-default', 'command')}`);
|
||||||
console.log('');
|
console.log('');
|
||||||
@@ -162,6 +172,10 @@ class AuthCommands {
|
|||||||
return handleCreate(this.getContext(), args);
|
return handleCreate(this.getContext(), args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleBackup(args: string[]): Promise<void> {
|
||||||
|
return handleBackup(this.getContext(), args);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all profiles - delegates to list-command.ts
|
* List all profiles - delegates to list-command.ts
|
||||||
*/
|
*/
|
||||||
@@ -227,6 +241,10 @@ class AuthCommands {
|
|||||||
await this.handleList(commandArgs);
|
await this.handleList(commandArgs);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'backup':
|
||||||
|
await this.handleBackup(commandArgs);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'show':
|
case 'show':
|
||||||
await this.handleShow(commandArgs);
|
await this.handleShow(commandArgs);
|
||||||
break;
|
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
|
// Command handlers
|
||||||
export { handleCreate } from './create-command';
|
export { handleCreate } from './create-command';
|
||||||
|
export { handleBackup } from './backup-command';
|
||||||
export { handleList } from './list-command';
|
export { handleList } from './list-command';
|
||||||
export { handleShow } from './show-command';
|
export { handleShow } from './show-command';
|
||||||
export { handleRemove } from './remove-command';
|
export { handleRemove } from './remove-command';
|
||||||
|
|||||||
@@ -100,6 +100,19 @@ function resolveMappedAccount(
|
|||||||
return undefined;
|
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.
|
* Resolve optional cross-profile continuity inheritance.
|
||||||
*
|
*
|
||||||
@@ -115,16 +128,10 @@ export async function resolveProfileContinuityInheritance(
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const inheritFromAccount = getContinuityInheritanceMap();
|
const sourceAccount = resolveConfiguredContinuitySourceAccount(
|
||||||
const sourceAccount =
|
input.profileName,
|
||||||
resolveMappedAccount(input.profileName, input.profileType, inheritFromAccount) ??
|
input.profileType
|
||||||
(!isUnifiedMode()
|
);
|
||||||
? resolveMappedAccount(
|
|
||||||
input.profileName,
|
|
||||||
input.profileType,
|
|
||||||
loadLegacyContinuityInheritanceMap()
|
|
||||||
)
|
|
||||||
: undefined);
|
|
||||||
if (!sourceAccount) {
|
if (!sourceAccount) {
|
||||||
return {};
|
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)
|
* 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 './utils/fetch-proxy-setup';
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
import { detectClaudeCli } from './utils/claude-detector';
|
import { detectClaudeCli } from './utils/claude-detector';
|
||||||
import {
|
import {
|
||||||
getSettingsPath,
|
getSettingsPath,
|
||||||
@@ -64,6 +65,10 @@ import { tryHandleRootCommand } from './commands/root-command-router';
|
|||||||
// Import extracted utility functions
|
// Import extracted utility functions
|
||||||
import { execClaude } from './utils/shell-executor';
|
import { execClaude } from './utils/shell-executor';
|
||||||
import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation';
|
import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation';
|
||||||
|
import {
|
||||||
|
parseResumeFlagIntent,
|
||||||
|
resolveRuntimePlainCcsResumeLane,
|
||||||
|
} from './auth/resume-lane-diagnostics';
|
||||||
|
|
||||||
// Import target adapter system
|
// Import target adapter system
|
||||||
import {
|
import {
|
||||||
@@ -259,6 +264,43 @@ function shouldPassthroughNativeCodexFlagCommand(args: string[]): boolean {
|
|||||||
return getNativeCodexPassthroughArgs(args) !== null;
|
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 {
|
function getNativeCodexPassthroughArgs(args: string[]): string[] | null {
|
||||||
const targetArgs = stripTargetFlag(args);
|
const targetArgs = stripTargetFlag(args);
|
||||||
if (resolveTargetType(args) !== 'codex' || targetArgs.length === 0) {
|
if (resolveTargetType(args) !== 'codex' || targetArgs.length === 0) {
|
||||||
@@ -1269,6 +1311,7 @@ async function main(): Promise<void> {
|
|||||||
CCS_WEBSEARCH_SKIP: '1',
|
CCS_WEBSEARCH_SKIP: '1',
|
||||||
CCS_IMAGE_ANALYSIS_SKIP: '1',
|
CCS_IMAGE_ANALYSIS_SKIP: '1',
|
||||||
};
|
};
|
||||||
|
await maybeWarnAboutResumeLaneMismatch(profileInfo.name, instancePath, remainingArgs);
|
||||||
const launchArgs = resolveNativeClaudeLaunchArgs(remainingArgs, 'account', instancePath);
|
const launchArgs = resolveNativeClaudeLaunchArgs(remainingArgs, 'account', instancePath);
|
||||||
execClaude(claudeCli, launchArgs, envVars);
|
execClaude(claudeCli, launchArgs, envVars);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ export const ROOT_COMMAND_FLAGS = [
|
|||||||
] as const;
|
] as const;
|
||||||
export const AUTH_SUBCOMMANDS = [
|
export const AUTH_SUBCOMMANDS = [
|
||||||
'create',
|
'create',
|
||||||
|
'backup',
|
||||||
'list',
|
'list',
|
||||||
'show',
|
'show',
|
||||||
'remove',
|
'remove',
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ function getSuggestionsForCommand(tokensBeforeCurrent: string[]): CompletionSugg
|
|||||||
return completeSubcommands(ROOT_HELP_TOPICS.map((topic) => topic.name));
|
return completeSubcommands(ROOT_HELP_TOPICS.map((topic) => topic.name));
|
||||||
case 'auth':
|
case 'auth':
|
||||||
if (!subcommand) return completeSubcommands(AUTH_SUBCOMMANDS);
|
if (!subcommand) return completeSubcommands(AUTH_SUBCOMMANDS);
|
||||||
|
if (subcommand === 'backup')
|
||||||
|
return completeSubcommands(['default', ...getProfileNames('accounts')], ['--json']);
|
||||||
if (subcommand === 'show')
|
if (subcommand === 'show')
|
||||||
return completeSubcommands(getProfileNames('accounts'), ['--json']);
|
return completeSubcommands(getProfileNames('accounts'), ['--json']);
|
||||||
if (subcommand === 'remove')
|
if (subcommand === 'remove')
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
type MergedAccountEntry,
|
type MergedAccountEntry,
|
||||||
} from './account-route-helpers';
|
} from './account-route-helpers';
|
||||||
import type { AccountConfig } from '../../config/unified-config-types';
|
import type { AccountConfig } from '../../config/unified-config-types';
|
||||||
|
import { resolveConfiguredPlainCcsResumeLane } from '../../auth/resume-lane-diagnostics';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ function hasAuthAccount(name: string): boolean {
|
|||||||
/**
|
/**
|
||||||
* GET /api/accounts - List accounts from both profiles.json and config.yaml
|
* 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 {
|
try {
|
||||||
const registry = createProfileRegistry();
|
const registry = createProfileRegistry();
|
||||||
|
|
||||||
@@ -147,8 +148,19 @@ router.get('/', (_req: Request, res: Response): void => {
|
|||||||
|
|
||||||
// Get default from unified config first, fallback to legacy
|
// Get default from unified config first, fallback to legacy
|
||||||
const defaultProfile = registry.getDefaultUnified() ?? registry.getDefaultProfile() ?? null;
|
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) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: (error as Error).message });
|
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 = '',
|
current = '',
|
||||||
shell: 'bash' | 'fish' | 'powershell' | 'zsh' = 'bash'
|
shell: 'bash' | 'fish' | 'powershell' | 'zsh' = 'bash'
|
||||||
): string[] {
|
): string[] {
|
||||||
return getCompletionSuggestions({ tokensBeforeCurrent, current, shell }).map((entry) => entry.value);
|
return getCompletionSuggestions({ tokensBeforeCurrent, current, shell }).map(
|
||||||
|
(entry) => entry.value
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -100,7 +102,9 @@ describe('completion backend', () => {
|
|||||||
|
|
||||||
test('suggests lifecycle subcommands for api', () => {
|
test('suggests lifecycle subcommands for api', () => {
|
||||||
const values = suggestionValues(['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', () => {
|
test('suggests account profiles for auth show', () => {
|
||||||
@@ -109,6 +113,13 @@ describe('completion backend', () => {
|
|||||||
expect(values).toContain('--json');
|
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', () => {
|
test('suggests cliproxy variants for variant-scoped commands', () => {
|
||||||
const values = suggestionValues(['cliproxy', 'edit']);
|
const values = suggestionValues(['cliproxy', 'edit']);
|
||||||
expect(values).toContain('my-codex');
|
expect(values).toContain('my-codex');
|
||||||
@@ -116,7 +127,9 @@ describe('completion backend', () => {
|
|||||||
|
|
||||||
test('suggests env format values after the format flag', () => {
|
test('suggests env format values after the format flag', () => {
|
||||||
const values = suggestionValues(['env', '--format']);
|
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', () => {
|
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);
|
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 () => {
|
it('does not delete metadata when instance deletion fails', async () => {
|
||||||
const ccsDir = path.join(tempHome, '.ccs');
|
const ccsDir = path.join(tempHome, '.ccs');
|
||||||
fs.mkdirSync(ccsDir, { recursive: true });
|
fs.mkdirSync(ccsDir, { recursive: true });
|
||||||
|
|||||||
@@ -35,14 +35,21 @@ import {
|
|||||||
useUpdateAccountContext,
|
useUpdateAccountContext,
|
||||||
} from '@/hooks/use-accounts';
|
} from '@/hooks/use-accounts';
|
||||||
import type { AuthAccountRow, SharedGroupSummary } from '@/lib/account-continuity';
|
import type { AuthAccountRow, SharedGroupSummary } from '@/lib/account-continuity';
|
||||||
|
import type { PlainCcsLane } from '@/lib/api-client';
|
||||||
|
|
||||||
interface AccountsTableProps {
|
interface AccountsTableProps {
|
||||||
data: AuthAccountRow[];
|
data: AuthAccountRow[];
|
||||||
defaultAccount: string | null;
|
defaultAccount: string | null;
|
||||||
groupSummaries: SharedGroupSummary[];
|
groupSummaries: SharedGroupSummary[];
|
||||||
|
plainCcsLane: PlainCcsLane | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AccountsTable({ data, defaultAccount, groupSummaries }: AccountsTableProps) {
|
export function AccountsTable({
|
||||||
|
data,
|
||||||
|
defaultAccount,
|
||||||
|
groupSummaries,
|
||||||
|
plainCcsLane,
|
||||||
|
}: AccountsTableProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const setDefaultMutation = useSetDefaultAccount();
|
const setDefaultMutation = useSetDefaultAccount();
|
||||||
const deleteMutation = useDeleteAccount();
|
const deleteMutation = useDeleteAccount();
|
||||||
@@ -114,7 +121,7 @@ export function AccountsTable({ data, defaultAccount, groupSummaries }: Accounts
|
|||||||
variant="outline"
|
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'}`}
|
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>
|
</Badge>
|
||||||
<span className="text-xs font-semibold text-foreground/80">{group}</span>
|
<span className="text-xs font-semibold text-foreground/80">{group}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,7 +143,7 @@ export function AccountsTable({ data, defaultAccount, groupSummaries }: Accounts
|
|||||||
variant="outline"
|
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"
|
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>
|
</Badge>
|
||||||
<p className="text-[10px] text-amber-700/80 dark:text-amber-400/80 whitespace-nowrap">
|
<p className="text-[10px] text-amber-700/80 dark:text-amber-400/80 whitespace-nowrap">
|
||||||
{t('accountsTable.legacyReview')}
|
{t('accountsTable.legacyReview')}
|
||||||
@@ -151,7 +158,7 @@ export function AccountsTable({ data, defaultAccount, groupSummaries }: Accounts
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="font-mono text-[10px] uppercase px-1.5 py-0 text-muted-foreground bg-muted/60 border-transparent shadow-none"
|
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>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -321,6 +328,7 @@ export function AccountsTable({ data, defaultAccount, groupSummaries }: Accounts
|
|||||||
<EditAccountContextDialog
|
<EditAccountContextDialog
|
||||||
account={contextTarget}
|
account={contextTarget}
|
||||||
groupSummaries={groupSummaries}
|
groupSummaries={groupSummaries}
|
||||||
|
plainCcsLane={plainCcsLane}
|
||||||
onClose={() => setContextTarget(null)}
|
onClose={() => setContextTarget(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { CopyButton } from '@/components/ui/copy-button';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
import type { PlainCcsLane } from '@/lib/api-client';
|
||||||
|
|
||||||
interface ContinuityOverviewProps {
|
interface ContinuityOverviewProps {
|
||||||
totalAccounts: number;
|
totalAccounts: number;
|
||||||
|
primaryAccountName?: string | null;
|
||||||
isolatedCount: number;
|
isolatedCount: number;
|
||||||
sharedStandardCount: number;
|
sharedStandardCount: number;
|
||||||
deeperSharedCount: number;
|
deeperSharedCount: number;
|
||||||
@@ -27,6 +30,7 @@ interface ContinuityOverviewProps {
|
|||||||
deeperReadyGroups: string[];
|
deeperReadyGroups: string[];
|
||||||
legacyTargetCount: number;
|
legacyTargetCount: number;
|
||||||
cliproxyCount: number;
|
cliproxyCount: number;
|
||||||
|
plainCcsLane: PlainCcsLane | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReadinessState =
|
type ReadinessState =
|
||||||
@@ -45,6 +49,7 @@ const InfoTooltip = ({ titleKey, descKey }: { titleKey: string; descKey: string
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
aria-label={t(titleKey)}
|
||||||
className="h-5 w-5 rounded-full hover:bg-muted text-muted-foreground/70 transition-colors"
|
className="h-5 w-5 rounded-full hover:bg-muted text-muted-foreground/70 transition-colors"
|
||||||
>
|
>
|
||||||
<Info className="h-3 w-3" />
|
<Info className="h-3 w-3" />
|
||||||
@@ -64,6 +69,7 @@ const InfoTooltip = ({ titleKey, descKey }: { titleKey: string; descKey: string
|
|||||||
|
|
||||||
export function ContinuityOverview({
|
export function ContinuityOverview({
|
||||||
totalAccounts,
|
totalAccounts,
|
||||||
|
primaryAccountName,
|
||||||
isolatedCount,
|
isolatedCount,
|
||||||
sharedStandardCount,
|
sharedStandardCount,
|
||||||
deeperSharedCount,
|
deeperSharedCount,
|
||||||
@@ -74,8 +80,26 @@ export function ContinuityOverview({
|
|||||||
deeperReadyGroups,
|
deeperReadyGroups,
|
||||||
legacyTargetCount,
|
legacyTargetCount,
|
||||||
cliproxyCount,
|
cliproxyCount,
|
||||||
|
plainCcsLane,
|
||||||
}: ContinuityOverviewProps) {
|
}: ContinuityOverviewProps) {
|
||||||
const { t } = useTranslation();
|
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 =
|
const readiness: ReadinessState =
|
||||||
totalAccounts < 2
|
totalAccounts < 2
|
||||||
@@ -110,6 +134,11 @@ export function ContinuityOverview({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const currentIcon = iconMap[readiness];
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@@ -167,7 +196,7 @@ export function ContinuityOverview({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="font-mono text-[11px] px-2 bg-muted/50 text-muted-foreground border-transparent"
|
className="font-mono text-[11px] px-2 bg-muted/50 text-muted-foreground border-transparent"
|
||||||
>
|
>
|
||||||
Recommend: {highlightGroup}
|
{t('continuityOverview.recommendBadge', { group: highlightGroup })}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{hasMixedState && (
|
{hasMixedState && (
|
||||||
@@ -175,13 +204,70 @@ export function ContinuityOverview({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="font-mono text-[11px] px-2 bg-muted/50 text-muted-foreground border-transparent"
|
className="font-mono text-[11px] px-2 bg-muted/50 text-muted-foreground border-transparent"
|
||||||
>
|
>
|
||||||
Partial sync ({highlightGroup})
|
{t('continuityOverview.partialBadge', { group: highlightGroup })}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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 */}
|
{/* Horizontal Progression Chain */}
|
||||||
<div className="flex flex-col md:flex-row items-center gap-3">
|
<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">
|
<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 {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -14,7 +14,9 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Unlink, Link2, Waves, ShieldAlert, Info } from 'lucide-react';
|
import { Unlink, Link2, Waves, ShieldAlert, Info } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { AuthAccountRow, SharedGroupSummary } from '@/lib/account-continuity';
|
import type { AuthAccountRow, SharedGroupSummary } from '@/lib/account-continuity';
|
||||||
|
import type { PlainCcsLane } from '@/lib/api-client';
|
||||||
import { useUpdateAccountContext } from '@/hooks/use-accounts';
|
import { useUpdateAccountContext } from '@/hooks/use-accounts';
|
||||||
|
import { CopyButton } from '@/components/ui/copy-button';
|
||||||
|
|
||||||
type ContextMode = 'isolated' | 'shared';
|
type ContextMode = 'isolated' | 'shared';
|
||||||
type ContinuityMode = 'standard' | 'deeper';
|
type ContinuityMode = 'standard' | 'deeper';
|
||||||
@@ -25,12 +27,14 @@ const CONTEXT_GROUP_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|||||||
interface EditAccountContextDialogProps {
|
interface EditAccountContextDialogProps {
|
||||||
account: AuthAccountRow;
|
account: AuthAccountRow;
|
||||||
groupSummaries: SharedGroupSummary[];
|
groupSummaries: SharedGroupSummary[];
|
||||||
|
plainCcsLane: PlainCcsLane | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditAccountContextDialog({
|
export function EditAccountContextDialog({
|
||||||
account,
|
account,
|
||||||
groupSummaries,
|
groupSummaries,
|
||||||
|
plainCcsLane,
|
||||||
onClose,
|
onClose,
|
||||||
}: EditAccountContextDialogProps) {
|
}: EditAccountContextDialogProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -73,6 +77,43 @@ export function EditAccountContextDialog({
|
|||||||
0
|
0
|
||||||
)
|
)
|
||||||
: 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 = () => {
|
const handleSave = () => {
|
||||||
if (!canSubmit) {
|
if (!canSubmit) {
|
||||||
@@ -124,7 +165,11 @@ export function EditAccountContextDialog({
|
|||||||
type="button"
|
type="button"
|
||||||
role="radio"
|
role="radio"
|
||||||
aria-checked={mode === 'isolated'}
|
aria-checked={mode === 'isolated'}
|
||||||
|
tabIndex={mode === 'isolated' ? 0 : -1}
|
||||||
onClick={() => setMode('isolated')}
|
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 ${
|
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'
|
mode === 'isolated'
|
||||||
? 'bg-background text-blue-600 dark:text-blue-400 shadow-sm ring-1 ring-border/50'
|
? '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"
|
type="button"
|
||||||
role="radio"
|
role="radio"
|
||||||
aria-checked={mode === 'shared'}
|
aria-checked={mode === 'shared'}
|
||||||
|
tabIndex={mode === 'shared' ? 0 : -1}
|
||||||
onClick={() => setMode('shared')}
|
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 ${
|
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'
|
mode === 'shared'
|
||||||
? 'bg-background text-emerald-600 dark:text-emerald-400 shadow-sm ring-1 ring-border/50'
|
? '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"
|
type="button"
|
||||||
role="radio"
|
role="radio"
|
||||||
aria-checked={continuityMode === 'standard'}
|
aria-checked={continuityMode === 'standard'}
|
||||||
|
tabIndex={continuityMode === 'standard' ? 0 : -1}
|
||||||
onClick={() => setContinuityMode('standard')}
|
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 ${
|
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'
|
continuityMode === 'standard'
|
||||||
? 'bg-background text-emerald-600 dark:text-emerald-400 shadow-sm ring-1 ring-border/50'
|
? '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"
|
type="button"
|
||||||
role="radio"
|
role="radio"
|
||||||
aria-checked={continuityMode === 'deeper'}
|
aria-checked={continuityMode === 'deeper'}
|
||||||
|
tabIndex={continuityMode === 'deeper' ? 0 : -1}
|
||||||
onClick={() => setContinuityMode('deeper')}
|
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 ${
|
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'
|
continuityMode === 'deeper'
|
||||||
? 'bg-background text-indigo-600 dark:text-indigo-400 shadow-sm ring-1 ring-border/50'
|
? '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')}
|
{t('editAccountContext.credentialsIsolated')}
|
||||||
</p>
|
</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
|
<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'}`}
|
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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { api } from '@/lib/api-client';
|
import { api } from '@/lib/api-client';
|
||||||
import type { Account } from '@/lib/api-client';
|
import type { Account, PlainCcsLane } from '@/lib/api-client';
|
||||||
import {
|
import {
|
||||||
summarizeAuthAccountContinuity,
|
summarizeAuthAccountContinuity,
|
||||||
type AuthAccountRow,
|
type AuthAccountRow,
|
||||||
@@ -30,6 +30,7 @@ export interface AuthAccountsView {
|
|||||||
deeperReadyGroups: string[];
|
deeperReadyGroups: string[];
|
||||||
sharedGroups: string[];
|
sharedGroups: string[];
|
||||||
groupSummaries: SharedGroupSummary[];
|
groupSummaries: SharedGroupSummary[];
|
||||||
|
plainCcsLane: PlainCcsLane | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAccounts() {
|
export function useAccounts() {
|
||||||
@@ -61,6 +62,7 @@ export function useAccounts() {
|
|||||||
deeperReadyGroups: continuity.deeperReadyGroups,
|
deeperReadyGroups: continuity.deeperReadyGroups,
|
||||||
sharedGroups: continuity.sharedGroups,
|
sharedGroups: continuity.sharedGroups,
|
||||||
groupSummaries: continuity.groupSummaries,
|
groupSummaries: continuity.groupSummaries,
|
||||||
|
plainCcsLane: data.plain_ccs_lane ?? null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -733,6 +733,14 @@ export interface Account {
|
|||||||
displayName?: string;
|
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 {
|
export interface UpdateAccountContext {
|
||||||
context_mode: 'isolated' | 'shared';
|
context_mode: 'isolated' | 'shared';
|
||||||
context_group?: string;
|
context_group?: string;
|
||||||
@@ -1120,7 +1128,10 @@ export const api = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
accounts: {
|
accounts: {
|
||||||
list: () => request<{ accounts: Account[]; default: string | null }>('/accounts'),
|
list: () =>
|
||||||
|
request<{ accounts: Account[]; default: string | null; plain_ccs_lane?: PlainCcsLane }>(
|
||||||
|
'/accounts'
|
||||||
|
),
|
||||||
setDefault: (name: string) =>
|
setDefault: (name: string) =>
|
||||||
request('/accounts/default', {
|
request('/accounts/default', {
|
||||||
method: 'POST',
|
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.',
|
'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: {
|
accountsTable: {
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
type: 'Type',
|
type: 'Type',
|
||||||
@@ -573,6 +588,12 @@ const resources = {
|
|||||||
sameGroupPeerCount_other: '{{count}} same-group peers',
|
sameGroupPeerCount_other: '{{count}} same-group peers',
|
||||||
legacyReview: 'Review and confirm this inferred default.',
|
legacyReview: 'Review and confirm this inferred default.',
|
||||||
noHandoff: 'Cross-account resume stays in the original account.',
|
noHandoff: 'Cross-account resume stays in the original account.',
|
||||||
|
badges: {
|
||||||
|
shared: 'Shared',
|
||||||
|
deeper: 'Deeper',
|
||||||
|
legacy: 'Legacy',
|
||||||
|
isolated: 'Isolated',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
addAccountDialog: {
|
addAccountDialog: {
|
||||||
title: 'Add {{displayName}} Account',
|
title: 'Add {{displayName}} Account',
|
||||||
@@ -1820,6 +1841,20 @@ const resources = {
|
|||||||
resumeOriginal: '在第二个账号配置完成之前,重要会话仍应从原账号继续恢复。',
|
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: {
|
accountsTable: {
|
||||||
name: '名称',
|
name: '名称',
|
||||||
type: '类型',
|
type: '类型',
|
||||||
@@ -1853,6 +1888,12 @@ const resources = {
|
|||||||
sameGroupPeerCount_other: '{{count}} 个同组账号',
|
sameGroupPeerCount_other: '{{count}} 个同组账号',
|
||||||
legacyReview: '请检查并确认这个推断出的默认状态。',
|
legacyReview: '请检查并确认这个推断出的默认状态。',
|
||||||
noHandoff: '跨账号续接仍会回到原账号。',
|
noHandoff: '跨账号续接仍会回到原账号。',
|
||||||
|
badges: {
|
||||||
|
shared: '共享',
|
||||||
|
deeper: '更深',
|
||||||
|
legacy: '旧策略',
|
||||||
|
isolated: '隔离',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
addAccountDialog: {
|
addAccountDialog: {
|
||||||
title: '添加 {{displayName}} 账号',
|
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.',
|
'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: {
|
accountsTable: {
|
||||||
name: 'Tên',
|
name: 'Tên',
|
||||||
type: 'Kiểu',
|
type: 'Kiểu',
|
||||||
@@ -3152,6 +3208,12 @@ const resources = {
|
|||||||
sameGroupPeerCount_other: '{{count}} tài khoản cùng nhóm',
|
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.',
|
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.',
|
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: {
|
addAccountDialog: {
|
||||||
title: 'Thêm tài khoản {{displayName}}',
|
title: 'Thêm tài khoản {{displayName}}',
|
||||||
@@ -4454,6 +4516,21 @@ const resources = {
|
|||||||
'2 つ目のアカウントが整うまでは、重要な会話は元のアカウントから再開し続けてください。',
|
'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: {
|
accountsTable: {
|
||||||
name: '名前',
|
name: '名前',
|
||||||
type: '種別',
|
type: '種別',
|
||||||
@@ -4488,6 +4565,12 @@ const resources = {
|
|||||||
sameGroupPeerCount_other: '{{count}} 件の同グループアカウント',
|
sameGroupPeerCount_other: '{{count}} 件の同グループアカウント',
|
||||||
legacyReview: 'この推定された既定状態を確認して明示的に確定してください。',
|
legacyReview: 'この推定された既定状態を確認して明示的に確定してください。',
|
||||||
noHandoff: 'アカウント間再開は元のアカウントに残ります。',
|
noHandoff: 'アカウント間再開は元のアカウントに残ります。',
|
||||||
|
badges: {
|
||||||
|
shared: '共有',
|
||||||
|
deeper: '拡張',
|
||||||
|
legacy: 'レガシー',
|
||||||
|
isolated: '分離',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
addAccountDialog: {
|
addAccountDialog: {
|
||||||
title: '{{displayName}} アカウントを追加',
|
title: '{{displayName}} アカウントを追加',
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export function AccountsPage() {
|
|||||||
const deeperReadyGroups = data?.deeperReadyGroups || [];
|
const deeperReadyGroups = data?.deeperReadyGroups || [];
|
||||||
const sharedGroups = data?.sharedGroups || [];
|
const sharedGroups = data?.sharedGroups || [];
|
||||||
const groupSummaries = data?.groupSummaries || [];
|
const groupSummaries = data?.groupSummaries || [];
|
||||||
|
const plainCcsLane = data?.plainCcsLane || null;
|
||||||
|
|
||||||
const legacyTargets = authAccounts.filter(
|
const legacyTargets = authAccounts.filter(
|
||||||
(account) => account.context_inferred || account.continuity_inferred
|
(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">
|
<div className="flex-1 min-h-0 p-5 space-y-4 overflow-y-auto">
|
||||||
<ContinuityOverview
|
<ContinuityOverview
|
||||||
totalAccounts={authAccounts.length}
|
totalAccounts={authAccounts.length}
|
||||||
|
primaryAccountName={authAccounts.length === 1 ? authAccounts[0]?.name : null}
|
||||||
isolatedCount={isolatedCount}
|
isolatedCount={isolatedCount}
|
||||||
sharedStandardCount={sharedStandardCount}
|
sharedStandardCount={sharedStandardCount}
|
||||||
deeperSharedCount={deeperSharedCount}
|
deeperSharedCount={deeperSharedCount}
|
||||||
@@ -200,6 +202,7 @@ export function AccountsPage() {
|
|||||||
deeperReadyGroups={deeperReadyGroups}
|
deeperReadyGroups={deeperReadyGroups}
|
||||||
legacyTargetCount={legacyTargetCount}
|
legacyTargetCount={legacyTargetCount}
|
||||||
cliproxyCount={cliproxyCount}
|
cliproxyCount={cliproxyCount}
|
||||||
|
plainCcsLane={plainCcsLane}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card className="flex flex-col">
|
<Card className="flex flex-col">
|
||||||
@@ -217,6 +220,7 @@ export function AccountsPage() {
|
|||||||
data={authAccounts}
|
data={authAccounts}
|
||||||
defaultAccount={data?.default ?? null}
|
defaultAccount={data?.default ?? null}
|
||||||
groupSummaries={groupSummaries}
|
groupSummaries={groupSummaries}
|
||||||
|
plainCcsLane={plainCcsLane}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -264,6 +268,7 @@ export function AccountsPage() {
|
|||||||
|
|
||||||
<ContinuityOverview
|
<ContinuityOverview
|
||||||
totalAccounts={authAccounts.length}
|
totalAccounts={authAccounts.length}
|
||||||
|
primaryAccountName={authAccounts.length === 1 ? authAccounts[0]?.name : null}
|
||||||
isolatedCount={isolatedCount}
|
isolatedCount={isolatedCount}
|
||||||
sharedStandardCount={sharedStandardCount}
|
sharedStandardCount={sharedStandardCount}
|
||||||
deeperSharedCount={deeperSharedCount}
|
deeperSharedCount={deeperSharedCount}
|
||||||
@@ -275,6 +280,7 @@ export function AccountsPage() {
|
|||||||
deeperReadyGroups={deeperReadyGroups}
|
deeperReadyGroups={deeperReadyGroups}
|
||||||
legacyTargetCount={legacyTargetCount}
|
legacyTargetCount={legacyTargetCount}
|
||||||
cliproxyCount={cliproxyCount}
|
cliproxyCount={cliproxyCount}
|
||||||
|
plainCcsLane={plainCcsLane}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
@@ -289,6 +295,7 @@ export function AccountsPage() {
|
|||||||
data={authAccounts}
|
data={authAccounts}
|
||||||
defaultAccount={data?.default ?? null}
|
defaultAccount={data?.default ?? null}
|
||||||
groupSummaries={groupSummaries}
|
groupSummaries={groupSummaries}
|
||||||
|
plainCcsLane={plainCcsLane}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user