diff --git a/devlog/_plan/260906_aside_profiles/020_profiles_gui.md b/devlog/_plan/260906_aside_profiles/020_profiles_gui.md index 16e5798e04..974d91f335 100644 --- a/devlog/_plan/260906_aside_profiles/020_profiles_gui.md +++ b/devlog/_plan/260906_aside_profiles/020_profiles_gui.md @@ -13,3 +13,21 @@ UPDATE guides/integrations.md and operating CLI docs with all-profile default, - Verification: remote focused GUI/API tests, GUI lint/i18n/build, root typecheck and required CI. Browser QA on local dev UI against three synthetic profiles, never real user profile mutation: initial mixed state, global enable, one profile disable, return to list after details, correct request selector, reload retains off state, failed profile does not imply sibling success, keyboard switches and narrow viewport. Capture actual screenshot for PR body using existing browser plugin, view it, and fix layout if needed. Screenshot contains synthetic labels only. A PR mentioning GUI includes screenshot. No local test suite or local typecheck; local dev server/browser probes are permitted. Terminal: verify every PR current head and all applicable hosted checks; native stack registration, owner-authorized admin merge, async merge completion, fetch dev and prove every merge SHA ancestry. Resolve CI or reviews rather than bypass evidence. No release or live service deployment. All original Grok/Pi/Codex and added Aside-profile criteria must be met before host goal completion. + +## P revalidation at1d4da9f9b + +Backend primary routes are now dedicated nested profile paths; server-only POST /aside/sync owns synchronization and preserves exclusions. The UI Sync-now button uses it, while the bulk switch sets all desired flags and per-row switches set one. Shared Switch already supports mixed state and aria-pressed. Preserve existing monochrome tokens/ClientMark (variance2/motion1/densityD5). StatusDTO carries desired enabled plus actual state, counts and per-profile errors; show partial/error states explicitly. FileIntegrationPage and RestoreDialog/Overview actions carry optional profile IDs into paths and cache keys. + +Main owns AsideProfilesPage, integration-api profile DTO/functions, parentpage/FileIntegrationPage/RestoreDialog/Overview wiring and CSS. A disjoint locale/test worker may own gui/src/i18n/{en,de,fr,ko,zh,zh-TW,ru,ja,tr}.ts and new gui/tests/aside-profiles-page.test.tsx after exactkeys/APIcontract are fixed. All GUI checks run remotely; local Vite and browser probes only. Capture wide+narrow realcomponent screenshots against synthetic three-profile management fixtures, not real accounts. + +A audit passed: DTO error precedes empty rendering; stale errors remain visible; always refetch after refused mutations because intent may already be saved; use mixed Switch and void refresh semantics. Exact locale keys are locked in ignored .tmp/aside-profiles/ui-keys.json. Main exports loadAsideProfiles and syncAsideProfiles from integration-api, and adds profileId as last optional argument to existing state/toggle/history/restore/delete functions. Toggle rejects207 partial only through returnedokfalse; UI reports it and refetches. + +Implementation modularization: profile DTO validation and load/sync readers live in new aside-profile-api.ts, reusing the existing integration transport/error owner without a circular re-export. Existing integration-api functions keep their non-Aside signatures and unscoped cache identities; profile scope is an optional final argument. Scoped successful state/toggle/restore/delete responses and journal rows must match the requested profile. + +Review fold-back: per-profile refusal/recovery outcomes remain typed and visible in bulk and Sync-now failures, including snapshotPath/residual; failed Aside restore reconciles owner resources while retaining the dialog/error; list and detail share one pure profile-status validator. Add a localized no-snapshot recovery warning and the corresponding regression cases. No source permission or confirmation boundary is weakened. + +B verification: the final interface provides bulk and individual desired-state switches, actual applied counts, per-profile retry/refusal and recovery details, profile-scoped history/restore/delete, and nine-locale copy. Remote checks passed: 74 interface/API cases, then 54 affected interface/cache cases after type narrowing, GUI build and both lint commands; 27 engine cases with real failed compensation, 62 CLI cases, and root typecheck. Initial failing compensation fixture was corrected to fail ownership after a successful file write, then fail rollback. Browser probes on three synthetic profiles covered bulk enable, exclusion persistence, scoped Undo, keyboard control, narrow layout without horizontal overflow, and per-profile external-edit refusal. Screenshot: docs-site/public/screenshots/aside-profiles.jpg. No real Aside profile files were written. Hosted exact-head CI and stack landing remain open. + +Hosted-review follow-up stays within terminal stabilization: Models selection/preset consumers must distinguish saved selection from refused client-file refresh. Add a persistent warning listing each failed client/profile and recovery details while retaining truthful selection-success feedback; clear it after a later successful refresh result. Failed HTTP saves never claim saved selection. Reuse shared refusal formatting and add nine-locale copy, focused interface cases and browser evidence. The ordinary owned-refresh producer also preserves backup/residual metadata. Native schema review follow-up uses structural object-key equality while retaining array order and true-conflict refusal; test both explicit and loaded duplicate declarations. These are review repairs to the existing stack slices, not new product scope. + +Final review verification: model-warning fixes passed 35 remote interface cases, build and lint; 80 schema/owned-refresh cases passed. Full remote interface suite passed 1,484 cases with one ownership-path copy failure; the missing per-profile path was restored in all nine locale sentences, and the unchanged five-case locale-parity file, build and i18n lint then passed. No test was weakened. Browser evidence confirms a saved selection with an independent affected-profile warning and clears the warning after a subsequent successful refresh. Independent reviews passed for the interface recovery, registry reconciliation, model-warning concurrency/fallback behavior and structural schema comparator. A complete source hash manifest matched 576e6b557 for the final root typecheck and privacy scan. Hosted exact-head checks and merge ancestry remain terminal evidence to capture in the session ledger. diff --git a/docs-site/public/screenshots/aside-profiles.jpg b/docs-site/public/screenshots/aside-profiles.jpg new file mode 100644 index 0000000000..7e78846321 Binary files /dev/null and b/docs-site/public/screenshots/aside-profiles.jpg differ diff --git a/docs-site/public/screenshots/models-client-refresh-warning.jpg b/docs-site/public/screenshots/models-client-refresh-warning.jpg new file mode 100644 index 0000000000..2b5a79a8b1 Binary files /dev/null and b/docs-site/public/screenshots/models-client-refresh-warning.jpg differ diff --git a/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md index c3b47efe21..0c95908206 100644 --- a/docs-site/src/content/docs/guides/integrations.md +++ b/docs-site/src/content/docs/guides/integrations.md @@ -218,12 +218,24 @@ ocx mcode Once connected, `ocx sync` refreshes owned MCode, Pi, and Aside catalogs with the current model selection, context windows, and reasoning-effort ladders. Changes to model visibility, -provider selection, or presets also refresh connected Pi and Aside catalogs. Missing, -foreign-edited, unsafe, and never-owned blocks stay untouched; reconnect them explicitly. +provider selection, or presets also refresh connected Pi and Aside catalogs. Foreign-edited +or unsafe blocks stay untouched, as do previously owned blocks you removed manually. +An enabled Aside profile is an exception to the usual owned-only refresh: if its account +directory exists and it has never had an owned block, sync may create its first block when +that slot is empty. A prior Aside connection enables this behavior for all registered +profiles by default. Sync does not create missing account directories or replace manual blocks. A refused or overlapping refresh is reported separately for each client. Start a new Pi session or fully quit and reopen Aside to load the updated file. Aside refresh requires a [compatible running proxy](#aside-profile-controls). +If Models reports **“Model selection saved”** together with a client-refresh warning, the +selection is already saved; one or more client files could not be updated. The warning names +the affected client and Aside profile, when applicable, and explains the refusal. Open +**Integrations** to inspect that client or profile before starting a new session. Resolve the +reported issue, then retry `ocx sync`; an overlapping operation must finish first. If the +warning includes a backup path or says recovery did not finish, inspect that recovery state +before retrying. A successful selection save alone does not confirm client-file recovery. + The separate MiniMax platform CLI (`mmx`) is not a file-toggle integration. Its text commands use MiniMax's Anthropic-compatible endpoint, so OpenCodex provides a credential-isolated, loopback-only launcher: @@ -274,6 +286,12 @@ refusal are reported for each profile. A partial bulk result is not an all-appli the CLI exits nonzero. Undo restores the selected profile's synchronization intent as well as its file, so a later sync does not silently reverse Undo. +The [profile API](/reference/management-api/#aside-profile-controls) returns HTTP 200 for a +successful bulk operation and HTTP 207 with `ok: false` if any profile refuses. Inspect every +entry in `results`: successful profiles are not rolled back when another fails. Desired +settings remain saved, so retry after addressing the affected profile rather than assuming +the entire change failed. If saving those settings fails, no profile files are changed. + Each profile has separate ownership and history. Existing user edits, unsafe paths and linked catalogs are refused; the existing explicit overwrite and drift-confirmation controls remain available. Fully quit and reopen Aside to load changed model files. diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index a46849900e..2a3ddd2787 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -94,7 +94,47 @@ For the concepts behind the model roster and encrypted worker-task behavior, see | `DELETE /api/client-integrations/journal?opId=...` | Retire one older rollback operation and remove its snapshot when possible. Success returns `snapshotRemoved`; `false` means cleanup was retained for maintenance retry. | 400 missing `opId`; 404 missing or already retired operation; 409 newest operation for that client | Deletion appends a tombstone instead of rewriting the journal. The newest operation for each client -is protected server-side so the current undo point remains available. +is protected server-side so the current undo point remains available. For Aside, protection is +per profile, and journal rows include `profileId`. + +### Aside profile controls + +Use these dedicated paths with a compatible running proxy. `{profileId}` is a registered +nonnegative integer account ID returned by the profile list; it is not a browser path. + +| Method and path | Purpose | Notable errors | +| --- | --- | --- | +| `GET /api/client-integrations/aside/profiles` | List `profiles[]`, desired `enabledCount`/`allEnabled`, actual `appliedCount`, and `total` | HTTP 200 may contain an empty, unsafe aggregate with an `error` when discovery is unavailable | +| `PUT /api/client-integrations/aside/profiles` | Set every registered profile's desired state and apply it; body `{ "enabled": true }`, with optional `overwriteConflict` when enabling | 400 invalid body; 409 operation busy; 500 preference-save failure; 207 per-profile refusals | +| `GET /api/client-integrations/aside/profiles/{profileId}` | Read one profile's desired `enabled` and actual integration status | 400 invalid ID; 404 unregistered profile | +| `PUT /api/client-integrations/aside/profiles/{profileId}` | Change one profile with the same body as bulk PUT, leaving sibling preferences unchanged | 400 invalid body/ID; 404 unknown profile; 409 busy or refusal; 500 save/write failure | +| `GET /api/client-integrations/aside/profiles/journal` | List history across registered Aside profiles | Rows include `profileId`, snapshot availability, `undoable`, and `deletable` | +| `GET /api/client-integrations/aside/profiles/{profileId}/journal` | List one profile's history, including matching legacy operations | 400 invalid ID; 404 unknown profile | +| `DELETE /api/client-integrations/aside/profiles/journal?opId=...` | Retire an older operation, resolving its profile from history | 400 missing ID; 404 missing operation; 409 newest operation for its profile | +| `DELETE /api/client-integrations/aside/profiles/{profileId}/journal?opId=...` | Retire an older operation belonging to the selected profile | Same deletion errors; an operation cannot target a different profile | +| `POST /api/client-integrations/aside/profiles/{profileId}/restore` | Undo an operation using `{ "opId": "..." }`; optional `confirmDrift: true` permits replacing later edits | 404 missing operation/profile; 409 busy, mismatch, or required drift confirmation; 410 expired snapshot; 500 save/write failure | +| `POST /api/client-integrations/aside/sync` | Refresh enabled profiles through the server's mutation owner; body `{}` | 400 nonempty body/profile selector; 409 busy; 207 per-profile refusals | + +Bulk PUT returns `{ ok, clientId, changed, state, message, results }`; each result identifies +its `profileId` and reports the writer outcome. Sync returns `{ ok, clientId, results }`, with +per-profile refresh outcomes. Both return HTTP 200 when all returned attempts succeed and +HTTP 207 with `ok: false` when any attempt refuses. HTTP 207 is a partial-result envelope, +including when every attempted profile refuses: inspect each result rather than treating a +2xx response as complete success. A successful no-op can have `changed: false`; sync does not +attempt disabled profiles. Single-profile writes and restores return HTTP 200 on success or +the corresponding error status on refusal. + +Explicit changes save desired preferences before writing files. A preference-save failure +leaves profile files unchanged. A later file refusal preserves saved intent and successful +sibling writes; inspect the affected profile before retrying. Refusals may include +`snapshotPath` and `residual: true` when recovery did not finish. Restore also reconciles the +target profile's desired state, so the next sync does not reverse Undo. Deletion returns +`snapshotRemoved`; `false` means snapshot cleanup still needs maintenance. + +The legacy `GET, PUT /api/client-integrations/aside` aliases remain available. New clients +should use the dedicated paths above so an older proxy cannot ignore a profile selector. +See [Aside profile controls](/guides/integrations/#aside-profile-controls) for CLI commands and +the proxy upgrade, restart, and retry sequence. ### Combos @@ -206,6 +246,16 @@ manual model. Valid PUT requests to `/api/selected-models` and `/api/model-presets` return HTTP 409 with code `initial_model_selection_pending` until a reliable initial model list is available. Refresh model discovery (for example, `GET /api/models`) and retry after it succeeds. +Successful visibility/selection writes to `/api/disabled-models`, `/api/model-visibility`, +`/api/selected-models`, and `/api/model-presets` report follow-up outcomes in `catalogRefresh` +and `clientIntegrations` when that refresh path runs. HTTP 200 and `ok: true` confirm the +selection save; they do not guarantee every client catalog updated. Inspect +`clientIntegrations[]` for `ok: false`, `client`, optional Aside `profileId`, and the refusal +`reason`; recovery details may also include `refusalReason`, `snapshotPath`, and `residual`. +The Models page keeps the saved selection and shows a separate client-refresh warning. +Inspect Integrations and resolve the reported issue before retrying `ocx sync`. Missing +outcome fields from an older server do not establish successful recovery. + ### OAuth accounts, provider keys, and data-plane keys | Method and path | Purpose | Notable errors | diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index b670ccda4c..69cafb05d5 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -601,6 +601,7 @@ export const de: Record = { "models.tipActive": "Aktiv", "models.tipDisabled": "Deaktiviert", "models.applied": "Angewendet — greift bei der nächsten Codex-Runde.", + "models.integrationRefreshWarning": "Modellauswahl gespeichert. Einige Client-Kataloge konnten nicht aktualisiert werden. Prüfe vor dem Start einer neuen Sitzung die Integrationen.", "models.saveFailed": "Speichern fehlgeschlagen", "models.networkError": "Netzwerkfehler — läuft der Proxy?", "models.loadFail": "Modelle konnten nicht geladen werden — läuft der Proxy?", @@ -1064,6 +1065,21 @@ export const de: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Aside-Profile", + "integrations.aside.profilesHint": "Wähle, welche Profile die ausgewählten Modelle erhalten. Das aktive Aside-Profil bleibt unverändert.", + "integrations.aside.all": "Alle Profile synchronisieren", + "integrations.aside.syncNow": "Jetzt synchronisieren", + "integrations.aside.applied": "Bei {count} von {total} Profilen angewendet", + "integrations.aside.current": "Aktuelles Profil", + "integrations.aside.profile": "Profil {id}", + "integrations.aside.toggle": "{name} synchronisieren", + "integrations.aside.details": "{name} verwalten", + "integrations.aside.back": "Alle Aside-Profile", + "integrations.aside.empty": "Öffne Aside und erstelle ein Profil, um es zu verbinden.", + "integrations.aside.partial": "Einige Profile benötigen Aufmerksamkeit. Deine Synchronisierungsauswahl ist gespeichert. Prüfe den Status der einzelnen Profile.", + "integrations.aside.pending": "Synchronisierungsauswahl gespeichert; Dateiaktualisierung steht aus.", + "integrations.aside.retry": "Für {name} erneut versuchen", + "integrations.aside.loadError": "Aside-Profile konnten nicht geladen werden. Versuche es erneut, um ihren Status zu prüfen.", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Die Codex-Anbindung wird vom Proxy-Dienst verwaltet. Beim Start von opencodex wird sie angewendet; beim Stoppen des Dienstes wird das native Routing wiederhergestellt.", "integrations.codex.openService": "Dienststeuerung öffnen", @@ -1182,6 +1198,7 @@ export const de: Record = { "integrations.bulk.success": "Angewendete Client-Integrationen wurden deaktiviert.", "integrations.retention.degraded": "Die Sicherungsbereinigung ist im Rückstand; ältere Sicherungen könnten noch auf dem Datenträger liegen.", "integrations.error.residual": "Die Datei könnte sich in einem Zwischenzustand befinden: {message} Stellen Sie sie aus {path} wieder her.", + "integrations.error.residualNoSnapshot": "{message} Die automatische Wiederherstellung wurde nicht abgeschlossen. Prüfe die Client-Konfiguration, bevor du es erneut versuchst.", "integrations.error.recover": "{message} Eine Sicherung liegt unter {path}.", "integrations.kind.apply": "Angewendet", "integrations.kind.disable": "Deaktiviert", @@ -1206,7 +1223,7 @@ export const de: Record = { "integrations.semantics.mcode": "Verwaltet nur custom_provider.opencodex. Standardmodell und MiniMax-Anmeldung bleiben unverändert.", "integrations.semantics.zcode": "Verwaltet nur provider.opencodex in ~/.zcode/v2/config.json. Z.ai-Anmeldung und andere Provider bleiben unverändert. ZCode nach Änderungen neu starten.", "integrations.semantics.prime": "Verwaltet nur providers.opencodex in der models.json von Prime Agent — ~/.prime/agent, sofern PRIME_AGENT_CODING_AGENT_DIR sie nicht umleitet. Andere Provider und Modell-Overrides bleiben unverändert. Gilt für neue Sitzungen.", - "integrations.semantics.aside": "Verwaltet nur providers.opencodex in der models.json von Aside für das angemeldete Konto (~/.aside/u/). Andere Provider bleiben unverändert. Aside überschreibt diese Datei im laufenden Betrieb, daher nach dem Anwenden vollständig beenden und neu öffnen.", + "integrations.semantics.aside": "Verwaltet nur providers.opencodex in der ~/.aside/u//models.json dieses Profils. Andere Provider bleiben unverändert. Beende Aside nach dem Anwenden vollständig und öffne es erneut.", "codexAuth.mainAccount": "Hauptkonto", "codexAuth.logLabel": "Log-Kennung", "codexAuth.codexApp": "Codex App", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index a3bff471c0..dcfa7c1067 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -626,6 +626,7 @@ export const en = { "models.tipActive": "Active", "models.tipDisabled": "Disabled", "models.applied": "Applied — takes effect on the next Codex turn.", + "models.integrationRefreshWarning": "Model selection saved. Some client catalogs could not be refreshed. Check Integrations before starting a new session.", "models.saveFailed": "Save failed", "models.networkError": "Network error — is the proxy running?", "models.loadFail": "Failed to load models — is the proxy running?", @@ -1571,6 +1572,21 @@ export const en = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Aside profiles", + "integrations.aside.profilesHint": "Choose which profiles receive the selected models. Aside’s active profile stays unchanged.", + "integrations.aside.all": "Sync all profiles", + "integrations.aside.syncNow": "Sync now", + "integrations.aside.applied": "{count} of {total} profiles applied", + "integrations.aside.current": "Current profile", + "integrations.aside.profile": "Profile {id}", + "integrations.aside.toggle": "Sync {name}", + "integrations.aside.details": "Manage {name}", + "integrations.aside.back": "All Aside profiles", + "integrations.aside.empty": "Open Aside and create a profile to connect it.", + "integrations.aside.partial": "Some profiles need attention. Your sync choices are saved; check each profile’s state.", + "integrations.aside.pending": "Sync choice saved; file update pending.", + "integrations.aside.retry": "Retry {name}", + "integrations.aside.loadError": "Could not load Aside profiles. Retry to check their state.", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Codex wiring is owned by the proxy service. Starting opencodex applies it; stopping the service restores native routing.", "integrations.codex.openService": "Open service controls", @@ -1729,6 +1745,7 @@ export const en = { "integrations.bulk.success": "Applied client integrations were disabled.", "integrations.retention.degraded": "Backup cleanup is behind; older backups may still be on disk.", "integrations.error.residual": "The file may be in an intermediate state: {message} Restore it from {path}.", + "integrations.error.residualNoSnapshot": "{message} Automatic recovery did not finish. Check the client configuration before retrying.", "integrations.error.recover": "{message} A backup is at {path}.", "integrations.kind.apply": "Applied", "integrations.kind.disable": "Disabled", @@ -1753,7 +1770,7 @@ export const en = { "integrations.semantics.mcode": "Manages only custom_provider.opencodex. Your default model and MiniMax login stay unchanged.", "integrations.semantics.zcode": "Manages only provider.opencodex in ~/.zcode/v2/config.json. Your Z.ai login and other providers stay unchanged. Restart ZCode after changes.", "integrations.semantics.prime": "Manages only providers.opencodex in Prime Agent's models.json — ~/.prime/agent unless PRIME_AGENT_CODING_AGENT_DIR redirects it. Your other providers and model overrides stay unchanged. Applies to new sessions.", - "integrations.semantics.aside": "Manages only providers.opencodex in Aside's models.json for the signed-in account (~/.aside/u/). Your other providers stay unchanged. Aside rewrites this file while running, so fully quit and reopen it after applying.", + "integrations.semantics.aside": "Manages only providers.opencodex in this profile’s ~/.aside/u//models.json. Your other providers stay unchanged. Fully quit and reopen Aside after applying.", "codexAuth.mainAccount": "Main Account", "codexAuth.logLabel": "Log label", "codexAuth.codexApp": "Codex App", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 84b6e3108f..d6bfd21935 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -611,6 +611,7 @@ export const fr: Record = { "models.tipActive": "Actif", "models.tipDisabled": "Désactivé", "models.applied": "Appliqué — prend effet au prochain tour Codex.", + "models.integrationRefreshWarning": "Sélection des modèles enregistrée. Certains catalogues clients n’ont pas pu être actualisés. Vérifiez les intégrations avant de démarrer une nouvelle session.", "models.saveFailed": "Échec de l’enregistrement", "models.networkError": "Erreur réseau — le proxy est-il en cours d’exécution ?", "models.loadFail": "Échec du chargement des modèles — le proxy est-il en cours d’exécution ?", @@ -1543,6 +1544,21 @@ export const fr: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Profils Aside", + "integrations.aside.profilesHint": "Choisissez les profils qui recevront les modèles sélectionnés. Le profil actif dans Aside reste inchangé.", + "integrations.aside.all": "Synchroniser tous les profils", + "integrations.aside.syncNow": "Synchroniser maintenant", + "integrations.aside.applied": "Appliqué à {count} profils sur {total}", + "integrations.aside.current": "Profil actuel", + "integrations.aside.profile": "Profil {id}", + "integrations.aside.toggle": "Synchroniser {name}", + "integrations.aside.details": "Gérer {name}", + "integrations.aside.back": "Tous les profils Aside", + "integrations.aside.empty": "Ouvrez Aside et créez un profil pour le connecter.", + "integrations.aside.partial": "Certains profils nécessitent votre attention. Vos choix de synchronisation sont enregistrés ; vérifiez l’état de chaque profil.", + "integrations.aside.pending": "Choix de synchronisation enregistré ; mise à jour du fichier en attente.", + "integrations.aside.retry": "Réessayer pour {name}", + "integrations.aside.loadError": "Impossible de charger les profils Aside. Réessayez pour vérifier leur état.", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Le câblage de Codex est géré par le service proxy. Le démarrage d’opencodex l’applique ; l’arrêt du service rétablit le routage natif.", "integrations.codex.openService": "Ouvrir les commandes du service", @@ -1661,6 +1677,7 @@ export const fr: Record = { "integrations.bulk.success": "Les intégrations client appliquées ont été désactivées.", "integrations.retention.degraded": "Le nettoyage des sauvegardes est en retard ; d’anciennes sauvegardes peuvent encore se trouver sur le disque.", "integrations.error.residual": "Le fichier peut être dans un état intermédiaire : {message} Restaurez-le depuis {path}.", + "integrations.error.residualNoSnapshot": "{message} La récupération automatique n’a pas abouti. Vérifiez la configuration du client avant de réessayer.", "integrations.error.recover": "{message} Une sauvegarde se trouve dans {path}.", "integrations.kind.apply": "Appliqué", "integrations.kind.disable": "Désactivé", @@ -1685,7 +1702,7 @@ export const fr: Record = { "integrations.semantics.mcode": "Gère uniquement custom_provider.opencodex. Votre modèle par défaut et votre connexion MiniMax restent inchangés.", "integrations.semantics.zcode": "Gère uniquement provider.opencodex dans ~/.zcode/v2/config.json. Votre connexion Z.ai et les autres fournisseurs restent inchangés. Redémarrez ZCode après toute modification.", "integrations.semantics.prime": "Gère uniquement providers.opencodex dans le models.json de Prime Agent — ~/.prime/agent, sauf si PRIME_AGENT_CODING_AGENT_DIR le redirige. Vos autres fournisseurs et surcharges de modèles restent inchangés. S'applique aux nouvelles sessions.", - "integrations.semantics.aside": "Gère uniquement providers.opencodex dans le models.json d'Aside pour le compte connecté (~/.aside/u/). Vos autres fournisseurs restent inchangés. Aside réécrit ce fichier pendant son exécution : quittez-le complètement et relancez-le après application.", + "integrations.semantics.aside": "Gère uniquement providers.opencodex dans le fichier ~/.aside/u//models.json de ce profil. Vos autres fournisseurs restent inchangés. Quittez complètement Aside et relancez-le après application.", "codexAuth.mainAccount": "Compte principal", "codexAuth.logLabel": "Libellé du journal", "codexAuth.codexApp": "Application Codex", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 6467a9ed4b..692ad3d878 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -573,6 +573,7 @@ export const ja: Record = { "models.customApply": "適用", "models.customPlaceholder": "トークン (例: 420000)", "models.applied": "適用しました — 次回の Codex ターンで有効になります。", + "models.integrationRefreshWarning": "モデルの選択を保存しました。一部のクライアントのモデル一覧を更新できませんでした。新しいセッションを始める前に「連携」を確認してください。", "models.saveFailed": "保存に失敗しました", "models.networkError": "ネットワークエラー — プロキシは起動していますか?", "models.loadFail": "モデルの読み込みに失敗しました — プロキシは起動していますか?", @@ -1498,6 +1499,21 @@ export const ja: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Asideのプロファイル", + "integrations.aside.profilesHint": "選択したモデルを同期するプロファイルを選んでください。Asideで使用中のプロファイルは変わりません。", + "integrations.aside.all": "すべてのプロファイルを同期", + "integrations.aside.syncNow": "今すぐ同期", + "integrations.aside.applied": "{total}件中{count}件のプロファイルに適用済み", + "integrations.aside.current": "使用中のプロファイル", + "integrations.aside.profile": "プロファイル {id}", + "integrations.aside.toggle": "{name}を同期", + "integrations.aside.details": "{name}を管理", + "integrations.aside.back": "Asideの全プロファイル", + "integrations.aside.empty": "Asideを開き、接続するプロファイルを作成してください。", + "integrations.aside.partial": "一部のプロファイルに確認が必要です。同期設定は保存されています。各プロファイルの状態を確認してください。", + "integrations.aside.pending": "同期設定を保存しました。ファイルの更新待ちです。", + "integrations.aside.retry": "{name}を再試行", + "integrations.aside.loadError": "Asideのプロファイルを読み込めませんでした。再試行して状態を確認してください。", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Codex の接続はプロキシサービスが管理します。opencodex を起動すると適用され、サービスを停止するとネイティブのルーティングに戻ります。", "integrations.codex.openService": "サービス制御を開く", @@ -1616,6 +1632,7 @@ export const ja: Record = { "integrations.bulk.success": "適用済みのクライアント連携を無効にしました。", "integrations.retention.degraded": "バックアップの整理が遅れています。古いバックアップがディスクに残っている可能性があります。", "integrations.error.residual": "ファイルが中間状態のままの可能性があります: {message} {path} から復元してください。", + "integrations.error.residualNoSnapshot": "{message} 自動復旧が完了しませんでした。再試行する前にクライアントの設定を確認してください。", "integrations.error.recover": "{message} バックアップは {path} にあります。", "integrations.kind.apply": "適用", "integrations.kind.disable": "解除", @@ -1640,7 +1657,7 @@ export const ja: Record = { "integrations.semantics.mcode": "custom_provider.opencodex のみを管理します。既定モデルと MiniMax ログインは変更しません。", "integrations.semantics.zcode": "~/.zcode/v2/config.json の provider.opencodex のみを管理します。Z.ai ログインと他のプロバイダーは変更しません。変更後は ZCode を再起動してください。", "integrations.semantics.prime": "Prime Agent の models.json 内の providers.opencodex のみを管理します。場所は ~/.prime/agent ですが、PRIME_AGENT_CODING_AGENT_DIR が設定されている場合はそちらが優先されます。他のプロバイダーとモデルオーバーライドは変更しません。新しいセッションから適用されます。", - "integrations.semantics.aside": "サインイン中のアカウントの Aside models.json 内の providers.opencodex のみを管理します。場所は ~/.aside/u/<アカウント> です。他のプロバイダーは変更しません。Aside は実行中にこのファイルを書き換えるため、適用後は Aside を完全に終了して再度開いてください。", + "integrations.semantics.aside": "このプロファイルの ~/.aside/u//models.json 内の providers.opencodex のみを管理します。他のプロバイダーは変更しません。適用後は Aside を完全に終了してから開き直してください。", "codexAuth.mainAccount": "メインアカウント", "codexAuth.logLabel": "ログラベル", "codexAuth.codexApp": "Codex App", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 7d5a918439..e0a2fb1838 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -612,6 +612,7 @@ export const ko: Record = { "models.tipActive": "활성", "models.tipDisabled": "비활성", "models.applied": "적용됨 — 다음 Codex 턴부터 반영됩니다.", + "models.integrationRefreshWarning": "모델 선택을 저장했습니다. 일부 클라이언트의 모델 목록을 갱신하지 못했습니다. 새 세션을 시작하기 전에 연동 메뉴를 확인하세요.", "models.saveFailed": "저장 실패", "models.networkError": "네트워크 오류 — 프록시가 실행 중인가요?", "models.loadFail": "모델을 불러오지 못했습니다 — 프록시가 실행 중인가요?", @@ -1088,6 +1089,21 @@ export const ko: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Aside 프로필", + "integrations.aside.profilesHint": "선택한 모델을 동기화할 프로필을 고르세요. Aside에서 사용 중인 프로필은 바뀌지 않습니다.", + "integrations.aside.all": "모든 프로필 동기화", + "integrations.aside.syncNow": "지금 동기화", + "integrations.aside.applied": "{total}개 중 {count}개 적용됨", + "integrations.aside.current": "사용 중", + "integrations.aside.profile": "프로필 {id}", + "integrations.aside.toggle": "{name} 동기화", + "integrations.aside.details": "{name} 관리", + "integrations.aside.back": "전체 Aside 프로필", + "integrations.aside.empty": "Aside를 열고 연결할 프로필을 만드세요.", + "integrations.aside.partial": "일부 프로필을 확인해 주세요. 동기화 설정은 저장됐습니다. 각 프로필의 상태를 확인하세요.", + "integrations.aside.pending": "동기화 설정 저장됨 · 파일 반영 대기 중", + "integrations.aside.retry": "{name} 다시 시도", + "integrations.aside.loadError": "Aside 프로필을 불러오지 못했습니다. 다시 시도해 상태를 확인하세요.", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Codex 연결은 프록시 서비스가 관리합니다. opencodex를 시작하면 적용되고 서비스를 중지하면 기본 라우팅으로 복원됩니다.", "integrations.codex.openService": "서비스 제어 열기", @@ -1206,6 +1222,7 @@ export const ko: Record = { "integrations.bulk.success": "적용된 클라이언트 연동을 해제했습니다.", "integrations.retention.degraded": "백업 정리가 밀려 있습니다 — 오래된 백업이 남아 있을 수 있습니다.", "integrations.error.residual": "파일이 중간 상태로 남았을 수 있습니다: {message} {path}에서 복원하세요.", + "integrations.error.residualNoSnapshot": "{message} 자동 복구를 마치지 못했습니다. 다시 시도하기 전에 클라이언트 설정을 확인하세요.", "integrations.error.recover": "{message} 백업 위치는 {path}입니다.", "integrations.kind.apply": "적용", "integrations.kind.disable": "해제", @@ -1230,7 +1247,7 @@ export const ko: Record = { "integrations.semantics.mcode": "custom_provider.opencodex만 관리하며 기본 모델과 MiniMax 로그인은 변경하지 않습니다.", "integrations.semantics.zcode": "~/.zcode/v2/config.json의 provider.opencodex만 관리하며 Z.ai 로그인과 다른 프로바이더는 변경하지 않습니다. 변경 후 ZCode를 재시작하세요.", "integrations.semantics.prime": "Prime Agent의 models.json에서 providers.opencodex만 관리합니다. 위치는 ~/.prime/agent이며 PRIME_AGENT_CODING_AGENT_DIR가 설정되면 그쪽이 우선합니다. 다른 프로바이더와 모델 오버라이드는 변경하지 않습니다. 새 세션부터 적용됩니다.", - "integrations.semantics.aside": "로그인된 계정의 Aside models.json에서 providers.opencodex만 관리합니다. 위치는 ~/.aside/u/<계정>이며 다른 프로바이더는 변경하지 않습니다. Aside는 실행 중에 이 파일을 다시 쓰기 때문에 적용한 뒤 Aside를 완전히 종료하고 다시 여세요.", + "integrations.semantics.aside": "이 프로필의 ~/.aside/u//models.json에서 providers.opencodex만 관리합니다. 다른 프로바이더는 그대로 유지됩니다. 적용 후 Aside를 완전히 종료하고 다시 여세요.", "codexAuth.mainAccount": "메인 계정", "codexAuth.logLabel": "로그 라벨", "codexAuth.codexApp": "Codex App", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 82c3ea4e8a..1459b145a2 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -614,6 +614,7 @@ export const ru: Record = { "models.tipActive": "Активна", "models.tipDisabled": "Отключена", "models.applied": "Применено — вступит в силу на следующем ходе Codex.", + "models.integrationRefreshWarning": "Выбор моделей сохранён. Не удалось обновить каталоги некоторых клиентов. Проверьте раздел «Интеграции» перед началом нового сеанса.", "models.saveFailed": "Не удалось сохранить", "models.networkError": "Ошибка сети — запущен ли прокси?", "models.loadFail": "Не удалось загрузить модели — запущен ли прокси?", @@ -1554,6 +1555,21 @@ export const ru: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Профили Aside", + "integrations.aside.profilesHint": "Выберите профили, в которые будут добавлены выбранные модели. Активный профиль Aside не изменится.", + "integrations.aside.all": "Синхронизировать все профили", + "integrations.aside.syncNow": "Синхронизировать сейчас", + "integrations.aside.applied": "Применено к профилям: {count} из {total}", + "integrations.aside.current": "Текущий профиль", + "integrations.aside.profile": "Профиль {id}", + "integrations.aside.toggle": "Синхронизировать {name}", + "integrations.aside.details": "Управление: {name}", + "integrations.aside.back": "Все профили Aside", + "integrations.aside.empty": "Откройте Aside и создайте профиль для подключения.", + "integrations.aside.partial": "Некоторые профили требуют внимания. Настройки синхронизации сохранены; проверьте состояние каждого профиля.", + "integrations.aside.pending": "Настройка синхронизации сохранена; обновление файла ожидается.", + "integrations.aside.retry": "Повторить для {name}", + "integrations.aside.loadError": "Не удалось загрузить профили Aside. Повторите попытку, чтобы проверить их состояние.", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Подключением Codex управляет прокси-сервис. При запуске opencodex оно применяется, а при остановке сервиса восстанавливается нативная маршрутизация.", "integrations.codex.openService": "Открыть управление сервисом", @@ -1672,6 +1688,7 @@ export const ru: Record = { "integrations.bulk.success": "Применённые интеграции клиентов отключены.", "integrations.retention.degraded": "Очистка резервных копий отстаёт; старые копии могут всё ещё находиться на диске.", "integrations.error.residual": "Файл может остаться в промежуточном состоянии: {message} Восстановите его из {path}.", + "integrations.error.residualNoSnapshot": "{message} Автоматическое восстановление не завершено. Проверьте настройки клиента перед повторной попыткой.", "integrations.error.recover": "{message} Резервная копия находится в {path}.", "integrations.kind.apply": "Применено", "integrations.kind.disable": "Отключено", @@ -1696,7 +1713,7 @@ export const ru: Record = { "integrations.semantics.mcode": "Управляет только custom_provider.opencodex. Модель по умолчанию и вход MiniMax не меняются.", "integrations.semantics.zcode": "Управляет только provider.opencodex в ~/.zcode/v2/config.json. Вход Z.ai и другие провайдеры не меняются. Перезапустите ZCode после изменений.", "integrations.semantics.prime": "Управляет только providers.opencodex в models.json Prime Agent — ~/.prime/agent, если PRIME_AGENT_CODING_AGENT_DIR не переопределяет путь. Другие провайдеры и переопределения моделей не меняются. Применяется к новым сессиям.", - "integrations.semantics.aside": "Управляет только providers.opencodex в models.json Aside для выполнившего вход аккаунта (~/.aside/u/<аккаунт>). Другие провайдеры не меняются. Aside перезаписывает этот файл во время работы, поэтому после применения полностью закройте и снова откройте его.", + "integrations.semantics.aside": "Управляет только providers.opencodex в файле ~/.aside/u//models.json этого профиля. Другие провайдеры остаются без изменений. После применения полностью закройте и снова откройте Aside.", "codexAuth.mainAccount": "Основной аккаунт", "codexAuth.logLabel": "Метка журнала", "codexAuth.codexApp": "Codex App", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 2db94dc0b8..a2404e4163 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -617,6 +617,7 @@ export const tr: Record = { "models.tipActive": "Aktif", "models.tipDisabled": "Devre Dışı", "models.applied": "Uygulandı.", + "models.integrationRefreshWarning": "Model seçimi kaydedildi. Bazı istemcilerin model katalogları yenilenemedi. Yeni bir oturum başlatmadan önce Entegrasyonlar bölümünü kontrol edin.", "models.saveFailed": "Kaydetme başarısız", "models.networkError": "Ağ hatası — proxy çalışıyor mu?", "models.loadFail": "Modeller yüklenemedi — proxy çalışıyor mu?", @@ -1561,6 +1562,21 @@ export const tr: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Aside profilleri", + "integrations.aside.profilesHint": "Seçili modellerin hangi profillere aktarılacağını seçin. Aside’ın etkin profili değişmez.", + "integrations.aside.all": "Tüm profilleri eşitle", + "integrations.aside.syncNow": "Şimdi eşitle", + "integrations.aside.applied": "{total} profilden {count} tanesine uygulandı", + "integrations.aside.current": "Geçerli profil", + "integrations.aside.profile": "Profil {id}", + "integrations.aside.toggle": "{name} profilini eşitle", + "integrations.aside.details": "{name} profilini yönet", + "integrations.aside.back": "Tüm Aside profilleri", + "integrations.aside.empty": "Bağlamak için Aside’ı açıp bir profil oluşturun.", + "integrations.aside.partial": "Bazı profillerle ilgilenmeniz gerekiyor. Eşitleme tercihleriniz kaydedildi; her profilin durumunu kontrol edin.", + "integrations.aside.pending": "Eşitleme tercihi kaydedildi; dosya güncellemesi bekleniyor.", + "integrations.aside.retry": "{name} için yeniden dene", + "integrations.aside.loadError": "Aside profilleri yüklenemedi. Durumlarını kontrol etmek için yeniden deneyin.", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Codex bağlantısı proxy servisine aittir.", "integrations.codex.openService": "Servis kontrollerini aç", @@ -1679,6 +1695,7 @@ export const tr: Record = { "integrations.bulk.success": "Uygulanan istemci entegrasyonları devre dışı bırakıldı.", "integrations.retention.degraded": "Yedek temizliği geride kaldı.", "integrations.error.residual": "{path} konumunda {message}", + "integrations.error.residualNoSnapshot": "{message} Otomatik kurtarma tamamlanamadı. Yeniden denemeden önce istemci yapılandırmasını kontrol edin.", "integrations.error.recover": "{path} kurtarılırken {message}", "integrations.kind.apply": "Uygulandı", "integrations.kind.disable": "Devre Dışı Bırakıldı", @@ -1702,7 +1719,7 @@ export const tr: Record = { "integrations.semantics.mcode": "Yalnızca custom_provider.opencodex bölümünü yönetir. Varsayılan model ve MiniMax oturumu değişmez.", "integrations.semantics.zcode": "Yalnızca ~/.zcode/v2/config.json içindeki provider.opencodex bölümünü yönetir. Z.ai oturumu ve diğer sağlayıcılar değişmez. Değişikliklerden sonra ZCode'u yeniden başlatın.", "integrations.semantics.prime": "Yalnızca Prime Agent'ın models.json dosyasındaki providers.opencodex bölümünü yönetir — PRIME_AGENT_CODING_AGENT_DIR ayarlı değilse ~/.prime/agent. Diğer sağlayıcılar ve model geçersiz kılmaları değişmez. Yeni oturumlarda geçerli olur.", - "integrations.semantics.aside": "Yalnızca oturum açmış hesabın Aside models.json dosyasındaki providers.opencodex bölümünü yönetir (~/.aside/u/). Diğer sağlayıcılar değişmez. Aside çalışırken bu dosyayı yeniden yazar; bu nedenle uyguladıktan sonra Aside'ı tamamen kapatıp yeniden açın.", + "integrations.semantics.aside": "Yalnızca bu profilin ~/.aside/u//models.json dosyasındaki providers.opencodex bölümünü yönetir. Diğer sağlayıcılarınız değişmez. Uyguladıktan sonra Aside’ı tamamen kapatıp yeniden açın.", "integrations.semantics.omp": "Kataloğu yüklemek için OMP'yi yeniden başlatın.", "codexAuth.mainAccount": "Ana Hesap", "codexAuth.logLabel": "Günlük etiketi", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index f543e948a5..c6e4746cbe 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -480,6 +480,7 @@ export const zhTW: Record = { "models.tipActive": "已啟用", "models.tipDisabled": "已停用", "models.applied": "已套用 — 將在下一個 Codex 回合生效。", + "models.integrationRefreshWarning": "已儲存模型選擇。部分用戶端的模型目錄無法更新。開始新工作階段前,請檢查「整合」頁面。", "models.saveFailed": "儲存失敗", "models.networkError": "網路錯誤 — 代理在執行嗎?", "models.loadFail": "載入模型失敗 — 代理在執行嗎?", @@ -2149,6 +2150,21 @@ export const zhTW: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Aside 設定檔", + "integrations.aside.profilesHint": "選擇要接收所選模型的設定檔。Aside 目前使用的設定檔不會改變。", + "integrations.aside.all": "同步所有設定檔", + "integrations.aside.syncNow": "立即同步", + "integrations.aside.applied": "已套用 {count}/{total} 個設定檔", + "integrations.aside.current": "目前的設定檔", + "integrations.aside.profile": "設定檔 {id}", + "integrations.aside.toggle": "同步 {name}", + "integrations.aside.details": "管理 {name}", + "integrations.aside.back": "所有 Aside 設定檔", + "integrations.aside.empty": "開啟 Aside 並建立設定檔以進行連線。", + "integrations.aside.partial": "部分設定檔需要處理。同步設定已儲存,請檢查各設定檔的狀態。", + "integrations.aside.pending": "同步設定已儲存,等待更新檔案。", + "integrations.aside.retry": "重試 {name}", + "integrations.aside.loadError": "無法載入 Aside 設定檔。請重試以查看狀態。", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Codex 連線由代理服務管理。啟動 opencodex 時套用;停止服務時還原原生路由。", "integrations.codex.openService": "開啟服務控制", @@ -2267,6 +2283,7 @@ export const zhTW: Record = { "integrations.bulk.success": "已套用的用戶端整合已停用。", "integrations.retention.degraded": "備份清理進度落後;磁碟上可能仍有較舊的備份。", "integrations.error.residual": "檔案可能處於中間狀態:{message} 請從 {path} 還原。", + "integrations.error.residualNoSnapshot": "{message} 自動復原未完成。請先檢查用戶端設定,再重試。", "integrations.error.recover": "{message} 備份位於 {path}。", "integrations.kind.apply": "已套用", "integrations.kind.disable": "已停用", @@ -2291,7 +2308,7 @@ export const zhTW: Record = { "integrations.semantics.mcode": "僅管理 custom_provider.opencodex,不會變更預設模型或 MiniMax 登入狀態。", "integrations.semantics.zcode": "僅管理 ~/.zcode/v2/config.json 中的 provider.opencodex,不會變更 Z.ai 登入狀態或其他供應商。變更後請重新啟動 ZCode。", "integrations.semantics.prime": "僅管理 Prime Agent 的 models.json 中的 providers.opencodex;預設位於 ~/.prime/agent,若設定 PRIME_AGENT_CODING_AGENT_DIR 則以其為準。不會變更其他供應商或模型覆寫設定。對新工作階段生效。", - "integrations.semantics.aside": "僅管理已登入帳號的 Aside models.json 中的 providers.opencodex,位於 ~/.aside/u/<帳號>。不會變更其他供應商。Aside 在執行時會重寫該檔案,因此套用後請完全結束並重新開啟 Aside。", + "integrations.semantics.aside": "僅管理此設定檔的 ~/.aside/u//models.json 中的 providers.opencodex。其他供應商維持不變。套用後請完全結束並重新開啟 Aside。", "codexAuth.pinned": "已固定", "codexAuth.pinnedHint": "你手動選取了此帳號,因此較高的選擇順序不會越過它。此固定會持續到該帳號用盡、你改選其他帳號,或你變更任一選擇順序為止。", "codexAuth.requestUserInput": "在 Default 模式中要求輸入", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index c0dbd81a5c..4c49583b8c 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -609,6 +609,7 @@ export const zh: Record = { "models.tipActive": "已启用", "models.tipDisabled": "已禁用", "models.applied": "已应用 — 将在下一个 Codex 回合生效。", + "models.integrationRefreshWarning": "模型选择已保存。部分客户端的模型目录未能刷新。开始新会话前,请检查“集成”页面。", "models.saveFailed": "保存失败", "models.networkError": "网络错误 — 代理在运行吗?", "models.loadFail": "加载模型失败 — 代理在运行吗?", @@ -1081,6 +1082,21 @@ export const zh: Record = { "integrations.tab.zcode": "ZCode", "integrations.tab.prime": "Prime Agent", "integrations.tab.aside": "Aside", + "integrations.aside.profilesTitle": "Aside 配置文件", + "integrations.aside.profilesHint": "选择要接收所选模型的配置文件。Aside 当前使用的配置文件不会改变。", + "integrations.aside.all": "同步所有配置文件", + "integrations.aside.syncNow": "立即同步", + "integrations.aside.applied": "已应用 {count}/{total} 个配置文件", + "integrations.aside.current": "当前配置文件", + "integrations.aside.profile": "配置文件 {id}", + "integrations.aside.toggle": "同步 {name}", + "integrations.aside.details": "管理 {name}", + "integrations.aside.back": "所有 Aside 配置文件", + "integrations.aside.empty": "打开 Aside 并创建配置文件以进行连接。", + "integrations.aside.partial": "部分配置文件需要处理。同步设置已保存,请检查各配置文件的状态。", + "integrations.aside.pending": "同步设置已保存,等待更新文件。", + "integrations.aside.retry": "重试 {name}", + "integrations.aside.loadError": "无法加载 Aside 配置文件。请重试以查看状态。", "integrations.codex.title": "Codex CLI", "integrations.codex.body": "Codex 连接由代理服务管理。启动 opencodex 时应用该连接;停止服务时恢复原生路由。", "integrations.codex.openService": "打开服务控制", @@ -1199,6 +1215,7 @@ export const zh: Record = { "integrations.bulk.success": "已禁用已应用的客户端集成。", "integrations.retention.degraded": "备份清理进度滞后;磁盘上可能仍有较旧的备份。", "integrations.error.residual": "文件可能处于中间状态:{message} 请从 {path} 恢复。", + "integrations.error.residualNoSnapshot": "{message} 自动恢复未完成。请先检查客户端配置,再重试。", "integrations.error.recover": "{message} 备份位于 {path}。", "integrations.kind.apply": "已应用", "integrations.kind.disable": "已停用", @@ -1223,7 +1240,7 @@ export const zh: Record = { "integrations.semantics.mcode": "仅管理 custom_provider.opencodex,不会更改默认模型或 MiniMax 登录状态。", "integrations.semantics.zcode": "仅管理 ~/.zcode/v2/config.json 中的 provider.opencodex,不会更改 Z.ai 登录状态或其他提供商。更改后请重启 ZCode。", "integrations.semantics.prime": "仅管理 Prime Agent 的 models.json 中的 providers.opencodex;默认位于 ~/.prime/agent,若设置 PRIME_AGENT_CODING_AGENT_DIR 则以其为准。不会更改其他提供商或模型覆盖设置。对新会话生效。", - "integrations.semantics.aside": "仅管理已登录账号的 Aside models.json 中的 providers.opencodex,位于 ~/.aside/u/<账号>。不会更改其他提供商。Aside 在运行时会重写该文件,因此应用后请完全退出并重新打开 Aside。", + "integrations.semantics.aside": "仅管理此配置文件的 ~/.aside/u//models.json 中的 providers.opencodex。其他提供商保持不变。应用后请完全退出并重新打开 Aside。", "codexAuth.mainAccount": "主账号", "codexAuth.logLabel": "日志标签", "codexAuth.codexApp": "Codex App", diff --git a/gui/src/model-visibility.ts b/gui/src/model-visibility.ts index 5007d979d2..df73ff5186 100644 --- a/gui/src/model-visibility.ts +++ b/gui/src/model-visibility.ts @@ -4,6 +4,26 @@ export interface ModelVisibilityTarget { id: string; native?: boolean; } export type ModelVisibilityScope = "models" | "provider"; +export interface ClientCatalogRefreshFailure { + client: string; profileId?: number; reason?: string; refusalReason?: string; + snapshotPath?: string; residual?: boolean; +} + +/** Selection persistence and client-file refresh have separate success outcomes. */ +export function clientCatalogRefreshFailures(body: unknown): ClientCatalogRefreshFailure[] | undefined { + // Missing outcomes (old servers or preset-empty fallback) do not prove recovery. + if (!isRecord(body) || !Array.isArray(body.clientIntegrations)) return undefined; + return body.clientIntegrations.filter((row): row is Record => isRecord(row) && row.ok === false) + .map(row => ({ + client: typeof row.client === "string" ? row.client : "", + ...(Number.isSafeInteger(row.profileId) && (row.profileId as number) >= 0 ? { profileId: row.profileId as number } : {}), + ...(typeof row.reason === "string" ? { reason: row.reason } : {}), + ...(typeof row.refusalReason === "string" ? { refusalReason: row.refusalReason } : {}), + ...(typeof row.snapshotPath === "string" ? { snapshotPath: row.snapshotPath } : {}), + ...(row.residual === true ? { residual: true } : {}), + })); +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } diff --git a/gui/src/pages/Integrations.tsx b/gui/src/pages/Integrations.tsx index b173771106..b7d7d86fc2 100644 --- a/gui/src/pages/Integrations.tsx +++ b/gui/src/pages/Integrations.tsx @@ -8,6 +8,7 @@ import Claude from "./Claude"; import Grok from "./Grok"; import CursorIntegrationPage from "./integrations/CursorIntegrationPage"; import IntegrationsOverview from "./integrations/IntegrationsOverview"; +import AsideProfilesPage from "./integrations/AsideProfilesPage"; import FileIntegrationPage, { type FileIntegrationClientId, } from "./integrations/FileIntegrationPage"; @@ -197,7 +198,7 @@ export default function Integrations({ apiBase, machineApiBase = apiBase, connec {definition.id === "grok" && } {definition.id === "cursor" && } {FILE_CLIENTS.has(definition.id as FileIntegrationClientId) && ( - : >(() => initialCollapsed ?? new Set()); const needsDefaultCollapseRef = useRef(initialCollapsed === null); const [status, setStatus] = useState(""); + const [integrationFailures, setIntegrationFailures] = useState([]); const [ok, setOk] = useState(false); // Feedback generation: a repeated identical message (same success string, same validation // error) must still re-arm the toast timer. Clearing `status` alone is not enough — a @@ -244,6 +248,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; }, [status, ok, feedbackGen]); const [busy, setBusy] = useState(false); const busyRef = useRef(false); + const catalogMutationRef = useRef(false); const loadGenerationRef = useRef(0); const loadPendingRef = useRef(false); // multi_agent_v2 / ultra gate. null = endpoint unavailable (older proxy build) -> section hidden. @@ -700,6 +705,8 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; targets: ModelVisibilityTarget[], enabled: boolean, ) => { + if (catalogMutationRef.current) return; + catalogMutationRef.current = true; ++loadGenerationRef.current; setBusy(true); busyRef.current = true; @@ -708,6 +715,10 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; try { const response = await putModelVisibility(apiBase, scope, provider, targets, enabled); if (!response.ok) errorKey = "models.saveFailed"; + else { + const failures = clientCatalogRefreshFailures(await response.json()); + if (failures !== undefined) setIntegrationFailures(failures); + } } catch { errorKey = "models.networkError"; } finally { @@ -721,6 +732,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; } setBusy(false); busyRef.current = false; + catalogMutationRef.current = false; } }; @@ -968,8 +980,11 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; }; const applyPreset = async (provider: string, mode: "preset" | "all") => { - if (presetBusy) return; + if (catalogMutationRef.current) return; + catalogMutationRef.current = true; setPresetBusy(provider); + setBusy(true); + busyRef.current = true; try { const bounded = createBoundedFetch(30_000); const r = await fetch(`${apiBase}/api/model-presets`, { @@ -978,12 +993,15 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; body: JSON.stringify({ provider, mode }), signal: bounded.signal, }); - const res = await readJsonIfOk<{ fallback?: string; selected?: string[] }>(r) ?? {}; + const res = await readJsonOrThrow<{ fallback?: string; selected?: string[]; clientIntegrations?: unknown }>(r, t("models.saveFailed")); + if (!res) throw new Error(t("models.saveFailed")); if (res.fallback === "preset-empty") { // Never silently narrow to nothing: the server kept the previous selection, so say so // rather than showing a success that changed nothing. publishFeedback(false, t("models.presetEmpty", { provider })); } else { + const failures = clientCatalogRefreshFailures(res); + if (failures !== undefined) setIntegrationFailures(failures); publishFeedback(true, mode === "all" ? t("models.presetClearedToast", { provider }) : t("models.presetAppliedToast", { provider, count: String(res.selected?.length ?? 0) })); @@ -993,6 +1011,9 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; publishFeedback(false, error instanceof Error ? error.message : String(error)); } finally { setPresetBusy(null); + setBusy(false); + busyRef.current = false; + catalogMutationRef.current = false; } }; @@ -1273,7 +1294,7 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; // control — a provider with nothing to curate would show a dead switch. const preset = presets[provider]; if (!preset) return null; - const busyHere = presetBusy === provider; + const busyHere = busy || presetBusy !== null; const stale = preset.mode === "custom" && preset.appliedVersion !== undefined && preset.appliedVersion < preset.availableVersion; @@ -2122,6 +2143,17 @@ export default function Models({ apiBase, restartEpoch = 0 }: { apiBase: string; )} {/* Keep the last-good catalog interactive but make a failed revalidation explicit. */} {catalogState.showError && {t("models.loadFail")}} + {integrationFailures.length > 0 &&
+ + {t("models.integrationRefreshWarning")} +
    {integrationFailures.map(row =>
  • + {row.client}{row.profileId === undefined ? "" : `:${row.profileId}`}: {describeIntegrationRefusalParts(t, { + clientId: row.client, message: row.reason === "integration_mutation_busy" ? t("integrations.error.busy") : row.reason, + reason: row.refusalReason, snapshotPath: row.snapshotPath, residual: row.residual, + })} +
  • )}
+
+
}