- Restructure sidebar: Start → Guide → Operations → Developers → Reference - Fix EN/RU sidebar order (Installation before Quickstart) - Expand troubleshooting with diagnostics commands and task-log triage - Improve quickstart with prerequisites, pitfalls, and contributor links - Expand installation docs with verification commands - Add cyberpunk hero theme to landing page - Add atomicFile utility with tests and stage-runtime script - Harden team provisioning with better error handling and progress output - Add cross-team communication, kanban, and workSync improvements
361 lines
11 KiB
TypeScript
361 lines
11 KiB
TypeScript
import { normalizeCreateLaunchProviderForUi } from '@renderer/utils/geminiUiFreeze';
|
|
import { normalizeExplicitTeamModelForUi } from '@renderer/utils/teamModelAvailability';
|
|
import { isTeamEffortLevel } from '@shared/utils/effortLevels';
|
|
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
|
|
|
import type { EffortLevel, TeamFastMode, TeamProviderId } from '@shared/types';
|
|
|
|
const CREATE_TEAM_PREFIX = 'createTeam:';
|
|
const LEGACY_TEAM_PREFIX = 'team:';
|
|
|
|
const CREATE_TEAM_PROVIDER_KEY = `${CREATE_TEAM_PREFIX}lastSelectedProvider`;
|
|
const CREATE_TEAM_FAST_MODE_KEY = `${CREATE_TEAM_PREFIX}lastSelectedFastMode`;
|
|
const CREATE_TEAM_LIMIT_CONTEXT_KEY = `${CREATE_TEAM_PREFIX}lastLimitContext`;
|
|
const CREATE_TEAM_SKIP_PERMISSIONS_KEY = `${CREATE_TEAM_PREFIX}lastSkipPermissions`;
|
|
const CREATE_TEAM_EFFORT_KEY = `${CREATE_TEAM_PREFIX}lastSelectedEffort`;
|
|
const CREATE_TEAM_SYNC_MODELS_KEY = `${CREATE_TEAM_PREFIX}lastSyncModelsWithLead`;
|
|
const CREATE_TEAM_MEMBER_RUNTIME_PREFERENCES_KEY = `${CREATE_TEAM_PREFIX}lastMemberRuntimePreferences`;
|
|
const CREATE_TEAM_MEMBER_RUNTIME_PREFERENCES_VERSION = 1;
|
|
|
|
export interface CreateTeamMemberRuntimePreference {
|
|
name: string;
|
|
providerId?: TeamProviderId;
|
|
model?: string;
|
|
effort?: EffortLevel;
|
|
}
|
|
|
|
interface StoredCreateTeamMemberRuntimePreferences {
|
|
version: number;
|
|
members: CreateTeamMemberRuntimePreference[];
|
|
}
|
|
|
|
function readStorageItem(key: string): string | null {
|
|
try {
|
|
return globalThis.localStorage?.getItem(key) ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function writeStorageItem(key: string, value: string): void {
|
|
try {
|
|
globalThis.localStorage?.setItem(key, value);
|
|
} catch {
|
|
// Ignore storage write failures in renderer helpers.
|
|
}
|
|
}
|
|
|
|
function removeStorageItem(key: string): void {
|
|
try {
|
|
globalThis.localStorage?.removeItem(key);
|
|
} catch {
|
|
// Ignore storage delete failures in renderer helpers.
|
|
}
|
|
}
|
|
|
|
function readCreateTeamPreference(key: string, legacyKey?: string): string | null {
|
|
const nextValue = readStorageItem(key);
|
|
if (nextValue != null) {
|
|
return nextValue;
|
|
}
|
|
return legacyKey ? readStorageItem(legacyKey) : null;
|
|
}
|
|
|
|
function isValidCreateTeamMemberRuntimePreference(
|
|
value: unknown
|
|
): value is CreateTeamMemberRuntimePreference {
|
|
if (typeof value !== 'object' || value === null) {
|
|
return false;
|
|
}
|
|
const entry = value as Record<string, unknown>;
|
|
return (
|
|
typeof entry.name === 'string' &&
|
|
(entry.providerId === undefined || normalizeOptionalTeamProviderId(entry.providerId) != null) &&
|
|
(entry.model === undefined || typeof entry.model === 'string') &&
|
|
(entry.effort === undefined || isTeamEffortLevel(entry.effort))
|
|
);
|
|
}
|
|
|
|
function parseStoredCreateTeamMemberRuntimePreferences(
|
|
value: string | null
|
|
): CreateTeamMemberRuntimePreference[] {
|
|
if (!value) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(value) as StoredCreateTeamMemberRuntimePreferences;
|
|
if (
|
|
parsed.version !== CREATE_TEAM_MEMBER_RUNTIME_PREFERENCES_VERSION ||
|
|
!Array.isArray(parsed.members)
|
|
) {
|
|
return [];
|
|
}
|
|
|
|
return parsed.members.filter(isValidCreateTeamMemberRuntimePreference).map((entry) => {
|
|
const providerId = normalizeOptionalTeamProviderId(entry.providerId);
|
|
const normalizedModel = normalizeExplicitTeamModelForUi(providerId, entry.model ?? '');
|
|
return {
|
|
name: entry.name.trim(),
|
|
providerId,
|
|
model: normalizedModel || undefined,
|
|
effort: entry.effort,
|
|
};
|
|
});
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function normalizeCreateTeamMemberRuntimePreferences(
|
|
members: readonly {
|
|
name: string;
|
|
providerId?: TeamProviderId;
|
|
model?: string;
|
|
effort?: EffortLevel;
|
|
removedAt?: number | string | null;
|
|
}[]
|
|
): CreateTeamMemberRuntimePreference[] {
|
|
const normalizedMembers: CreateTeamMemberRuntimePreference[] = [];
|
|
const seenNames = new Set<string>();
|
|
|
|
for (const member of members) {
|
|
if (member.removedAt) {
|
|
continue;
|
|
}
|
|
|
|
const name = member.name.trim();
|
|
const normalizedName = name.toLowerCase();
|
|
if (!name || seenNames.has(normalizedName)) {
|
|
continue;
|
|
}
|
|
seenNames.add(normalizedName);
|
|
|
|
const providerId = normalizeOptionalTeamProviderId(member.providerId);
|
|
const model = normalizeExplicitTeamModelForUi(providerId, member.model ?? '');
|
|
normalizedMembers.push({
|
|
name,
|
|
providerId,
|
|
model: model || undefined,
|
|
effort: isTeamEffortLevel(member.effort) ? member.effort : undefined,
|
|
});
|
|
}
|
|
|
|
return normalizedMembers;
|
|
}
|
|
|
|
function getCreateTeamModelKey(providerId: TeamProviderId): string {
|
|
return `${CREATE_TEAM_PREFIX}lastSelectedModel:${providerId}`;
|
|
}
|
|
|
|
function getLegacyTeamModelKey(providerId: TeamProviderId): string {
|
|
return `${LEGACY_TEAM_PREFIX}lastSelectedModel:${providerId}`;
|
|
}
|
|
|
|
function copyLegacyPreferenceIfMissing(key: string, legacyKey: string): void {
|
|
if (readStorageItem(key) != null) {
|
|
return;
|
|
}
|
|
const legacyValue = readStorageItem(legacyKey);
|
|
if (legacyValue != null) {
|
|
writeStorageItem(key, legacyValue);
|
|
}
|
|
}
|
|
|
|
export function migrateLegacyCreateTeamPreferences(): void {
|
|
copyLegacyPreferenceIfMissing(
|
|
CREATE_TEAM_PROVIDER_KEY,
|
|
`${LEGACY_TEAM_PREFIX}lastSelectedProvider`
|
|
);
|
|
copyLegacyPreferenceIfMissing(
|
|
CREATE_TEAM_FAST_MODE_KEY,
|
|
`${LEGACY_TEAM_PREFIX}lastSelectedFastMode`
|
|
);
|
|
copyLegacyPreferenceIfMissing(
|
|
CREATE_TEAM_LIMIT_CONTEXT_KEY,
|
|
`${LEGACY_TEAM_PREFIX}lastLimitContext`
|
|
);
|
|
copyLegacyPreferenceIfMissing(
|
|
CREATE_TEAM_SKIP_PERMISSIONS_KEY,
|
|
`${LEGACY_TEAM_PREFIX}lastSkipPermissions`
|
|
);
|
|
copyLegacyPreferenceIfMissing(CREATE_TEAM_EFFORT_KEY, `${LEGACY_TEAM_PREFIX}lastSelectedEffort`);
|
|
|
|
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode'] as const) {
|
|
copyLegacyPreferenceIfMissing(
|
|
getCreateTeamModelKey(providerId),
|
|
getLegacyTeamModelKey(providerId)
|
|
);
|
|
}
|
|
|
|
const legacyTeamModel = readStorageItem(`${LEGACY_TEAM_PREFIX}lastSelectedModel`);
|
|
if (legacyTeamModel != null && readStorageItem(getCreateTeamModelKey('anthropic')) == null) {
|
|
writeStorageItem(getCreateTeamModelKey('anthropic'), legacyTeamModel);
|
|
}
|
|
removeStorageItem(`${LEGACY_TEAM_PREFIX}lastSelectedModel`);
|
|
}
|
|
|
|
export function getStoredCreateTeamProvider(): TeamProviderId {
|
|
const stored = readCreateTeamPreference(
|
|
CREATE_TEAM_PROVIDER_KEY,
|
|
`${LEGACY_TEAM_PREFIX}lastSelectedProvider`
|
|
);
|
|
return normalizeCreateLaunchProviderForUi(normalizeOptionalTeamProviderId(stored), true);
|
|
}
|
|
|
|
export function setStoredCreateTeamProvider(providerId: TeamProviderId): void {
|
|
writeStorageItem(CREATE_TEAM_PROVIDER_KEY, providerId);
|
|
}
|
|
|
|
export function getStoredCreateTeamModel(providerId: TeamProviderId): string {
|
|
const stored = readCreateTeamPreference(
|
|
getCreateTeamModelKey(providerId),
|
|
getLegacyTeamModelKey(providerId)
|
|
);
|
|
if (stored === null) {
|
|
return providerId === 'anthropic' ? 'opus' : '';
|
|
}
|
|
return normalizeExplicitTeamModelForUi(providerId, stored === '__default__' ? '' : stored);
|
|
}
|
|
|
|
export function setStoredCreateTeamModel(providerId: TeamProviderId, model: string): void {
|
|
writeStorageItem(getCreateTeamModelKey(providerId), model);
|
|
}
|
|
|
|
export function getStoredCreateTeamFastMode(): TeamFastMode {
|
|
const stored = readCreateTeamPreference(
|
|
CREATE_TEAM_FAST_MODE_KEY,
|
|
`${LEGACY_TEAM_PREFIX}lastSelectedFastMode`
|
|
);
|
|
return stored === 'on' || stored === 'off' || stored === 'inherit' ? stored : 'inherit';
|
|
}
|
|
|
|
export function setStoredCreateTeamFastMode(value: TeamFastMode): void {
|
|
writeStorageItem(CREATE_TEAM_FAST_MODE_KEY, value);
|
|
}
|
|
|
|
export function getStoredCreateTeamLimitContext(): boolean {
|
|
return (
|
|
readCreateTeamPreference(
|
|
CREATE_TEAM_LIMIT_CONTEXT_KEY,
|
|
`${LEGACY_TEAM_PREFIX}lastLimitContext`
|
|
) === 'true'
|
|
);
|
|
}
|
|
|
|
export function setStoredCreateTeamLimitContext(value: boolean): void {
|
|
writeStorageItem(CREATE_TEAM_LIMIT_CONTEXT_KEY, String(value));
|
|
}
|
|
|
|
export function getStoredCreateTeamSkipPermissions(): boolean {
|
|
return (
|
|
readCreateTeamPreference(
|
|
CREATE_TEAM_SKIP_PERMISSIONS_KEY,
|
|
`${LEGACY_TEAM_PREFIX}lastSkipPermissions`
|
|
) !== 'false'
|
|
);
|
|
}
|
|
|
|
export function setStoredCreateTeamSkipPermissions(value: boolean): void {
|
|
writeStorageItem(CREATE_TEAM_SKIP_PERMISSIONS_KEY, String(value));
|
|
}
|
|
|
|
export function getStoredCreateTeamEffort(): string {
|
|
return (
|
|
readCreateTeamPreference(CREATE_TEAM_EFFORT_KEY, `${LEGACY_TEAM_PREFIX}lastSelectedEffort`) ??
|
|
''
|
|
);
|
|
}
|
|
|
|
export function setStoredCreateTeamEffort(value: string): void {
|
|
writeStorageItem(CREATE_TEAM_EFFORT_KEY, value);
|
|
}
|
|
|
|
export function getStoredCreateTeamSyncModelsWithLead(): boolean {
|
|
return readStorageItem(CREATE_TEAM_SYNC_MODELS_KEY) !== 'false';
|
|
}
|
|
|
|
export function setStoredCreateTeamSyncModelsWithLead(value: boolean): void {
|
|
writeStorageItem(CREATE_TEAM_SYNC_MODELS_KEY, String(value));
|
|
}
|
|
|
|
export function getStoredCreateTeamMemberRuntimePreferences(): CreateTeamMemberRuntimePreference[] {
|
|
return parseStoredCreateTeamMemberRuntimePreferences(
|
|
readStorageItem(CREATE_TEAM_MEMBER_RUNTIME_PREFERENCES_KEY)
|
|
);
|
|
}
|
|
|
|
export function setStoredCreateTeamMemberRuntimePreferences(
|
|
members: readonly {
|
|
name: string;
|
|
providerId?: TeamProviderId;
|
|
model?: string;
|
|
effort?: EffortLevel;
|
|
removedAt?: number | string | null;
|
|
}[]
|
|
): void {
|
|
const existingMembers = getStoredCreateTeamMemberRuntimePreferences();
|
|
const nextMembers = normalizeCreateTeamMemberRuntimePreferences(members);
|
|
const mergedMembers = [...existingMembers];
|
|
const mergedIndexByName = new Map(
|
|
mergedMembers.map((member, index) => [member.name.trim().toLowerCase(), index] as const)
|
|
);
|
|
|
|
for (const member of nextMembers) {
|
|
const normalizedName = member.name.trim().toLowerCase();
|
|
const existingIndex = mergedIndexByName.get(normalizedName);
|
|
if (existingIndex == null) {
|
|
mergedIndexByName.set(normalizedName, mergedMembers.length);
|
|
mergedMembers.push(member);
|
|
continue;
|
|
}
|
|
mergedMembers[existingIndex] = member;
|
|
}
|
|
|
|
writeStorageItem(
|
|
CREATE_TEAM_MEMBER_RUNTIME_PREFERENCES_KEY,
|
|
JSON.stringify({
|
|
version: CREATE_TEAM_MEMBER_RUNTIME_PREFERENCES_VERSION,
|
|
members: mergedMembers,
|
|
} satisfies StoredCreateTeamMemberRuntimePreferences)
|
|
);
|
|
}
|
|
|
|
export function applyStoredCreateTeamMemberRuntimePreferences<
|
|
T extends {
|
|
name: string;
|
|
providerId?: TeamProviderId;
|
|
model?: string;
|
|
effort?: EffortLevel;
|
|
removedAt?: number | string | null;
|
|
},
|
|
>(members: readonly T[]): T[] {
|
|
const storedPreferences = getStoredCreateTeamMemberRuntimePreferences();
|
|
if (storedPreferences.length === 0) {
|
|
return [...members];
|
|
}
|
|
|
|
const preferenceByName = new Map(
|
|
storedPreferences
|
|
.map((entry) => [entry.name.trim().toLowerCase(), entry] as const)
|
|
.filter(([name]) => name.length > 0)
|
|
);
|
|
|
|
return members.map((member) => {
|
|
if (member.removedAt) {
|
|
return member;
|
|
}
|
|
|
|
const preference = preferenceByName.get(member.name.trim().toLowerCase());
|
|
if (!preference) {
|
|
return member;
|
|
}
|
|
|
|
return {
|
|
...member,
|
|
providerId: preference.providerId,
|
|
model: preference.model ?? '',
|
|
effort: preference.effort,
|
|
};
|
|
});
|
|
}
|