@raingor/pi-web-switch 0.8.0 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  import { useMemo, useState, useEffect } from "react";
2
- import { Gauge, Loader2, Zap, Check, X, RotateCcw, Download } from "lucide-react";
2
+ import { Gauge, Loader2, Zap, Check, X, RotateCcw, Download, Plus, ListPlus } from "lucide-react";
3
3
  import { useTranslation } from "@/lib/i18n";
4
4
  import { useConfigStore } from "@/store/config-store";
5
5
  import { cn } from "@/lib/utils";
6
- import type { Provider } from "@/types";
6
+ import type { Provider, Model } from "@/types";
7
7
 
8
8
  // Model returned by /api/pi/provider-models. Kept local to this page — these
9
9
  // are stored separately from the provider's configured/enabled models.
@@ -63,6 +63,41 @@ function saveCatalog(catalog: Record<string, FetchedModel[]>) {
63
63
  }
64
64
  }
65
65
 
66
+ // LocalStorage key: per-provider speed-test results, so they survive
67
+ // navigating away and back (the route unmounts this page).
68
+ const RESULTS_KEY = "speedtest:model-results";
69
+ // LocalStorage key: last selected provider on this page.
70
+ const LAST_PROVIDER_KEY = "speedtest:last-provider";
71
+
72
+ type AllResults = Record<string, Record<string, ModelResult>>;
73
+
74
+ function loadResults(): AllResults {
75
+ try {
76
+ const raw = localStorage.getItem(RESULTS_KEY);
77
+ if (!raw) return {};
78
+ const all = JSON.parse(raw) as AllResults;
79
+ // Drop entries stuck in "testing" (page left mid-run) — they never finished.
80
+ for (const pid of Object.keys(all)) {
81
+ const kept = Object.fromEntries(
82
+ Object.entries(all[pid] ?? {}).filter(([, r]) => r.status === "done")
83
+ );
84
+ if (Object.keys(kept).length === 0) delete all[pid];
85
+ else all[pid] = kept;
86
+ }
87
+ return all;
88
+ } catch {
89
+ return {};
90
+ }
91
+ }
92
+
93
+ function saveResults(all: AllResults) {
94
+ try {
95
+ localStorage.setItem(RESULTS_KEY, JSON.stringify(all));
96
+ } catch {
97
+ /* ignore quota errors */
98
+ }
99
+ }
100
+
66
101
  export function ModelSpeedTestPage() {
67
102
  const { t } = useTranslation();
68
103
  const { allProviders, auth } = useConfigStore();
@@ -79,22 +114,93 @@ export function ModelSpeedTestPage() {
79
114
  );
80
115
 
81
116
  const [selectedId, setSelectedId] = useState<string | null>(
82
- testableProviders[0]?.id ?? null
117
+ () => localStorage.getItem(LAST_PROVIDER_KEY) ?? testableProviders[0]?.id ?? null
83
118
  );
84
119
  const selected = testableProviders.find((p) => p.id === selectedId) ?? testableProviders[0] ?? null;
120
+ useEffect(() => {
121
+ if (selected?.id) localStorage.setItem(LAST_PROVIDER_KEY, selected.id);
122
+ }, [selected?.id]);
85
123
 
86
124
  // Speed-test model catalog, persisted in localStorage.
87
125
  const [catalog, setCatalog] = useState<Record<string, FetchedModel[]>>({});
88
126
  useEffect(() => { setCatalog(loadCatalog()); }, []);
89
127
  const models = selected ? (catalog[selected.id] ?? []) : [];
90
128
 
91
- const [results, setResults] = useState<Map<string, ModelResult>>(new Map());
129
+ // Results persisted per provider so they survive route changes; the map
130
+ // below is the current provider's slice.
131
+ const [allResults, setAllResults] = useState<AllResults>(() => loadResults());
132
+ useEffect(() => { saveResults(allResults); }, [allResults]);
133
+ const results = useMemo(
134
+ () => new Map(Object.entries(allResults[selected?.id ?? ""] ?? {})),
135
+ [allResults, selected]
136
+ );
137
+ // setResults scoped to the selected provider (drop-in for the old state setter).
138
+ const setResults = (
139
+ next: Map<string, ModelResult> | ((prev: Map<string, ModelResult>) => Map<string, ModelResult>)
140
+ ) => {
141
+ const pid = selected?.id ?? "";
142
+ setAllResults((prevAll) => {
143
+ const prevMap = new Map(Object.entries(prevAll[pid] ?? {}));
144
+ const nextMap = typeof next === "function" ? next(prevMap) : next;
145
+ return { ...prevAll, [pid]: Object.fromEntries(nextMap) };
146
+ });
147
+ };
92
148
  const [running, setRunning] = useState(false);
93
149
  const [speedMode, setSpeedMode] = useState<SpeedMode>("normal");
94
150
  const [fetching, setFetching] = useState(false);
95
151
  const [fetchError, setFetchError] = useState<string | null>(null);
96
152
  const [fetchInfo, setFetchInfo] = useState<string | null>(null);
97
153
 
154
+ // Add-to-provider support: models already configured under this provider.
155
+ const { addModel } = useConfigStore();
156
+ const configuredIds = useMemo(
157
+ () => new Set((selected?.models ?? []).map((m) => m.id)),
158
+ [selected]
159
+ );
160
+
161
+ const toModelDef = (m: FetchedModel): Model => {
162
+ const input: Model["input"] = ["text"];
163
+ if (m.vision) input.push("image");
164
+ if (m.audio) input.push("audio");
165
+ return {
166
+ id: m.id,
167
+ name: m.name,
168
+ reasoning: m.reasoning ?? false,
169
+ input,
170
+ contextWindow: m.contextWindow ?? 262144,
171
+ maxTokens: m.maxTokens ?? 32768,
172
+ cost: m.cost
173
+ ? {
174
+ input: m.cost.input ?? 0,
175
+ output: m.cost.output ?? 0,
176
+ cacheRead: m.cost.cacheRead ?? 0,
177
+ cacheWrite: m.cost.cacheWrite ?? 0,
178
+ }
179
+ : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
180
+ };
181
+ };
182
+
183
+ // Models that passed 100% and are not configured yet — the batch-add set.
184
+ const passedNew = useMemo(
185
+ () =>
186
+ models.filter((m) => {
187
+ const r = results.get(m.id);
188
+ return !!r && r.runs > 0 && r.success === r.runs && !configuredIds.has(m.id);
189
+ }),
190
+ [models, results, configuredIds]
191
+ );
192
+
193
+ const addToProvider = async (m: FetchedModel) => {
194
+ if (!selected || configuredIds.has(m.id)) return;
195
+ // Added disabled by default — the user enables it on the providers page.
196
+ addModel(selected.id, toModelDef(m));
197
+ };
198
+
199
+ const addAllPassed = async () => {
200
+ if (!selected || running || passedNew.length === 0) return;
201
+ passedNew.forEach((m) => addModel(selected.id, toModelDef(m)));
202
+ };
203
+
98
204
  const setResult = (modelId: string, patch: Partial<ModelResult>) => {
99
205
  setResults((prev) => {
100
206
  const next = new Map(prev);
@@ -313,6 +419,15 @@ export function ModelSpeedTestPage() {
313
419
  <RotateCcw className="h-4 w-4" />
314
420
  {t("speed_test.reset")}
315
421
  </button>
422
+ <button
423
+ onClick={addAllPassed}
424
+ disabled={running || fetching || passedNew.length === 0}
425
+ className="flex items-center gap-1.5 rounded-lg border border-emerald-600/40 bg-emerald-600/10 px-3 py-2 text-sm text-emerald-300 transition-colors hover:bg-emerald-600/20 disabled:cursor-not-allowed disabled:opacity-50"
426
+ title={t("speed_test.add_all_passed_desc")}
427
+ >
428
+ <ListPlus className="h-4 w-4" />
429
+ {t("speed_test.add_all_passed")} ({passedNew.length})
430
+ </button>
316
431
  <button
317
432
  onClick={runAll}
318
433
  disabled={running || fetching || models.length === 0}
@@ -355,6 +470,7 @@ export function ModelSpeedTestPage() {
355
470
  <th className="px-3 py-2 font-medium text-right">{t("speed_test.col_avg_latency")}</th>
356
471
  <th className="px-3 py-2 font-medium text-right">{t("speed_test.col_range")}</th>
357
472
  <th className="px-3 py-2 font-medium">{t("speed_test.col_status")}</th>
473
+ <th className="px-3 py-2 font-medium text-right">{t("speed_test.col_action")}</th>
358
474
  </tr>
359
475
  </thead>
360
476
  <tbody>
@@ -412,6 +528,24 @@ export function ModelSpeedTestPage() {
412
528
  </span>
413
529
  )}
414
530
  </td>
531
+ <td className="px-3 py-2 text-right">
532
+ {rate === 100 &&
533
+ (configuredIds.has(m.id) ? (
534
+ <span className="inline-flex items-center gap-1 text-xs text-gray-500">
535
+ <Check className="h-3.5 w-3.5" />
536
+ {t("speed_test.exists")}
537
+ </span>
538
+ ) : (
539
+ <button
540
+ onClick={() => addToProvider(m)}
541
+ disabled={running || fetching}
542
+ className="inline-flex items-center gap-1 rounded-md border border-blue-600/50 bg-blue-600/10 px-2 py-1 text-xs font-medium text-blue-400 transition-colors hover:bg-blue-600/20 disabled:cursor-not-allowed disabled:opacity-50"
543
+ >
544
+ <Plus className="h-3.5 w-3.5" />
545
+ {t("speed_test.add_to_provider")}
546
+ </button>
547
+ ))}
548
+ </td>
415
549
  </tr>
416
550
  );
417
551
  })}
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from "react";
1
+ import { useEffect, useState, useMemo } from "react";
2
2
  import { useTranslation } from "@/lib/i18n";
3
3
  import { useConfigStore } from "@/store/config-store";
4
4
  import { Badge } from "@/components/ui/Badge";
@@ -189,7 +189,10 @@ function AgentList({
189
189
  searchActive: boolean;
190
190
  }) {
191
191
  const { t } = useTranslation();
192
- const [selected, setSelected] = useState<AgentDef | null>(null);
192
+ const [selectedFile, setSelectedFile] = useState<string | null>(null);
193
+ // Resolve the selected agent from the live list so it reflects saved edits
194
+ // after a refresh (matching by fileName), instead of a stale captured object.
195
+ const selected = agents.find((a) => a.fileName === selectedFile) ?? null;
193
196
 
194
197
  if (agents.length === 0) {
195
198
  return (
@@ -208,7 +211,7 @@ function AgentList({
208
211
  {agents.map((agent) => (
209
212
  <button
210
213
  key={agent.fileName}
211
- onClick={() => setSelected(agent)}
214
+ onClick={() => setSelectedFile(agent.fileName)}
212
215
  className={`w-full rounded-lg border px-3 py-3 text-left transition-colors ${
213
216
  selected?.fileName === agent.fileName
214
217
  ? "border-blue-500/30 bg-gray-800 text-white"
@@ -243,7 +246,14 @@ function AgentList({
243
246
 
244
247
  function AgentDetail({ agent, onSaved }: { agent: AgentDef; onSaved: () => void }) {
245
248
  const { t } = useTranslation();
246
- const { allModels } = useConfigStore();
249
+ const { allModels, allProviders } = useConfigStore();
250
+ // Eligible = custom providers + built-in providers with an API key saved.
251
+ const eligibleModels = useMemo(() => {
252
+ const usable = new Set(
253
+ allProviders.filter((p) => p.type === "custom" || p.hasAuth).map((p) => p.id)
254
+ );
255
+ return allModels.filter((m) => usable.has(m.providerId));
256
+ }, [allModels, allProviders]);
247
257
  const [editing, setEditing] = useState(false);
248
258
  const [model, setModel] = useState(agent.model ?? "");
249
259
  const [thinking, setThinking] = useState(agent.thinking ?? "");
@@ -307,23 +317,22 @@ function AgentDetail({ agent, onSaved }: { agent: AgentDef; onSaved: () => void
307
317
  <div className={editing ? "col-span-2" : ""}>
308
318
  <span className="text-xs text-gray-500">{t("subagents.model")}</span>
309
319
  {editing ? (
310
- <>
311
- <input
312
- type="text"
313
- list="agent-model-options"
314
- value={model}
315
- onChange={(e) => setModel(e.target.value)}
316
- placeholder={t("subagents.model_placeholder")}
317
- className="mt-1 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 font-mono text-sm text-gray-100 outline-none focus:ring-1 focus:ring-blue-500"
318
- />
319
- <datalist id="agent-model-options">
320
- {allModels.map((m) => (
321
- <option key={`${m.providerId}/${m.id}`} value={`${m.providerId}/${m.id}`}>
322
- {m.providerName} · {m.name ?? m.id}
323
- </option>
324
- ))}
325
- </datalist>
326
- </>
320
+ <select
321
+ value={model}
322
+ onChange={(e) => setModel(e.target.value)}
323
+ className="mt-1 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 font-mono text-sm text-gray-100 outline-none focus:ring-1 focus:ring-blue-500"
324
+ >
325
+ <option value="">{t("subagents.model_default")}</option>
326
+ {/* Keep a saved-but-unavailable model selectable so it isn't lost. */}
327
+ {model && !eligibleModels.some((m) => `${m.providerId}/${m.id}` === model) && (
328
+ <option value={model}>{model}</option>
329
+ )}
330
+ {eligibleModels.map((m) => (
331
+ <option key={`${m.providerId}/${m.id}`} value={`${m.providerId}/${m.id}`}>
332
+ {m.providerName} · {m.name ?? m.id}
333
+ </option>
334
+ ))}
335
+ </select>
327
336
  ) : (
328
337
  <p className="mt-0.5 text-sm text-gray-200 font-mono">{agent.model || t("subagents.model_default")}</p>
329
338
  )}
@@ -9,6 +9,21 @@ export interface ChangelogEntry {
9
9
  }
10
10
 
11
11
  export const CHANGELOG: ChangelogEntry[] = [
12
+ {
13
+ version: "0.8.3",
14
+ date: "2026-08-31",
15
+ itemKeys: ["changelog.0_8_3_1", "changelog.0_8_3_2"],
16
+ },
17
+ {
18
+ version: "0.8.2",
19
+ date: "2026-08-28",
20
+ itemKeys: ["changelog.0_8_2_1", "changelog.0_8_2_2", "changelog.0_8_2_3", "changelog.0_8_2_4"],
21
+ },
22
+ {
23
+ version: "0.8.1",
24
+ date: "2026-08-27",
25
+ itemKeys: ["changelog.0_8_1_1", "changelog.0_8_1_2", "changelog.0_8_1_3"],
26
+ },
12
27
  {
13
28
  version: "0.8.0",
14
29
  date: "2026-08-27",
package/src/index.css CHANGED
@@ -692,8 +692,7 @@ table { border-collapse: collapse; }
692
692
  align-items: center;
693
693
  justify-content: center;
694
694
  gap: 8px;
695
- width: 100%;
696
- margin-bottom: 10px;
695
+ margin: 14px 14px 0;
697
696
  padding: 9px 12px;
698
697
  border-radius: 9px;
699
698
  border: 1px solid color-mix(in srgb, #229ED9 45%, var(--card-border));
@@ -33,15 +33,20 @@ const en: Record<string, string> = {
33
33
  "providers_models.invalid_url": "Enter a valid http(s) URL",
34
34
  "providers_models.baseurl_override": "This URL overrides the built-in default (saved to models.json)",
35
35
  "providers_models.enable_all": "Enable All",
36
- "providers_models.sort_by": "Sort",
37
- "providers_models.sort_default": "Default",
38
- "providers_models.sort_family": "By family",
39
- "providers_models.sort_price_asc": "Price ↑",
40
- "providers_models.sort_price_desc": "Price ↓",
41
36
  "providers_models.disable_all": "Disable All",
42
37
  "providers_models.enabled_models_title": "Enabled Models",
43
38
  "providers_models.no_enabled_models": "No enabled models yet. Enable models in a provider's model list and they'll appear here.",
44
39
  "providers_models.api_key_env": "Uses the value of environment variable {0} as the key (not stored in plaintext)",
40
+ "providers_models.api_key_add": "Add Key",
41
+ "providers_models.api_key_count": "{0} keys",
42
+ "providers_models.api_key_active": "In use",
43
+ "providers_models.api_key_use": "Use this key",
44
+ "providers_models.api_key_show": "Show key",
45
+ "providers_models.api_key_hide": "Hide key",
46
+ "providers_models.api_key_delete": "Delete key",
47
+ "providers_models.api_key_empty": "No key yet — add one below.",
48
+ "providers_models.api_key_duplicate": "This key is already in the list",
49
+ "providers_models.api_key_switch_hint": "Pick a key to switch instantly — pi uses the one marked “In use”.",
45
50
  "providers_models.test_all": "Test All",
46
51
  "providers_models.id_exists": "ID “{0}” already exists, pick another name",
47
52
  "providers_models.id_preview": "Will be saved as ID “{0}”",
@@ -89,6 +94,11 @@ const en: Record<string, string> = {
89
94
  "speed_test.empty_catalog": "No models fetched yet",
90
95
  "speed_test.empty_catalog_desc": "Click \"Fetch Models\" to pull all models from the provider endpoint. Used only for speed testing; does not affect configured models.",
91
96
  "speed_test.testing": "Testing",
97
+ "speed_test.col_action": "Action",
98
+ "speed_test.add_to_provider": "Add",
99
+ "speed_test.exists": "Exists",
100
+ "speed_test.add_all_passed": "Add all passed",
101
+ "speed_test.add_all_passed_desc": "Add every model that passed 100% and is not yet configured to this provider",
92
102
  "speed_test.reset": "Reset",
93
103
  "speed_test.pending": "Pending",
94
104
  "speed_test.ok": "All OK",
@@ -129,6 +139,15 @@ const en: Record<string, string> = {
129
139
  "app.version": "pi-switch v0.7.0",
130
140
  "changelog.button": "Changelog",
131
141
  "changelog.title": "What's New",
142
+ "changelog.0_8_3_1": "Usage: shows the local openai-codex sign-in state under Pi.",
143
+ "changelog.0_8_3_2": "Shows official OpenAI 5-hour and 7-day quota remaining, countdowns, and exact reset timestamps.",
144
+ "changelog.0_8_2_1": "Speed-test results now persist across page switches; the last selected provider is remembered.",
145
+ "changelog.0_8_2_2": "Models that pass 100% can be added to their provider in one click (single or batch); already-configured ones are marked.",
146
+ "changelog.0_8_2_3": "Newly added models default to disabled until manually enabled.",
147
+ "changelog.0_8_2_4": "Removed the search boxes from the provider model list and the remote-model fetch dialog.",
148
+ "changelog.0_8_1_1": "Settings: removed the top stat cards (providers / enabled models / theme).",
149
+ "changelog.0_8_1_2": "Package browser gained a \"Recommended\" tab; the search tab is filterable by All/Available/Installed.",
150
+ "changelog.0_8_1_3": "Fix: memory & subagent model pickers are now real dropdowns; the subagent panel updates immediately after saving.",
132
151
  "changelog.0_8_0_1": "Settings: added a package browser dialog with \"Recommended\" and \"Search\" tabs.",
133
152
  "changelog.0_8_0_2": "Search tab fuzzy-searches npm for pi packages, filterable by All/Available/Installed.",
134
153
  "changelog.0_8_0_3": "One-click install with an installed-state indicator.",
@@ -188,6 +207,13 @@ const en: Record<string, string> = {
188
207
  // Dashboard
189
208
  "dashboard.title": "Usage Statistics",
190
209
  "dashboard.source_pi": "Pi",
210
+ "dashboard.codex_logged_in": "Signed in to openai-codex",
211
+ "dashboard.codex_not_logged_in": "Not signed in to openai-codex",
212
+ "dashboard.codex_5h": "5 hours",
213
+ "dashboard.codex_7d": "7 days",
214
+ "dashboard.codex_remaining": "{0} remaining",
215
+ "dashboard.codex_resets": "resets {0}",
216
+ "dashboard.codex_quota_unavailable": "Official quota unavailable",
191
217
  "dashboard.source_chatgpt": "ChatGPT",
192
218
  "dashboard.data_source": "Data Source",
193
219
  "dashboard.source_chatgpt_note": "Reads local calls from ~/.codex/sessions and archived_sessions.",
@@ -457,9 +483,9 @@ const en: Record<string, string> = {
457
483
  "settings.updates_failed": "Update check failed",
458
484
  "settings.update_all": "Update All",
459
485
  "settings.updating": "Updating…",
460
- "settings.update_success": "Successfully updated {0} extension(s)",
461
- "settings.update_failed_names": "{0} extension(s) failed to update: {1}",
462
- "settings.updates_hint": "Extensions can be updated by reinstalling in pi (or running npm update in ~/.pi/agent/npm); update pi core via its installer.",
486
+ "settings.update_success": "Successfully updated {0} item(s)",
487
+ "settings.update_failed_names": "{0} item(s) failed to update: {1}",
488
+ "settings.updates_hint": "Extensions are updated with npm install in ~/.pi/agent/npm; pi core is updated by running `pi update`. Restart pi after updating.",
463
489
  "settings.defaults": "Defaults",
464
490
  "settings.default_provider": "Default Provider",
465
491
  "settings.default_model": "Default Model",
@@ -31,15 +31,20 @@ const ja: Record<string, string> = {
31
31
  "providers_models.invalid_url": "有効な http(s) URL を入力してください",
32
32
  "providers_models.baseurl_override": "この URL は内蔵デフォルトを上書きします(models.json に保存)",
33
33
  "providers_models.enable_all": "すべて有効化",
34
- "providers_models.sort_by": "並び順",
35
- "providers_models.sort_default": "デフォルト",
36
- "providers_models.sort_family": "ファミリー順",
37
- "providers_models.sort_price_asc": "価格昇順",
38
- "providers_models.sort_price_desc": "価格降順",
39
34
  "providers_models.disable_all": "すべて無効化",
40
35
  "providers_models.enabled_models_title": "有効なモデル",
41
36
  "providers_models.no_enabled_models": "有効なモデルはまだありません。プロバイダーのモデルリストでモデルを有効にすると、ここに表示されます。",
42
37
  "providers_models.api_key_env": "環境変数 {0} の値をキーとして使用します(平文保存されません)",
38
+ "providers_models.api_key_add": "キーを追加",
39
+ "providers_models.api_key_count": "キー {0} 件",
40
+ "providers_models.api_key_active": "使用中",
41
+ "providers_models.api_key_use": "このキーを使用",
42
+ "providers_models.api_key_show": "キーを表示",
43
+ "providers_models.api_key_hide": "キーを非表示",
44
+ "providers_models.api_key_delete": "キーを削除",
45
+ "providers_models.api_key_empty": "キーがまだありません。下で追加してください。",
46
+ "providers_models.api_key_duplicate": "このキーは既にリストにあります",
47
+ "providers_models.api_key_switch_hint": "選択するとすぐに切り替わります。pi は「使用中」のキーを使います。",
43
48
  "providers_models.test_all": "すべてテスト",
44
49
  "providers_models.id_exists": "ID “{0}” は既に存在します。別の名前を使用してください",
45
50
  "providers_models.id_preview": "ID “{0}” として保存されます",
@@ -87,6 +92,11 @@ const ja: Record<string, string> = {
87
92
  "speed_test.empty_catalog": "モデル未取得",
88
93
  "speed_test.empty_catalog_desc": "「モデルを取得」をクリックしてプロバイダーから全モデルを取得します。速度テスト専用で、設定済みモデルには影響しません。",
89
94
  "speed_test.testing": "テスト中",
95
+ "speed_test.col_action": "操作",
96
+ "speed_test.add_to_provider": "追加",
97
+ "speed_test.exists": "存在",
98
+ "speed_test.add_all_passed": "合格を一括追加",
99
+ "speed_test.add_all_passed_desc": "100%合格かつ未設定のモデルをすべてこのプロバイダーに追加",
90
100
  "speed_test.reset": "リセット",
91
101
  "speed_test.pending": "待機中",
92
102
  "speed_test.ok": "すべて成功",
@@ -127,6 +137,15 @@ const ja: Record<string, string> = {
127
137
  "app.version": "pi-switch v0.7.0",
128
138
  "changelog.button": "変更履歴",
129
139
  "changelog.title": "更新情報",
140
+ "changelog.0_8_3_1": "使用状況:Pi の下にローカル openai-codex ログイン状態を表示。",
141
+ "changelog.0_8_3_2": "OpenAI公式の5時間・7日間クォータ残量、カウントダウン、正確なリセット日時を表示。",
142
+ "changelog.0_8_2_1": "速度テスト結果をページ切替後も保持し、前回選択したプロバイダーも記憶。",
143
+ "changelog.0_8_2_2": "100%合格モデルをワンクリックでプロバイダーに追加(単発/一括)、設定済みは自動マーク。",
144
+ "changelog.0_8_2_3": "追加したモデルはデフォルト無効、手動で有効化。",
145
+ "changelog.0_8_2_4": "プロバイダーのモデル一覧とリモート取得ダイアログの検索ボックスを削除。",
146
+ "changelog.0_8_1_1": "設定:上部の統計カード(プロバイダー / 有効モデル / テーマ)を削除。",
147
+ "changelog.0_8_1_2": "パッケージ検索に「おすすめ」タブを追加。検索タブはすべて/未インストール/インストール済みで絞り込み可能。",
148
+ "changelog.0_8_1_3": "修正:メモリとサブエージェントのモデル選択を実際のドロップダウンに。サブエージェントは保存後すぐ反映。",
130
149
  "changelog.0_8_0_1": "設定:「おすすめ」と「検索」タブを備えたパッケージ検索ダイアログを追加。",
131
150
  "changelog.0_8_0_2": "検索タブは npm の pi パッケージをあいまい検索し、すべて/未インストール/インストール済みで絞り込み可能。",
132
151
  "changelog.0_8_0_3": "ワンクリックインストールとインストール済み表示。",
@@ -185,6 +204,13 @@ const ja: Record<string, string> = {
185
204
 
186
205
  "dashboard.title": "使用統計",
187
206
  "dashboard.source_pi": "Pi",
207
+ "dashboard.codex_logged_in": "openai-codex にログイン済み",
208
+ "dashboard.codex_not_logged_in": "openai-codex に未ログイン",
209
+ "dashboard.codex_5h": "5時間",
210
+ "dashboard.codex_7d": "7日間",
211
+ "dashboard.codex_remaining": "残り {0}",
212
+ "dashboard.codex_resets": "リセット {0}",
213
+ "dashboard.codex_quota_unavailable": "公式クォータは一時利用不可",
188
214
  "dashboard.source_chatgpt": "ChatGPT",
189
215
  "dashboard.data_source": "データソース",
190
216
  "dashboard.source_chatgpt_note": "~/.codex/sessions と archived_sessions のローカル呼び出し記録を読み取ります。",
@@ -449,9 +475,9 @@ const ja: Record<string, string> = {
449
475
  "settings.updates_failed": "アップデート確認に失敗しました",
450
476
  "settings.update_all": "一括更新",
451
477
  "settings.updating": "更新中…",
452
- "settings.update_success": "{0} 件の拡張を更新しました",
453
- "settings.update_failed_names": "{0} 件の拡張の更新に失敗:{1}",
454
- "settings.updates_hint": "拡張は pi での再インストール(または ~/.pi/agent/npm npm update)で更新できます。pi 本体はインストーラーから更新してください。",
478
+ "settings.update_success": "{0} 件を更新しました",
479
+ "settings.update_failed_names": "{0} 件の更新に失敗:{1}",
480
+ "settings.updates_hint": "拡張は ~/.pi/agent/npm での npm install で更新します。pi 本体は `pi update` で更新します。更新後は pi を再起動してください。",
455
481
  "settings.defaults": "デフォルト",
456
482
  "settings.default_provider": "デフォルトプロバイダー",
457
483
  "settings.default_model": "デフォルトモデル",
@@ -31,15 +31,20 @@ const zhCN: Record<string, string> = {
31
31
  "providers_models.invalid_url": "请输入合法的 http(s) 地址",
32
32
  "providers_models.baseurl_override": "此地址将覆盖内置默认地址(写入 models.json)",
33
33
  "providers_models.enable_all": "全部启用",
34
- "providers_models.sort_by": "排序",
35
- "providers_models.sort_default": "默认",
36
- "providers_models.sort_family": "按厂商",
37
- "providers_models.sort_price_asc": "价格升序",
38
- "providers_models.sort_price_desc": "价格降序",
39
34
  "providers_models.disable_all": "全部禁用",
40
35
  "providers_models.enabled_models_title": "已启用模型",
41
36
  "providers_models.no_enabled_models": "暂无启用的模型。在供应商的模型列表中开启模型后,会显示在这里。",
42
37
  "providers_models.api_key_env": "将以环境变量 {0} 的值作为密钥(不会明文写入)",
38
+ "providers_models.api_key_add": "添加密钥",
39
+ "providers_models.api_key_count": "共 {0} 个密钥",
40
+ "providers_models.api_key_active": "使用中",
41
+ "providers_models.api_key_use": "使用此密钥",
42
+ "providers_models.api_key_show": "显示密钥",
43
+ "providers_models.api_key_hide": "隐藏密钥",
44
+ "providers_models.api_key_delete": "删除密钥",
45
+ "providers_models.api_key_empty": "还没有密钥,请在下方添加。",
46
+ "providers_models.api_key_duplicate": "该密钥已在列表中",
47
+ "providers_models.api_key_switch_hint": "选中即可立即切换,pi 会使用标记为「使用中」的那一个。",
43
48
  "providers_models.test_all": "测试全部",
44
49
  "providers_models.id_exists": "ID “{0}” 已存在,请换一个名称",
45
50
  "providers_models.id_preview": "将以 ID “{0}” 保存",
@@ -87,6 +92,11 @@ const zhCN: Record<string, string> = {
87
92
  "speed_test.empty_catalog": "尚未获取模型列表",
88
93
  "speed_test.empty_catalog_desc": "点击“一键获取模型”从供应商接口拉取全部模型,仅用于测速,不影响已配置的模型。",
89
94
  "speed_test.testing": "测速中",
95
+ "speed_test.col_action": "操作",
96
+ "speed_test.add_to_provider": "加入",
97
+ "speed_test.exists": "已存在",
98
+ "speed_test.add_all_passed": "一键加入全部通过",
99
+ "speed_test.add_all_passed_desc": "把测速 100% 通过且尚未配置的模型全部加入该供应商",
90
100
  "speed_test.reset": "重置",
91
101
  "speed_test.pending": "待测",
92
102
  "speed_test.ok": "全部成功",
@@ -127,6 +137,15 @@ const zhCN: Record<string, string> = {
127
137
  "app.version": "pi-switch v0.7.0",
128
138
  "changelog.button": "更新日志",
129
139
  "changelog.title": "版本更新说明",
140
+ "changelog.0_8_3_1": "使用统计:Pi 程序下显示 openai-codex 的本地登录状态。",
141
+ "changelog.0_8_3_2": "显示 OpenAI 官方 5小时与7天额度剩余百分比、倒计时及准确重置日期时间。",
142
+ "changelog.0_8_2_1": "测速结果持久化:切换页面再回来结果不丢,并记住上次选中的供应商。",
143
+ "changelog.0_8_2_2": "测速 100% 通过的模型支持一键加入对应供应商(单个/批量),已存在自动标记。",
144
+ "changelog.0_8_2_3": "新添加的模型默认为禁用状态,需手动启用。",
145
+ "changelog.0_8_2_4": "移除供应商模型列表与远程模型获取弹窗中的搜索框。",
146
+ "changelog.0_8_1_1": "设置页移除顶部统计卡片(供应商 / 已启用模型 / 当前主题)。",
147
+ "changelog.0_8_1_2": "包浏览弹窗新增「推荐安装」标签页,搜索标签支持按全部/未安装/已安装筛选。",
148
+ "changelog.0_8_1_3": "修复:记忆与子代理的模型选择改为真正的下拉;子代理保存后面板即时更新。",
130
149
  "changelog.0_8_0_1": "设置:新增扩展包浏览弹窗,含「推荐安装」与「搜索」两个标签页。",
131
150
  "changelog.0_8_0_2": "搜索标签支持模糊搜索 npm 上的 pi 扩展包,并可按全部/未安装/已安装筛选。",
132
151
  "changelog.0_8_0_3": "扩展包一键安装,已安装状态自动标识。",
@@ -185,6 +204,13 @@ const zhCN: Record<string, string> = {
185
204
 
186
205
  "dashboard.title": "使用统计",
187
206
  "dashboard.source_pi": "Pi 程序",
207
+ "dashboard.codex_logged_in": "已登录 openai-codex",
208
+ "dashboard.codex_not_logged_in": "未登录 openai-codex",
209
+ "dashboard.codex_5h": "5小时",
210
+ "dashboard.codex_7d": "7天",
211
+ "dashboard.codex_remaining": "剩余 {0}",
212
+ "dashboard.codex_resets": "重置于 {0}",
213
+ "dashboard.codex_quota_unavailable": "官方额度暂不可用",
188
214
  "dashboard.source_chatgpt": "ChatGPT",
189
215
  "dashboard.data_source": "数据来源",
190
216
  "dashboard.source_chatgpt_note": "读取 ~/.codex/sessions 与 archived_sessions 中的本地调用记录。",
@@ -449,9 +475,9 @@ const zhCN: Record<string, string> = {
449
475
  "settings.updates_failed": "检查更新失败",
450
476
  "settings.update_all": "一键更新",
451
477
  "settings.updating": "更新中…",
452
- "settings.update_success": "已成功更新 {0} 个扩展",
453
- "settings.update_failed_names": "{0} 个扩展更新失败:{1}",
454
- "settings.updates_hint": "扩展可通过 pi 重新安装(或在 ~/.pi/agent/npm 执行 npm update)更新;pi 本体请通过安装器更新。",
478
+ "settings.update_success": "已成功更新 {0} ",
479
+ "settings.update_failed_names": "{0} 项更新失败:{1}",
480
+ "settings.updates_hint": "扩展通过 ~/.pi/agent/npm 里的 npm install 更新;pi 本体通过执行 `pi update` 更新。更新后请重启 pi",
455
481
  "settings.defaults": "默认值",
456
482
  "settings.default_provider": "默认提供商",
457
483
  "settings.default_model": "默认模型",
@@ -31,15 +31,20 @@ const zhTW: Record<string, string> = {
31
31
  "providers_models.invalid_url": "請輸入合法的 http(s) 位址",
32
32
  "providers_models.baseurl_override": "此位址將覆蓋內建預設位址(寫入 models.json)",
33
33
  "providers_models.enable_all": "全部啟用",
34
- "providers_models.sort_by": "排序",
35
- "providers_models.sort_default": "預設",
36
- "providers_models.sort_family": "依廠商",
37
- "providers_models.sort_price_asc": "價格升序",
38
- "providers_models.sort_price_desc": "價格降序",
39
34
  "providers_models.disable_all": "全部停用",
40
35
  "providers_models.enabled_models_title": "已啟用模型",
41
36
  "providers_models.no_enabled_models": "尚無啟用的模型。在供應商的模型清單中開啟模型後,會顯示在這裡。",
42
37
  "providers_models.api_key_env": "將以環境變數 {0} 的值作為密鑰(不會明碼寫入)",
38
+ "providers_models.api_key_add": "新增密鑰",
39
+ "providers_models.api_key_count": "共 {0} 個密鑰",
40
+ "providers_models.api_key_active": "使用中",
41
+ "providers_models.api_key_use": "使用此密鑰",
42
+ "providers_models.api_key_show": "顯示密鑰",
43
+ "providers_models.api_key_hide": "隱藏密鑰",
44
+ "providers_models.api_key_delete": "刪除密鑰",
45
+ "providers_models.api_key_empty": "尚無密鑰,請在下方新增。",
46
+ "providers_models.api_key_duplicate": "該密鑰已在清單中",
47
+ "providers_models.api_key_switch_hint": "選取即可立即切換,pi 會使用標記為「使用中」的那一個。",
43
48
  "providers_models.test_all": "測試全部",
44
49
  "providers_models.id_exists": "ID “{0}” 已存在,請換一個名稱",
45
50
  "providers_models.id_preview": "將以 ID “{0}” 儲存",
@@ -86,6 +91,11 @@ const zhTW: Record<string, string> = {
86
91
  "speed_test.empty_catalog": "尚未取得模型列表",
87
92
  "speed_test.empty_catalog_desc": "點擊「一鍵取得模型」從供應商接口拉取全部模型,僅用於測速,不影響已配置的模型。",
88
93
  "speed_test.testing": "測速中",
94
+ "speed_test.col_action": "操作",
95
+ "speed_test.add_to_provider": "加入",
96
+ "speed_test.exists": "已存在",
97
+ "speed_test.add_all_passed": "一鍵加入全部通過",
98
+ "speed_test.add_all_passed_desc": "把測速 100% 通過且尚未配置的模型全部加入該供應商",
89
99
  "speed_test.reset": "重置",
90
100
  "speed_test.pending": "待測",
91
101
  "speed_test.ok": "全部成功",
@@ -126,6 +136,15 @@ const zhTW: Record<string, string> = {
126
136
  "app.version": "pi-switch v0.7.0",
127
137
  "changelog.button": "更新日誌",
128
138
  "changelog.title": "版本更新說明",
139
+ "changelog.0_8_3_1": "使用統計:Pi 程式下顯示 openai-codex 的本機登入狀態。",
140
+ "changelog.0_8_3_2": "顯示 OpenAI 官方 5小時與7天額度剩餘百分比、倒數及準確重置日期時間。",
141
+ "changelog.0_8_2_1": "測速結果持久化:切換頁面再回來結果不丟,並記住上次選中的供應商。",
142
+ "changelog.0_8_2_2": "測速 100% 通過的模型支援一鍵加入對應供應商(單個/批次),已存在自動標記。",
143
+ "changelog.0_8_2_3": "新添加的模型預設為停用狀態,需手動啟用。",
144
+ "changelog.0_8_2_4": "移除供應商模型列表與遠端模型取得彈窗中的搜尋框。",
145
+ "changelog.0_8_1_1": "設定頁移除頂部統計卡片(供應商 / 已啟用模型 / 當前主題)。",
146
+ "changelog.0_8_1_2": "套件瀏覽彈窗新增「推薦安裝」標籤頁,搜尋標籤支援按全部/未安裝/已安裝篩選。",
147
+ "changelog.0_8_1_3": "修復:記憶與子代理的模型選擇改為真正的下拉;子代理儲存後面板即時更新。",
129
148
  "changelog.0_8_0_1": "設定:新增擴充包瀏覽彈窗,含「推薦安裝」與「搜尋」兩個標籤頁。",
130
149
  "changelog.0_8_0_2": "搜尋標籤支援模糊搜尋 npm 上的 pi 擴充包,並可按全部/未安裝/已安裝篩選。",
131
150
  "changelog.0_8_0_3": "擴充包一鍵安裝,已安裝狀態自動標示。",
@@ -184,6 +203,13 @@ const zhTW: Record<string, string> = {
184
203
 
185
204
  "dashboard.title": "使用統計",
186
205
  "dashboard.source_pi": "Pi 程式",
206
+ "dashboard.codex_logged_in": "已登入 openai-codex",
207
+ "dashboard.codex_not_logged_in": "未登入 openai-codex",
208
+ "dashboard.codex_5h": "5小時",
209
+ "dashboard.codex_7d": "7天",
210
+ "dashboard.codex_remaining": "剩餘 {0}",
211
+ "dashboard.codex_resets": "重置於 {0}",
212
+ "dashboard.codex_quota_unavailable": "官方額度暫時不可用",
187
213
  "dashboard.source_chatgpt": "ChatGPT",
188
214
  "dashboard.data_source": "資料來源",
189
215
  "dashboard.source_chatgpt_note": "讀取 ~/.codex/sessions 與 archived_sessions 中的本機呼叫記錄。",
@@ -448,9 +474,9 @@ const zhTW: Record<string, string> = {
448
474
  "settings.updates_failed": "檢查更新失敗",
449
475
  "settings.update_all": "一鍵更新",
450
476
  "settings.updating": "更新中…",
451
- "settings.update_success": "已成功更新 {0} 個擴充",
452
- "settings.update_failed_names": "{0} 個擴充更新失敗:{1}",
453
- "settings.updates_hint": "擴充可透過 pi 重新安裝(或在 ~/.pi/agent/npm 執行 npm update)更新;pi 本體請透過安裝器更新。",
477
+ "settings.update_success": "已成功更新 {0} ",
478
+ "settings.update_failed_names": "{0} 項更新失敗:{1}",
479
+ "settings.updates_hint": "擴充透過 ~/.pi/agent/npm 裡的 npm install 更新;pi 本體透過執行 `pi update` 更新。更新後請重啟 pi",
454
480
  "settings.defaults": "預設值",
455
481
  "settings.default_provider": "預設提供商",
456
482
  "settings.default_model": "預設模型",