Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Privacy Policy

Last updated: August 23, 2026
Last updated: August 26, 2026

AI Limits is a locally running Chrome extension by TiantianFlow. This policy
describes version 0.4.3.
Expand Down Expand Up @@ -162,7 +162,7 @@ metrics; raw provider responses are not persisted.
## Local storage and retention

AI Limits stores one application-state record in `chrome.storage.local`. It
contains display and automatic-refresh preferences, per-instance nonsecret
contains display, language, and automatic-refresh preferences, per-instance nonsecret
configuration such as a normalized configurable-provider base URL and label, provider
permission state, the normalized usage fields listed above, History, and
sanitized refresh-attempt metadata. It does not contain provider cookies,
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ consumption with elapsed time to show a pace signal. Cursor also shows weekly
Grok Bot usage next to its monthly Cursor-model and Other-model limits, plus a
per-model included-usage breakdown. It starts empty and reads usage only after
you connect an individual provider and approve that provider's optional access.
The side panel supports English and Simplified Chinese. Follow Chrome uses those
languages when Chrome is set to one of them, and falls back to English for every
other Chrome language. Settings can pin English or Simplified Chinese for this
panel.

AI Limits is an independent project by TiantianFlow. It is not affiliated with,
endorsed by, or authorized by any supported provider, its parent company, or
Expand Down
7 changes: 4 additions & 3 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ AI Limits 支持二十个服务:ChatGPT、Claude、Kimi、Cursor、Grok、Mist
服务商提供完整的重置周期信息时,还会将配额消耗与已过时间比较,显示用量节奏。
Cursor 还会在 Cursor 模型与其他模型的月度限额旁显示 Grok Bot 每周用量,并提供
按模型划分的包含用量明细。扩展初始不读取任何数据;只有当你主动连接某个服务
并批准该服务的可选访问权限后,才会读取其用量。
并批准该服务的可选访问权限后,才会读取其用量。侧边栏仅支持英文和简体中文;跟随
Chrome 时,仅在 Chrome 为英文或简体中文时使用对应语言,其他 Chrome 语言会回退到英文。
也可在 Settings 中为侧边栏固定英文或简体中文。

AI Limits 是 TiantianFlow 开发的独立项目,与任何受支持的服务商、其母公司或
关联方不存在隶属、背书或授权关系。

> 当前扩展界面为英文。下图的中文文字是项目介绍文案;嵌入的实际扩展界面
> 仍为英文。后续完成扩展本身的简体中文支持后,我们会重新生成截图。
> 下图外圈介绍文案为中文;图中嵌入的侧边栏截图目前仍是英文界面。

![AI Limits Chrome 侧边栏展示示例已用视图、订阅用量、重置时间、用量节奏和服务导航](store-assets/chrome-web-store/zh_CN/screenshot-overview-1280x800.png)

Expand Down
14 changes: 14 additions & 0 deletions entrypoints/sidepanel/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,11 @@ describe("side-panel App", () => {
name: "Disconnect relay.example · 22222222",
}),
);
fireEvent.click(
screen.getByRole("button", {
name: "Confirm disconnect relay.example · 22222222",
}),
);
expect(await screen.findByRole("status")).toHaveTextContent(
"Couldn’t disconnect relay.example · 22222222.",
);
Expand Down Expand Up @@ -1555,6 +1560,9 @@ describe("side-panel App", () => {
await screen.findByText("ChatGPT");
fireEvent.click(screen.getByRole("button", { name: "Settings" }));
fireEvent.click(screen.getByRole("button", { name: "Disconnect ChatGPT" }));
fireEvent.click(
screen.getByRole("button", { name: "Confirm disconnect ChatGPT" }),
);

expect(await screen.findByRole("status")).toHaveTextContent(
"Disconnected ChatGPT and deleted its stored usage.",
Expand Down Expand Up @@ -1584,6 +1592,9 @@ describe("side-panel App", () => {
await screen.findByText("ChatGPT");
fireEvent.click(screen.getByRole("button", { name: "Settings" }));
fireEvent.click(screen.getByRole("button", { name: "Disconnect ChatGPT" }));
fireEvent.click(
screen.getByRole("button", { name: "Confirm disconnect ChatGPT" }),
);

expect(await screen.findByRole("status")).toHaveTextContent(
"Deleted ChatGPT’s local usage. Browser access could not be removed.",
Expand Down Expand Up @@ -1615,6 +1626,9 @@ describe("side-panel App", () => {
fireEvent.click(screen.getByRole("button", { name: "Settings" }));
fireEvent.click(screen.getByRole("switch", { name: "Automatic refresh" }));
fireEvent.click(screen.getByRole("button", { name: "Disconnect ChatGPT" }));
fireEvent.click(
screen.getByRole("button", { name: "Confirm disconnect ChatGPT" }),
);
fireEvent.click(screen.getByRole("button", { name: "Delete all local data" }));
fireEvent.click(screen.getByRole("button", { name: "Confirm delete all local data" }));

Expand Down
41 changes: 41 additions & 0 deletions entrypoints/sidepanel/Cockpit.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@ describe("Cockpit", () => {
);
fireEvent.click(screen.getByRole("button", { name: "Settings" }));
fireEvent.click(screen.getByRole("button", { name: "Disconnect Work relay" }));
expect(onDisconnectInstance).not.toHaveBeenCalled();
fireEvent.click(
screen.getByRole("button", { name: "Confirm disconnect Work relay" }),
);
expect(onDisconnectInstance).toHaveBeenCalledWith(WORK_NEW_API_ID);
expect(onDisconnectInstance).not.toHaveBeenCalledWith(PERSONAL_NEW_API_ID);
});
Expand Down Expand Up @@ -1488,6 +1492,10 @@ describe("Cockpit", () => {
);
fireEvent.click(screen.getByRole("button", { name: "Settings for ChatGPT" }));
fireEvent.click(screen.getByRole("button", { name: "Disconnect ChatGPT" }));
expect(onDisconnectInstance).not.toHaveBeenCalled();
fireEvent.click(
screen.getByRole("button", { name: "Confirm disconnect ChatGPT" }),
);

expect(onDisconnectInstance).toHaveBeenCalledWith("chatgpt:default");
expect(
Expand All @@ -1505,6 +1513,35 @@ describe("Cockpit", () => {
).not.toBeInTheDocument();
});

it("keeps Settings open when disconnect is cancelled", () => {
const onDisconnectInstance = vi.fn();
render(
<Cockpit
state={createFixtureState(NOW)}
now={NOW}
onDisplayModeChange={vi.fn()}
onRefresh={vi.fn()}
onConnectProvider={vi.fn()}
onDisconnectInstance={onDisconnectInstance}
/>,
);

fireEvent.click(screen.getByRole("button", { name: "Settings" }));
fireEvent.click(screen.getByRole("button", { name: "Disconnect ChatGPT" }));
fireEvent.click(
screen.getByRole("button", { name: "Cancel disconnect ChatGPT" }),
);

expect(onDisconnectInstance).not.toHaveBeenCalled();
expect(
screen.getByRole("region", { name: "Provider settings" }),
).toBeVisible();
expect(
screen.queryByRole("group", { name: "Confirm disconnect" }),
).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Disconnect ChatGPT" })).toHaveFocus();
});

it("shows a truthful unavailable screen if the active provider disconnects", () => {
const state = createFixtureState(NOW);
const view = render(
Expand Down Expand Up @@ -2908,6 +2945,10 @@ describe("Cockpit", () => {
expect(action.parentElement).toBe(providerActions);
}
fireEvent.click(screen.getByRole("button", { name: "Disconnect Claude" }));
expect(onDisconnectProvider).not.toHaveBeenCalled();
fireEvent.click(
screen.getByRole("button", { name: "Confirm disconnect Claude" }),
);
expect(onDisconnectProvider).toHaveBeenCalledWith("claude");
});

Expand Down
6 changes: 3 additions & 3 deletions entrypoints/sidepanel/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2396,8 +2396,8 @@ section[aria-labelledby="overview-title"] .provider-list {
align-items: flex-start;
}

.settings-provider-row--renaming .settings-provider-actions {
justify-content: flex-start;
.settings-provider-row--disconnecting .settings-disconnect-confirm {
min-width: 0;
}

.settings-provider-actions > button {
Expand All @@ -2421,7 +2421,7 @@ section[aria-labelledby="overview-title"] .provider-list {
background: color-mix(in srgb, var(--surface-muted) 55%, var(--surface));
}

.settings-provider-actions > button:last-child {
.settings-provider-actions > button.settings-provider-actions__disconnect {
color: #be123c;
}

Expand Down
97 changes: 88 additions & 9 deletions entrypoints/sidepanel/views/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ export function SettingsView({
const restoreRenameFocus = useRef<ProviderInstanceId | undefined>(undefined);
const renameInputRef = useRef<HTMLInputElement>(null);
const restoreRenameInputFocus = useRef(false);
const [confirmDisconnectId, setConfirmDisconnectId] =
useState<ProviderInstanceId>();
const disconnectTriggerRefs = useRef<
Partial<Record<ProviderInstanceId, HTMLButtonElement | null>>
>({});
const restoreDisconnectFocus = useRef<ProviderInstanceId | undefined>(
undefined,
);
const labelsByInstance = instanceLabels(instances);

useEffect(() => {
Expand All @@ -129,6 +137,23 @@ export function SettingsView({
}
}, [renameError, renamePending]);

useEffect(() => {
if (
confirmDisconnectId &&
!instances.some((instance) => instance.id === confirmDisconnectId)
) {
setConfirmDisconnectId(undefined);
}
}, [confirmDisconnectId, instances]);

useEffect(() => {
if (!confirmDisconnectId && restoreDisconnectFocus.current) {
const instanceId = restoreDisconnectFocus.current;
restoreDisconnectFocus.current = undefined;
disconnectTriggerRefs.current[instanceId]?.focus();
}
}, [confirmDisconnectId]);

return (
<section className="screen settings-panel" aria-label={l10n.t("settings.screen")}>
<PageHeader
Expand Down Expand Up @@ -231,11 +256,18 @@ export function SettingsView({
instance.snapshot?.planLabel,
);
const editing = renamingInstanceId === instance.id;
const confirmingDisconnect = confirmDisconnectId === instance.id;
const inputId = `settings-label-${instance.id.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
return (
<li
key={instance.id}
className={editing ? "settings-provider-row--renaming" : undefined}
className={
editing
? "settings-provider-row--renaming"
: confirmingDisconnect
? "settings-provider-row--disconnecting"
: undefined
}
aria-label={l10n.t("settings.instanceSettings", { label })}
>
<ProviderMark providerId={instance.providerKind} size="sm" />
Expand Down Expand Up @@ -365,6 +397,7 @@ export function SettingsView({
restoreRenameInputFocus.current = false;
setRenameError("");
setLabelDraft(instance.userLabel ?? "");
setConfirmDisconnectId(undefined);
setRenamingInstanceId(instance.id);
}}
>
Expand Down Expand Up @@ -398,15 +431,61 @@ export function SettingsView({
<span aria-hidden="true">{l10n.t("common.reconnect")}</span>
</button>
) : null}
<button
type="button"
aria-label={l10n.t("settings.disconnectNamed", { label })}
data-focus-key={`settings-disconnect-${instance.id}`}
onClick={() => onDisconnectInstance(instance.id)}
>
<span aria-hidden="true">{l10n.t("common.disconnect")}</span>
</button>
{confirmingDisconnect ? null : (
<button
ref={(element) => {
disconnectTriggerRefs.current[instance.id] = element;
}}
type="button"
aria-label={l10n.t("settings.disconnectNamed", { label })}
data-focus-key={`settings-disconnect-${instance.id}`}
className="settings-provider-actions__disconnect"
onClick={() => {
setRenamingInstanceId(undefined);
setConfirmDisconnectId(instance.id);
}}
>
<span aria-hidden="true">{l10n.t("common.disconnect")}</span>
</button>
)}
</div>
{confirmingDisconnect ? (
<div
className="delete-confirmation settings-disconnect-confirm"
role="group"
aria-label={l10n.t("providerDetail.disconnectConfirmGroup")}
>
<p>{l10n.t("providerDetail.disconnectConfirmWarning")}</p>
<div className="confirmation-actions">
<button
className="button button--danger"
type="button"
aria-label={l10n.t(
"providerDetail.disconnectConfirmNamed",
{ label },
)}
autoFocus
onClick={() => onDisconnectInstance(instance.id)}
>
{l10n.t("providerDetail.disconnectConfirm")}
</button>
<button
className="button button--secondary"
type="button"
aria-label={l10n.t(
"providerDetail.disconnectCancelNamed",
{ label },
)}
onClick={() => {
restoreDisconnectFocus.current = instance.id;
setConfirmDisconnectId(undefined);
}}
>
{l10n.t("common.cancel")}
</button>
</div>
</div>
) : null}
</div>
</li>
);
Expand Down
4 changes: 2 additions & 2 deletions locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,15 +198,15 @@ settings:
automaticOn: On · about every 15 minutes
automaticOff: Off · refresh manually from the header
language: Language
languageDescription: Applies to this side panel. Chrome’s own menus and the extension name stay in Chrome’s language.
languageDescription: English and Simplified Chinese only. Other Chrome languages fall back to English. Chrome’s own menus and the extension name stay in Chrome’s language.
languageFollowChrome: Follow Chrome
connectedProviders: Connected providers
instanceSettings: "{label} settings"
identityNamed: "{provider} · {label}"
freshnessMethod: "{freshness} · {method} · {readOnly}"
apiKeySaved: API key saved
noProviders: No providers connected.
disconnectExplanation: Disconnecting removes the provider from Overview and deletes its stored local history.
disconnectExplanation: Disconnecting permanently deletes this provider's credentials, configuration, usage, and history. Permission cleanup can be delayed or shared with another instance.
renameGroup: "Rename {label}"
instanceLabel: Instance label
saveLabelNamed: "Save label for {label}"
Expand Down
4 changes: 2 additions & 2 deletions locales/zh_CN.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,15 +198,15 @@ settings:
automaticOn: 开 · 大约每 15 分钟
automaticOff: 关 · 请从顶部手动刷新
language: 语言
languageDescription: 仅作用于本侧边栏。Chrome 自身的菜单和扩展名称仍跟随 Chrome 的语言。
languageDescription: 仅支持英文和简体中文。其他 Chrome 语言会回退到英文。Chrome 自身的菜单和扩展名称仍跟随 Chrome 的语言。
languageFollowChrome: 跟随 Chrome
connectedProviders: 已连接的服务
instanceSettings: "{label} 设置"
identityNamed: "{provider} · {label}"
freshnessMethod: "{freshness} · {method} · {readOnly}"
apiKeySaved: 已保存 API 密钥
noProviders: 尚未连接服务。
disconnectExplanation: 断开连接会将该服务从总览中移除,并删除其本地历史。
disconnectExplanation: 断开连接会永久删除该服务的凭据、配置、用量和历史。权限清理可能延迟,或与另一实例共享。
renameGroup: "重命名 {label}"
instanceLabel: 实例标签
saveLabelNamed: "保存 {label} 的标签"
Expand Down
4 changes: 2 additions & 2 deletions scripts/publication-contract.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,9 @@ const requiredVisibleElevenLabsStatements = [
{
key: "listing",
statement:
"confirm the side panel opens with twenty permission-required cards.",
"confirm First Run opens with twenty opt-in provider rows and no prefilled usage.",
error:
"Store listing reviewer flow must expect twenty permission-required cards.",
"Store listing reviewer flow must expect twenty First Run provider rows.",
},
];
const requiredVisibleV030Statements = [
Expand Down
4 changes: 2 additions & 2 deletions scripts/publication-contract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@ const elevenLabsPublicationStatements = [
{
key: "listing",
statement:
"confirm the side panel opens with twenty permission-required cards.",
"confirm First Run opens with twenty opt-in provider rows and no prefilled usage.",
error:
"Store listing reviewer flow must expect twenty permission-required cards.",
"Store listing reviewer flow must expect twenty First Run provider rows.",
},
];
const v030PublicationStatements = [
Expand Down
Loading
Loading