Skip to content

Commit ff44212

Browse files
committed
feat: cache account profile details
1 parent ab7b13e commit ff44212

11 files changed

Lines changed: 304 additions & 35 deletions

apps/kimi-code/src/AccountSettings.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
ManagedUsageProgress,
88
} from "./components/AccountUsagePopover";
99
import { t } from "./i18n";
10-
import type { AccountUsage, AuthStatus, ManagedUserInfo } from "./types";
10+
import type { AccountProfile, AccountUsage, AuthStatus } from "./types";
1111

1212
export default function AccountSettings({
1313
auth,
@@ -20,7 +20,7 @@ export default function AccountSettings({
2020
onSignOut,
2121
}: {
2222
auth: AuthStatus;
23-
profile?: ManagedUserInfo;
23+
profile?: AccountProfile;
2424
usage?: AccountUsage;
2525
usageBusy: boolean;
2626
usageError?: string;

apps/kimi-code/src/App.tsx

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ import {
2727
type PluginCommandDef,
2828
} from "./agentRpc";
2929
import { subscribeToAppEvents } from "./app/appEventSubscriptions";
30+
import {
31+
accountProfileFromUserInfo,
32+
clearCachedAccountProfiles,
33+
readCachedAccountProfile,
34+
writeCachedAccountProfile,
35+
} from "./accountProfileCache";
3036
import {
3137
BACKGROUND_TASK_LIST_LIMIT,
3238
BACKGROUND_TASK_OUTPUT_TAIL,
@@ -169,6 +175,7 @@ import {
169175
webCredentialRequired
170176
} from "./transport";
171177
import type {
178+
AccountProfile,
172179
AccountUsage,
173180
AgentInteraction,
174181
AgentUsageStatus,
@@ -248,7 +255,7 @@ export default function App() {
248255
const [accountUsage, setAccountUsage] = useState<AccountUsage>();
249256
const [accountUsageBusy, setAccountUsageBusy] = useState(false);
250257
const [accountUsageError, setAccountUsageError] = useState<string>();
251-
const [accountProfile, setAccountProfile] = useState<ManagedUserInfo>();
258+
const [accountProfile, setAccountProfile] = useState<AccountProfile>();
252259
const [modelsBusy, setModelsBusy] = useState(false);
253260
const [modelBusy, setModelBusy] = useState(false);
254261
const [notice, setNotice] = useState<string>();
@@ -1013,12 +1020,18 @@ export default function App() {
10131020
}
10141021
};
10151022

1016-
const loadAccountProfile = async (): Promise<void> => {
1023+
const loadAccountProfile = async (
1024+
provider = auth.provider,
1025+
): Promise<void> => {
10171026
const request = accountProfileRequest.current + 1;
10181027
accountProfileRequest.current = request;
10191028
try {
1020-
const profile = await invoke<ManagedUserInfo>("account_profile");
1021-
if (request === accountProfileRequest.current) setAccountProfile(profile);
1029+
const userInfo = await invoke<ManagedUserInfo>("account_profile");
1030+
if (request === accountProfileRequest.current) {
1031+
const profile = accountProfileFromUserInfo(userInfo);
1032+
setAccountProfile(profile);
1033+
writeCachedAccountProfile(provider, profile);
1034+
}
10221035
} catch {
10231036
// Profile display is best-effort; keep the previous value on failure.
10241037
}
@@ -1088,21 +1101,26 @@ export default function App() {
10881101
.catch(() => {
10891102
// Vite's browser preview has no Tauri bridge.
10901103
});
1091-
void loadModels().then(() => {
1092-
if (!active) return;
1093-
void invoke<AuthStatus>("auth_status")
1094-
.then((status) => {
1095-
if (!active) return;
1096-
setAuth(status);
1097-
if (status.loggedIn) {
1098-
void refreshModels();
1099-
void loadAccountProfile();
1100-
}
1101-
})
1102-
.catch(() => {
1103-
// Vite's browser preview has no Tauri bridge; the actual desktop app does.
1104-
});
1105-
});
1104+
const modelsLoaded = loadModels();
1105+
void invoke<AuthStatus>("auth_status")
1106+
.then((status) => {
1107+
if (!active) return;
1108+
setAuth(status);
1109+
if (status.loggedIn) {
1110+
setAccountProfile(readCachedAccountProfile(status.provider));
1111+
void loadAccountProfile(status.provider);
1112+
void modelsLoaded.then(() => {
1113+
if (active) void refreshModels();
1114+
});
1115+
} else {
1116+
accountProfileRequest.current += 1;
1117+
setAccountProfile(undefined);
1118+
clearCachedAccountProfiles();
1119+
}
1120+
})
1121+
.catch(() => {
1122+
// Vite's browser preview has no Tauri bridge; the actual desktop app does.
1123+
});
11061124
return () => {
11071125
active = false;
11081126
};

apps/kimi-code/src/SettingsDialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { LANGUAGE_OPTIONS, t, type Language } from "./i18n";
1111
import PluginSettings from "./PluginSettings";
1212
import ProviderSettings from "./ProviderSettings";
1313
import { invoke, isDesktop, openExternalUrl } from "./transport";
14-
import type { AccountUsage, AuthStatus, ManagedUserInfo } from "./types";
14+
import type { AccountProfile, AccountUsage, AuthStatus } from "./types";
1515

1616
type SettingsTab = "general" | "account" | "providers" | "plugins" | "web" | "archived" | "about";
1717
type WebServerListenScope = "local" | "global";
@@ -69,7 +69,7 @@ export default function SettingsDialog({
6969
language: Language;
7070
notificationsEnabled: boolean;
7171
auth: AuthStatus;
72-
accountProfile?: ManagedUserInfo;
72+
accountProfile?: AccountProfile;
7373
accountUsage?: AccountUsage;
7474
accountUsageBusy: boolean;
7575
accountUsageError?: string;
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import type { AccountProfile, ManagedUserInfo } from "./types";
2+
3+
const CACHE_KEY_PREFIX = "kimi-code.account-profile.v1:";
4+
const CACHE_VERSION = 1;
5+
const CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
6+
7+
export interface AccountProfileStorage {
8+
readonly length: number;
9+
getItem(key: string): string | null;
10+
key(index: number): string | null;
11+
removeItem(key: string): void;
12+
setItem(key: string, value: string): void;
13+
}
14+
15+
interface CachedAccountProfile {
16+
version: typeof CACHE_VERSION;
17+
provider: string;
18+
updatedAt: number;
19+
profile: AccountProfile;
20+
}
21+
22+
function browserStorage(): AccountProfileStorage | undefined {
23+
try {
24+
return typeof window === "undefined" ? undefined : window.localStorage;
25+
} catch {
26+
return undefined;
27+
}
28+
}
29+
30+
function cacheKey(provider: string): string {
31+
return `${CACHE_KEY_PREFIX}${provider}`;
32+
}
33+
34+
function optionalString(value: unknown): string | undefined {
35+
return typeof value === "string" ? value : undefined;
36+
}
37+
38+
function parseProfile(value: unknown): AccountProfile | undefined {
39+
if (!value || typeof value !== "object") return undefined;
40+
const profile = value as Record<string, unknown>;
41+
if (
42+
typeof profile.userId !== "string" ||
43+
typeof profile.nickname !== "string" ||
44+
typeof profile.userLevel !== "number" ||
45+
typeof profile.userLevelName !== "string"
46+
) {
47+
return undefined;
48+
}
49+
return {
50+
userId: profile.userId,
51+
nickname: profile.nickname,
52+
userLevel: profile.userLevel,
53+
userLevelName: profile.userLevelName,
54+
avatar: optionalString(profile.avatar),
55+
username: optionalString(profile.username),
56+
};
57+
}
58+
59+
export function accountProfileFromUserInfo(
60+
userInfo: ManagedUserInfo,
61+
): AccountProfile {
62+
return {
63+
userId: userInfo.userId,
64+
nickname: userInfo.nickname,
65+
userLevel: userInfo.userLevel,
66+
userLevelName: userInfo.userLevelName,
67+
avatar: userInfo.avatar,
68+
username: userInfo.username,
69+
};
70+
}
71+
72+
export function readCachedAccountProfile(
73+
provider: string,
74+
storage: AccountProfileStorage | undefined = browserStorage(),
75+
now = Date.now(),
76+
): AccountProfile | undefined {
77+
if (!storage) return undefined;
78+
const key = cacheKey(provider);
79+
try {
80+
const raw = storage.getItem(key);
81+
if (!raw) return undefined;
82+
const cached = JSON.parse(raw) as Partial<CachedAccountProfile>;
83+
const profile = parseProfile(cached.profile);
84+
if (
85+
cached.version !== CACHE_VERSION ||
86+
cached.provider !== provider ||
87+
typeof cached.updatedAt !== "number" ||
88+
now - cached.updatedAt > CACHE_MAX_AGE_MS ||
89+
cached.updatedAt > now ||
90+
!profile
91+
) {
92+
storage.removeItem(key);
93+
return undefined;
94+
}
95+
return profile;
96+
} catch {
97+
try {
98+
storage.removeItem(key);
99+
} catch {
100+
// Storage access is best-effort.
101+
}
102+
return undefined;
103+
}
104+
}
105+
106+
export function writeCachedAccountProfile(
107+
provider: string,
108+
profile: AccountProfile,
109+
storage: AccountProfileStorage | undefined = browserStorage(),
110+
now = Date.now(),
111+
): void {
112+
if (!storage) return;
113+
const cached: CachedAccountProfile = {
114+
version: CACHE_VERSION,
115+
provider,
116+
updatedAt: now,
117+
profile,
118+
};
119+
try {
120+
storage.setItem(cacheKey(provider), JSON.stringify(cached));
121+
} catch {
122+
// Profile display remains functional when persistence is unavailable.
123+
}
124+
}
125+
126+
export function clearCachedAccountProfiles(
127+
storage: AccountProfileStorage | undefined = browserStorage(),
128+
): void {
129+
if (!storage) return;
130+
try {
131+
for (let index = storage.length - 1; index >= 0; index -= 1) {
132+
const key = storage.key(index);
133+
if (key?.startsWith(CACHE_KEY_PREFIX)) storage.removeItem(key);
134+
}
135+
} catch {
136+
// Storage access is best-effort.
137+
}
138+
}

apps/kimi-code/src/app/useConversationResources.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
getSkillContent,
1212
listSkills,
1313
} from "../agentRpc";
14+
import { clearCachedAccountProfiles } from "../accountProfileCache";
1415
import {
1516
completedTurnMessageId,
1617
type RenderMessage,
@@ -36,12 +37,12 @@ import { parseSkillPromptDisplay } from "../prompt/skills";
3637
import type { PromptDraftUpdater } from "../promptDrafts";
3738
import { invoke } from "../transport";
3839
import type {
40+
AccountProfile,
3941
AccountUsage,
4042
AuthStatus,
4143
CompactionEvent,
4244
Conversation,
4345
DeviceCode,
44-
ManagedUserInfo,
4546
Model,
4647
SkillContent,
4748
SkillDescriptor,
@@ -68,13 +69,13 @@ interface ConversationResourceOptions {
6869
composerAddOpen: boolean;
6970
historyRequests: RefObject<Record<string, number>>;
7071
inFlightTurnsRef: RefObject<Record<string, InFlightTurn>>;
71-
loadAccountProfile: () => Promise<void>;
72+
loadAccountProfile: (provider?: string) => Promise<void>;
7273
promptAttachments: PromptAttachment[];
7374
promptSkills: SkillDescriptor[];
7475
refreshModels: () => Promise<void>;
7576
resetPrompt: (value?: string, conversationId?: string) => void;
7677
selectedModel?: Model;
77-
setAccountProfile: Setter<ManagedUserInfo | undefined>;
78+
setAccountProfile: Setter<AccountProfile | undefined>;
7879
setAccountUsage: Setter<AccountUsage | undefined>;
7980
setAccountUsageBusy: Setter<boolean>;
8081
setAccountUsageError: Setter<string | undefined>;
@@ -176,14 +177,17 @@ export function useConversationResources({
176177
setLoginOpen(true);
177178
setLoginBusy(true);
178179
setDeviceCode(undefined);
180+
accountProfileRequest.current += 1;
181+
setAccountProfile(undefined);
182+
clearCachedAccountProfiles();
179183
try {
180184
const status = await invoke<AuthStatus>("login");
181185
setAuth(status);
182186
if (status.loggedIn) {
183187
setLoginOpen(false);
184188
showNotice(t("notice.loginSuccess"));
185189
void refreshModels();
186-
void loadAccountProfile();
190+
void loadAccountProfile(status.provider);
187191
}
188192
} catch (error) {
189193
showNotice(conciseError(error));
@@ -202,6 +206,7 @@ export function useConversationResources({
202206
setAccountUsageError(undefined);
203207
accountProfileRequest.current += 1;
204208
setAccountProfile(undefined);
209+
clearCachedAccountProfiles();
205210
setProfileOpen(false);
206211
showNotice(t("notice.logoutSuccess"));
207212
} catch (error) {

apps/kimi-code/src/components/AccountAvatar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { useState } from "react";
22
import { CircleUserRound } from "lucide-react";
33

4-
import type { ManagedUserInfo } from "../types";
4+
import type { AccountProfile } from "../types";
55

66
export function AccountAvatar({
77
profile,
88
size,
99
}: {
10-
profile?: ManagedUserInfo;
10+
profile?: AccountProfile;
1111
size: number;
1212
}) {
1313
const [failedUrl, setFailedUrl] = useState<string>();

apps/kimi-code/src/components/AccountUsagePopover.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77

88
import { resolveAccountMenuVisibility } from "../accountMenu";
99
import { localeTag, t } from "../i18n";
10-
import type { AccountUsage, ManagedUsageRow, ManagedUserInfo } from "../types";
10+
import type { AccountProfile, AccountUsage, ManagedUsageRow } from "../types";
1111
import { AccountAvatar } from "./AccountAvatar";
1212

1313
export function AccountUsagePopover({
@@ -21,7 +21,7 @@ export function AccountUsagePopover({
2121
onOpenSettings,
2222
onSignOut,
2323
}: {
24-
profile?: ManagedUserInfo;
24+
profile?: AccountProfile;
2525
loggedIn: boolean;
2626
usage?: AccountUsage;
2727
busy: boolean;

apps/kimi-code/src/components/AppOverlays.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import SettingsDialog from "../SettingsDialog";
77
import { CustomAgentManagerDialog } from "./CustomAgentManagerDialog";
88
import { isDesktop } from "../transport";
99
import type {
10+
AccountProfile,
1011
AccountUsage,
1112
AuthStatus,
1213
DeviceCode,
1314
GoalSnapshot,
14-
ManagedUserInfo,
1515
} from "../types";
1616
import {
1717
DirectoryPickerDialog,
@@ -40,7 +40,7 @@ interface AppOverlaysProps {
4040
agentManagerWorkspace?: { id: string; name: string };
4141
appVersion?: string;
4242
auth: AuthStatus;
43-
accountProfile?: ManagedUserInfo;
43+
accountProfile?: AccountProfile;
4444
accountUsage?: AccountUsage;
4545
accountUsageBusy: boolean;
4646
accountUsageError?: string;

0 commit comments

Comments
 (0)