fix(projects): keep the new-tab launcher project-only (#1039)

## Summary
Remove the three bottom actions (Servers, SideCar, Full home) from the
project-tab plus launcher. Also remove its server disclosure, profile
hook/loading, local expansion state, server actions/dialog binding,
unused props/icons/CSS and four keys in all locales. This is not just
hidden chrome.

Recent search/list, Browse, Temporary and Clone remain. A normal plus
click still opens home. All long-press/context/keyboard gestures and
Escape focus remain. Home keeps remote server and SideCar functionality;
shared server setup is mounted only for its caller.

## Validation
UI typecheck passed. Real new-instance-launcher browser suite 8/8:
retained actions/gestures/focus, no server requests from launcher even
with saved profiles, home remote/SideCar controls retained.
Desktop/narrow captures inspected. git diff --check passed. No
app/daemon/state touched.

## Scope
48 files (+110/-190), largely 10-locale help and removed translations.
App.tsx remains oversized (~915 lines); only obsolete launcher props
changed.
This commit is contained in:
Pascal André 2026-10-20 18:44:36 +02:00 • committed by GitHub
parent b85a32b210
commit 7a2ee897ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 110 additions and 190 deletions

View file

@ -187,7 +187,7 @@
- Native OpenCode plugins share `packages/server/scripts/native-plugin-build.mjs`: never embed Effect or the public plugin SDK into daemon-loaded bundles. The host Effect/Schema boundary dates from 2.0.27 (the reproduced RC-to-stable break); the minimum is now 2.1.0 for the native replay contract (`native`, `metadata.keepID`) consumed by the pinned client reducer, and pins and the recommendation follow the qualified target (2.1.0); newer/custom runtimes still undergo contract recognition. Keep backend-side OpenTunnel isolation separate, and run the bundle regression plus isolated plugin-runtime/pruning/automation/Missions fixtures when changing this boundary.
- Temporary instances (`workspaces/temporary-workspaces.ts`, UI `stores/temporary-instances.ts`) are ordinary non-Git workspaces on CodeNomad-created direct children of `<config>/temporary-workspaces`. Registry membership (`temporary-workspaces.json`), not location, makes a folder temporary; keeping only unregisters it and adds a named recent project. Discard deletes only registered children of that root, refuses running conversations and other open tabs of the folder, removes conversations deepest-first, closes the workspace before deleting the folder and keeps failed removals registered; with no resume list, such a folder is reachable only by browsing to it. Closing a temporary tab from the UI asks Keep/Discard/Cancel, except that an untouched one (no conversation, file or unsent draft) closes and is discarded silently (`onlyIfEmpty`: the server refuses with `temporary_has_conversations`/`temporary_not_empty`, and any failure of that quiet discard returns the choice without an error). Before the server stops it, a visible temporary tab yields to its neighbour (or closes if alone) and a refusal restores it in place; `workspace.stopped` likewise closes the tab first and clears the instance in a microtask, because clearing a visible shell's state in the same update as its tab froze the window. Same-minute names get a `(n)` suffix. Registry changes broadcast `workspace.temporaryChanged`, which clears kept marks in every window. Open temporary tabs are saved and restored with the app state like any project tab (still marked temporary); the home page has no resume list. Only the choice dialog overrides initial focus (to its last, non-destructive choice); confirms keep focus on Cancel. Refusals use stable `temporary_*` codes localized by the client. Validate with `scripts/test-temporary-workspaces-native.mjs` against an isolated CLI.
- The tab-bar new-instance button follows the last tab like a browser: the instance `TabScroll` frame only spans its tabs and the button sits after it, outside the scroller (so focusing it never scrolls the strip), ending up at the strip's end once tabs overflow. Dragged tabs stay visually within the tab list like a browser's (`KeepDraggedTabInStrip`), while the drop slot follows the pointer's unbounded position (`closestUnboundedCenter`). The button keeps its click (full home page, Cmd/Ctrl+N) while a long press, right-click, Alt+Down, Shift+F10 or Menu opens the anchored `NewInstanceLauncher` (`lib/long-press.ts`; only Escape returns focus to the button, never over another surface or the home page): search-first recent projects (an open project switches to its tab), Browse/Temporary/Clone, then remote servers, SideCar and the full home page. Management (rename/remove recents, servers, settings) stays on the home page. Browse/clone/add-server flows live once in `components/new-instance-dialogs.tsx` and recent-folder display helpers in `lib/recent-folder-display.ts`, shared with `FolderSelectionView`; styles in `styles/components/new-instance-launcher.css`.
- The tab-bar new-instance button follows the last tab like a browser: the instance `TabScroll` frame only spans its tabs and the button sits after it, outside the scroller (so focusing it never scrolls the strip), ending up at the strip's end once tabs overflow. Dragged tabs stay visually within the tab list like a browser's (`KeepDraggedTabInStrip`), while the drop slot follows the pointer's unbounded position (`closestUnboundedCenter`). The button keeps its click (full home page, Cmd/Ctrl+N) while a long press, right-click, Alt+Down, Shift+F10 or Menu opens the anchored `NewInstanceLauncher` (`lib/long-press.ts`; only Escape returns focus to the button, never over another surface or the home page): search-first recent projects (an open project switches to its tab) and Browse/Temporary/Clone only. Remote servers, SideCar and management (rename/remove recents, servers, settings) stay on the home page. Browse/clone flows live once in `components/new-instance-dialogs.tsx` (with optional home-only add-server setup) and recent-folder display helpers in `lib/recent-folder-display.ts`, shared with `FolderSelectionView`; styles in `styles/components/new-instance-launcher.css`.
- Favor KISS by keeping modules narrowly scoped and limiting public APIs to what callers actually need.
- Native transcript re-projections run per event and must not invalidate unchanged messages: `hydrateMessages` (message-v2/instance-store.ts) reuses the resident `MessageRecord` object whenever content, explicit timestamps and prompt-display metadata are unchanged, and an identical whole snapshot returns before any store write or session-revision bump. Part/info comparisons use allocation-free structural equality (`partDataEqual`), never `JSON.stringify`. The SDK reduce mutates rows in place, so reference identity can never prove a session unchanged — never add a reference-anchored projection skip. Action-menu item arrays are memoized (`createMemo`, or stable per-row arrays) rather than rebuilt with fresh JSX icons per evaluation behind `<For>` or into `ActionOverflowMenu` (message-item, tool-call, prompt-input, WorkspaceTree, instance-shell2). A memoized item whose icon renders in two places at once (inline buttons and the overflow menu, as in `FileRowActions` or tool-call headers) must create the icon in a getter, or come from an unmemoized builder like message-block's `headerMenuItems`: one DOM node cannot be inserted twice.
- Built-in right-panel tabs are constructed directly in `right-panel/core-runtime.tsx`; external sandboxed panels retain their independent consent/loading path. Do not restore an empty first-party plugin loader. Native transcript events reduce through `stores/opencode-data.ts` and publish bounded snapshots with `hydrateMessages`; do not reintroduce an unused second partial-part reducer. Shared speech byte/MediaSource transport lives in `lib/audio-utils.ts`, while playback ownership and cancellation stay with its callers.

View file

@ -45,7 +45,7 @@ Native Mobilvorlagen setzen Android-/Chromium-Identität, Ausrichtung, Touch und
1. Starten Sie das Webwerkzeug separat auf dem CodeNomad-Backend-Host, lauschend auf `127.0.0.1:<port>`.
2. Öffnen Sie **Einstellungen → SideCars**. Geben Sie **Name** und **Port** ein; prüfen Sie den abgeleiteten **Basispfad** (`/sidecars/<id>`).
3. Setzen Sie **Protokoll** passend zum Upstream-Dienst und wählen Sie **Präfix-Modus**.
4. Wählen Sie **SideCar hinzufügen**, dann **SideCar öffnen** auf der Startseite oder im Instanz-Schnellstarter, um dessen Tab zu öffnen.
4. Wählen Sie **SideCar hinzufügen**, dann **SideCar öffnen** auf der Startseite, um dessen Tab zu öffnen.
| Auswahl | Exakte Wirkung |
| --- | --- |

View file

@ -18,7 +18,6 @@ Zuletzt verwendete Einträge sind Verknüpfungen. **Arbeitsbereich umbenennen**
1. Halten Sie `+` gedrückt oder klicken Sie mit der rechten Maustaste darauf; bei Tastaturfokus dort verwenden Sie **Alt+Down**, **Shift+F10** oder die Menütaste.
2. Suchen Sie zuletzt verwendete Projekte oder wählen Sie **Durchsuchen…**, **Temporär** oder **Klonen…**.
3. Verwenden Sie **Remote-Server**, **SideCar** oder **Vollständige Startseite** für die anderen Ziele.
Ein normaler Klick öffnet weiterhin die vollständige Startseite. Der Schnellstarter ist ein Direktzugriff, keine Verwaltungsoberfläche: Benennen/entfernen Sie zuletzt verwendete Projekte und verwalten Sie Server-Lesezeichen auf der Startseite. Escape schließt den Schnellstarter und setzt den Fokus auf `+` zurück.

View file

@ -85,6 +85,6 @@ Meldet ein Client eine Zertifikatswarnung, halten Sie an und prüfen Sie Hostnam
2. Geben Sie **Servername** und **Server-URL** mit einer vertrauenswürdigen HTTPS-Origin ein. Lassen Sie **TLS-Verifizierung für selbstsignierte Zertifikate überspringen.** deaktiviert; richten Sie stattdessen Vertrauen ein.
3. Wählen Sie **Speichern** für ein Lesezeichen oder **Verbinden** zum Speichern und Öffnen. Melden Sie sich auf diesem Server an.
Lesezeichen gehören zu dieser CodeNomad-Konfiguration, nicht den Projekten des Remote-Servers. Eine URL zu speichern startet keinen Server und gewährt keinen Zugriff. **Gespeicherten Server entfernen** entfernt das Lesezeichen, nicht entfernte Unterhaltungen, Backend oder Zugangsdaten. Gespeicherte Server lassen sich auch aus **Remote-Server** im Instanz-Schnellstarter öffnen.
Lesezeichen gehören zu dieser CodeNomad-Konfiguration, nicht den Projekten des Remote-Servers. Eine URL zu speichern startet keinen Server und gewährt keinen Zugriff. **Gespeicherten Server entfernen** entfernt das Lesezeichen, nicht entfernte Unterhaltungen, Backend oder Zugangsdaten.
Fügen Sie für Fernsteuerung stattdessen einen frischen Kopplungslink in **Server-URL** ein. Das Lesezeichen behält nur die Tunnel-Origin, nie den Einmal-Kopplungscode; siehe [Fernsteuerung](remote-control.md) zur erneuten Kopplung nach Verlust des Geräte-Cookies.

View file

@ -45,7 +45,7 @@ Native mobile templates set Android/Chromium identity, orientation, touch and DP
1. Start the web tool separately on the CodeNomad backend host, listening on `127.0.0.1:<port>`.
2. Open **Settings → SideCars**. Enter **Name** and **Port**; check the derived **Base path** (`/sidecars/<id>`).
3. Set **Protocol** to match the upstream service, and choose **Prefix mode**.
4. Choose **Add SideCar**, then **Open SideCar** from the home page or new-instance launcher to open its tab.
4. Choose **Add SideCar**, then **Open SideCar** from the home page to open its tab.
| Choice | Exact effect |
| --- | --- |

View file

@ -18,7 +18,6 @@ Recent entries are shortcuts. **Rename workspace** changes its displayed name; *
1. Long-press or right-click `+`; with keyboard focus there, use **Alt+Down**, **Shift+F10** or the Menu key.
2. Search recent projects, or select **Browse…**, **Temporary** or **Clone…**.
3. Use **Remote servers**, **SideCar** or **Full home page** for the other destinations.
A normal click still opens the full home page. The launcher is a shortcut, not the management surface: rename/remove recent projects and manage server bookmarks on the home page. Escape dismisses the launcher and returns focus to `+`.

View file

@ -85,6 +85,6 @@ If a client reports a certificate warning, stop and check the hostname, SANs, tr
2. Enter **Server name** and **Server URL** using a trusted HTTPS origin. Leave **Skip TLS verification for self-signed certificates.** unchecked; establish trust instead.
3. Choose **Save** to bookmark, or **Connect** to save and open it. Sign in on that server.
Bookmarks belong to this CodeNomad configuration, not the remote server's projects. Saving a URL does not start the server or grant access. **Remove saved server** removes the bookmark, not remote conversations, the backend or its credentials. Saved servers can also be opened from the new-instance launcher's **Remote servers** list.
Bookmarks belong to this CodeNomad configuration, not the remote server's projects. Saving a URL does not start the server or grant access. **Remove saved server** removes the bookmark, not remote conversations, the backend or its credentials.
For Remote Control, paste a fresh pairing link into **Server URL** instead. The bookmark retains only the tunnel origin, never the one-use pairing code; see [Remote Control](remote-control.md) for re-pairing after device-cookie loss.

View file

@ -45,7 +45,7 @@ Las plantillas móviles nativas establecen identidad Android/Chromium, orientaci
1. Inicia la herramienta web por separado en el equipo del backend de CodeNomad, escuchando en `127.0.0.1:<port>`.
2. Abre **Configuración → SideCars**. Introduce **Nombre** y **Puerto**; comprueba la **Ruta base** derivada (`/sidecars/<id>`).
3. Establece **Protocolo** para que coincida con el servicio de destino y elige **Modo de prefijo**.
4. Elige **Añadir SideCar** y después **Abrir SideCar** desde la página de inicio o el lanzador de nuevas instancias para abrir su pestaña.
4. Elige **Añadir SideCar** y después **Abrir SideCar** desde la página de inicio para abrir su pestaña.
| Opción | Efecto exacto |
| --- | --- |

View file

@ -18,7 +18,6 @@ Las entradas recientes son accesos directos. **Renombrar workspace** cambia su n
1. Mantén pulsado `+` o haz clic derecho; con el foco del teclado allí, usa **Alt+Down**, **Shift+F10** o la tecla Menú.
2. Busca proyectos recientes o selecciona **Examinar…**, **Temporal** o **Clonar…**.
3. Usa **Servidores remotos**, **SideCar** o **Página de inicio completa** para los demás destinos.
Un clic normal sigue abriendo la página de inicio completa. El lanzador es un acceso directo, no la superficie de gestión: renombra/elimina proyectos recientes y gestiona los marcadores de servidores en la página de inicio. Escape cierra el lanzador y devuelve el foco a `+`.

View file

@ -85,6 +85,6 @@ Si un cliente muestra una advertencia de certificado, detente y comprueba el nom
2. Introduce **Nombre del servidor** y **URL del servidor** usando un origen HTTPS de confianza. Deja sin marcar **Omitir la verificación TLS para certificados autofirmados.**; establece confianza en su lugar.
3. Elige **Guardar** para crear un marcador o **Conectar** para guardarlo y abrirlo. Inicia sesión en ese servidor.
Los marcadores pertenecen a esta configuración de CodeNomad, no a los proyectos del servidor remoto. Guardar una URL no inicia el servidor ni concede acceso. **Eliminar servidor guardado** elimina el marcador, no las conversaciones remotas, el backend ni sus credenciales. Los servidores guardados también se pueden abrir desde la lista **Servidores remotos** del lanzador de nuevas instancias.
Los marcadores pertenecen a esta configuración de CodeNomad, no a los proyectos del servidor remoto. Guardar una URL no inicia el servidor ni concede acceso. **Eliminar servidor guardado** elimina el marcador, no las conversaciones remotas, el backend ni sus credenciales.
Para Control remoto, pega en su lugar un enlace de emparejamiento nuevo en **URL del servidor**. El marcador conserva solo el origen del túnel, nunca el código de un solo uso; consulta [Control remoto](remote-control.md) para volver a emparejar tras perder la cookie del dispositivo.

View file

@ -45,7 +45,7 @@ Les modèles mobiles natifs définissent identité Android/Chromium, orientation
1. Démarrez l’outil web séparément sur l’hôte backend CodeNomad, en écoute sur `127.0.0.1:<port>`.
2. Ouvrez **Paramètres → SideCars**. Saisissez **Nom** et **Port** ; vérifiez le **Chemin de base** dérivé (`/sidecars/<id>`).
3. Réglez **Protocole** selon le service amont et choisissez **Mode de préfixe**.
4. Choisissez **Ajouter un SideCar**, puis **Ouvrir SideCar** depuis l’accueil ou le lanceur de nouvelle instance pour ouvrir son onglet.
4. Choisissez **Ajouter un SideCar**, puis **Ouvrir SideCar** depuis l’accueil pour ouvrir son onglet.
| Choix | Effet exact |
| --- | --- |

View file

@ -18,7 +18,6 @@ Les entrées récentes sont des raccourcis. **Renommer l'espace de travail** cha
1. Appuyez longuement ou faites un clic droit sur `+` ; lorsque ce bouton a le focus clavier, utilisez **Alt+Down**, **Shift+F10** ou la touche Menu.
2. Recherchez les projets récents ou sélectionnez **Parcourir…**, **Temporaire** ou **Cloner…**.
3. Utilisez **Serveurs distants**, **SideCar** ou **Accueil complet** pour les autres destinations.
Un clic normal ouvre toujours l’accueil complet. Le lanceur est un raccourci, pas l’interface de gestion : renommez/retirez les projets récents et gérez les serveurs enregistrés depuis l’accueil. Échap ferme le lanceur et rend le focus à `+`.

View file

@ -85,6 +85,6 @@ Si un client signale un avertissement de certificat, arrêtez-vous et vérifiez
2. Saisissez **Nom du serveur** et **URL du serveur** avec une origine HTTPS de confiance. Laissez **Ignorer la vérification TLS pour les certificats auto-signés.** décoché ; établissez plutôt la confiance.
3. Choisissez **Enregistrer** pour ajouter un favori, ou **Se connecter** pour l’enregistrer et l’ouvrir. Connectez-vous sur ce serveur.
Les favoris appartiennent à cette configuration CodeNomad, pas aux projets du serveur distant. Enregistrer une URL ne démarre pas le serveur et n’accorde pas l’accès. **Supprimer le serveur enregistré** retire le favori, pas les conversations distantes, le backend ou ses identifiants. Les serveurs enregistrés peuvent aussi s’ouvrir depuis la liste **Serveurs distants** du lanceur de nouvelle instance.
Les favoris appartiennent à cette configuration CodeNomad, pas aux projets du serveur distant. Enregistrer une URL ne démarre pas le serveur et n’accorde pas l’accès. **Supprimer le serveur enregistré** retire le favori, pas les conversations distantes, le backend ou ses identifiants.
Pour le Contrôle à distance, collez plutôt un lien d’appairage récent dans **URL du serveur**. Le favori ne conserve que l’origine du tunnel, jamais le code d’appairage à usage unique ; voir [Contrôle à distance](remote-control.md) pour le réappairage après perte du cookie de l’appareil.

View file

@ -45,7 +45,7 @@
1. הפעילו את כלי הרשת בנפרד במארח שרת הקצה של CodeNomad, בהאזנה ב־`127.0.0.1:<port>`.
2. פתחו **הגדרות → SideCars**. הזינו **שם** ו־**פורט**; בדקו את **נתיב בסיס** הנגזר (`/sidecars/<id>`).
3. הגדירו **פרוטוקול** בהתאם לשירות שמאחוריו, ובחרו **מצב קידומת**.
4. בחרו **הוסף SideCar**, ואז **פתח SideCar** מדף הבית או ממשגר המופע החדש לפתיחת הלשונית שלו.
4. בחרו **הוסף SideCar**, ואז **פתח SideCar** מדף הבית לפתיחת הלשונית שלו.
| בחירה | השפעה מדויקת |
| --- | --- |

View file

@ -18,7 +18,6 @@
1. לחצו לחיצה ארוכה או לחיצה ימנית על `+`; כשהמקלדת ממוקדת בו, השתמשו ב־**Alt+Down**, ב־**Shift+F10** או במקש Menu.
2. חפשו פרויקטים אחרונים, או בחרו **עיון…**, **זמני** או **שכפול…**.
3. השתמשו ב־**שרתים מרוחקים**, ב־**SideCar** או ב־**דף הבית המלא** ליעדים האחרים.
לחיצה רגילה עדיין פותחת את דף הבית המלא. המשגר הוא קיצור דרך, לא ממשק הניהול: שנו שמות/הסירו פרויקטים אחרונים ונהלו סימניות שרתים בדף הבית. Escape סוגר את המשגר ומחזיר מיקוד ל־`+`.

View file

@ -85,6 +85,6 @@ codenomad --host 127.0.0.1 --https=false --http=true
2. הזינו **שם השרת** ו־**כתובת השרת** עם מקור HTTPS מהימן. השאירו את **דלג על אימות TLS עבור תעודות בחתימה עצמית.** לא מסומן; הגדירו אמון במקום זאת.
3. בחרו **שמור** לסימנייה, או **התחבר** לשמירה ולפתיחה. התחברו באותו שרת.
סימניות שייכות לתצורת CodeNomad זו, לא לפרויקטי השרת המרוחק. שמירת כתובת אינה מפעילה שרת או מעניקה גישה. **הסר שרת שמור** מסיר את הסימנייה, לא שיחות מרוחקות, את שרת הקצה או את פרטי ההתחברות שלו. אפשר לפתוח שרתים שמורים גם מרשימת **שרתים מרוחקים** במשגר המופע החדש.
סימניות שייכות לתצורת CodeNomad זו, לא לפרויקטי השרת המרוחק. שמירת כתובת אינה מפעילה שרת או מעניקה גישה. **הסר שרת שמור** מסיר את הסימנייה, לא שיחות מרוחקות, את שרת הקצה או את פרטי ההתחברות שלו.
לשליטה מרחוק, הדביקו במקום זאת קישור צימוד חדש ב־**כתובת השרת**. הסימנייה שומרת רק את מקור המנהרה, לעולם לא את קוד הצימוד החד־פעמי; ראו [שליטה מרחוק](remote-control.md) לצימוד מחדש לאחר אובדן עוגיית המכשיר.

View file

@ -45,7 +45,7 @@
1. CodeNomad バックエンドホストで、ウェブツールを別途起動し、`127.0.0.1:<port>` で待ち受けます。
2. **設定 → SideCar** を開き、**名前** と **ポート** を入力し、算出された **ベースパス**(`/sidecars/<id>`)を確認します。
3. 上流サービスに合わせて **プロトコル** を設定し、**プレフィックスモード** を選びます。
4. **SideCar を追加** を選び、ホームページまたは新しいインスタンスのランチャーの **SideCar を開く** でタブを開きます。
4. **SideCar を追加** を選び、ホームページの **SideCar を開く** でタブを開きます。
| 選択 | 正確な効果 |
| --- | --- |

View file

@ -18,7 +18,6 @@
1. `+` を長押しまたは右クリックします。キーボードフォーカスがある場合は、**Alt+Down**、**Shift+F10**、または Menu キーを使用します。
2. 最近のプロジェクトを検索するか、**参照…**、**一時**、**クローン…** を選びます。
3. ほかの移動先には、**リモートサーバー**、**SideCar**、**ホームページ全体** を使用します。
通常のクリックでは、引き続きホームページ全体が開きます。ランチャーはショートカットであり、管理画面ではありません。最近のプロジェクトの名前変更/削除やサーバーブックマークの管理は、ホームページで行います。Escape でランチャーを閉じ、フォーカスを `+` に戻せます。

View file

@ -85,6 +85,6 @@ HTTPS が有効で鍵/証明書のペアを指定していない場合、CodeN
2. 信頼された HTTPS オリジンを使用して **サーバー名** と **サーバー URL** を入力します。**自己署名証明書の TLS 検証をスキップします。** はチェックせず、代わりに信頼を確立します。
3. ブックマークには **保存**、保存して開くには **接続** を選び、そのサーバーでログインします。
ブックマークは、この CodeNomad 設定に属し、リモートサーバーのプロジェクトに属するわけではありません。URL の保存はサーバーの起動やアクセス許可を行いません。**保存したサーバーを削除** はブックマークを削除しますが、リモートの会話、バックエンド、認証情報は削除しません。保存済みサーバーは、新しいインスタンスのランチャーの **リモートサーバー** 一覧からも開けます。
ブックマークは、この CodeNomad 設定に属し、リモートサーバーのプロジェクトに属するわけではありません。URL の保存はサーバーの起動やアクセス許可を行いません。**保存したサーバーを削除** はブックマークを削除しますが、リモートの会話、バックエンド、認証情報は削除しません。
リモートコントロールでは、代わりに新しいペアリングリンクを **サーバー URL** に貼り付けます。ブックマークはトンネルのオリジンだけを保持し、使い捨てのペアリングコードは保持しません。端末 Cookie 消失後の再ペアリングは、[リモートコントロール](remote-control.md)を参照してください。

View file

@ -45,7 +45,7 @@ Developer Mode toggle वा थप activation restart छैन। नेटि
1. CodeNomad backend host मा `127.0.0.1:<port>` मा सुन्ने गरी web tool छुट्टै सुरु गर्नुहोस्।
2. **सेटिङहरू → SideCars** खोल्नुहोस्। **नाम** र **पोर्ट** भर्नुहोस्; निकालिएको **आधार मार्ग (Base path)** (`/sidecars/<id>`) जाँच्नुहोस्।
3. upstream सेवासँग मिल्ने **प्रोटोकल** सेट गरी **प्रिफिक्स मोड** रोज्नुहोस्।
4. **SideCar थप्नुहोस्** रोजेर गृह पृष्ठ वा new-instance launcher को **SideCar खोल्नुहोस्** बाट ट्याब खोल्नुहोस्।
4. **SideCar थप्नुहोस्** रोजेर गृह पृष्ठको **SideCar खोल्नुहोस्** बाट ट्याब खोल्नुहोस्।
| चयन | ठ्याक्कै असर |
| --- | --- |

View file

@ -18,7 +18,6 @@
1. `+` लामो थिच्नुहोस् वा right-click गर्नुहोस्; त्यहाँ keyboard focus हुँदा **Alt+Down**, **Shift+F10** वा Menu key प्रयोग गर्नुहोस्।
2. भर्खरका परियोजना खोज्नुहोस्, वा **ब्राउज…**, **अस्थायी** वा **क्लोन…** चयन गर्नुहोस्।
3. अन्य गन्तव्यका लागि **रिमोट सर्भरहरू**, **SideCar** वा **पूर्ण गृह पृष्ठ** प्रयोग गर्नुहोस्।
सामान्य क्लिकले अझै पूर्ण गृह पृष्ठ खोल्छ। launcher shortcut हो, व्यवस्थापन सतह होइन: recent परियोजनाको नाम बदल्न/हटाउन र सर्भर bookmark व्यवस्थापन गर्न गृह पृष्ठ प्रयोग गर्नुहोस्। Escape ले launcher बन्द गरी `+` मा focus फर्काउँछ।

View file

@ -85,6 +85,6 @@ certificate चेतावनी आए रोकिएर hostname, SAN, trust
2. trusted HTTPS origin सहित **सर्भरको नाम** र **सर्भर URL** भर्नुहोस्। **स्व-हस्ताक्षरित प्रमाणपत्रहरूको लागि TLS प्रमाणीकरण छोड्नुहोस्।** नछान्नुहोस्; बरु trust स्थापित गर्नुहोस्।
3. bookmark गर्न **बचत गर्नुहोस्**, वा बचत र खोल्न **जडान गर्नुहोस्** रोज्नुहोस्। त्यो server मा sign in गर्नुहोस्।
bookmark यो CodeNomad कन्फिगरेसनका हुन्, remote server का project होइनन्। URL बचतले server सुरु वा access प्रदान गर्दैन। **बचत गरिएको सर्भर हटाउनुहोस्** ले bookmark हटाउँछ, remote कुराकानी, backend वा credential होइन। new-instance launcher को **रिमोट सर्भरहरू** सूचीबाट पनि खोल्न सकिन्छ।
bookmark यो CodeNomad कन्फिगरेसनका हुन्, remote server का project होइनन्। URL बचतले server सुरु वा access प्रदान गर्दैन। **बचत गरिएको सर्भर हटाउनुहोस्** ले bookmark हटाउँछ, remote कुराकानी, backend वा credential होइन।
Remote Control मा **सर्भर URL** मा नयाँ pairing link paste गर्नुहोस्। bookmark ले tunnel origin मात्र राख्छ, one-use code कहिल्यै होइन; device-cookie हराएपछि पुनःpair गर्न [Remote Control](remote-control.md) हेर्नुहोस्।

View file

@ -45,7 +45,7 @@
1. Отдельно запустите веб-инструмент на хосте серверной части CodeNomad с прослушиванием `127.0.0.1:<port>`.
2. Откройте **Настройки → SideCars**. Введите **Имя** и **Порт**; проверьте вычисленный **Базовый путь** (`/sidecars/<id>`).
3. Задайте **Протокол** по исходной службе и выберите **Режим префикса**.
4. Нажмите **Добавить SideCar**, затем **Открыть SideCar** на главной странице или в меню нового экземпляра для открытия вкладки.
4. Нажмите **Добавить SideCar**, затем **Открыть SideCar** на главной странице для открытия вкладки.
| Выбор | Точный эффект |
| --- | --- |

View file

@ -18,7 +18,6 @@
1. Удерживайте `+` или щёлкните правой кнопкой; когда на ней фокус, используйте **Alt+Down**, **Shift+F10** или клавишу Menu.
2. Найдите недавний проект либо выберите **Обзор…**, **Временный** или **Клонировать…**.
3. Для других назначений используйте **Удалённые серверы**, **SideCar** или **Полная главная страница**.
Обычное нажатие по-прежнему открывает полную главную страницу. Меню — быстрый переход, а не средство управления: переименовывайте/удаляйте недавние проекты и управляйте закладками серверов на главной странице. Escape закрывает меню и возвращает фокус на `+`.

View file

@ -85,6 +85,6 @@ codenomad --host 127.0.0.1 --https=false --http=true
2. Введите **Имя сервера** и **URL сервера**, используя доверенный HTTPS-origin. Оставьте **Пропустить проверку TLS для самоподписанных сертификатов.** выключенным; вместо этого настройте доверие.
3. Нажмите **Сохранить** для закладки или **Подключиться** для сохранения и открытия. Войдите на этом сервере.
Закладки принадлежат этой конфигурации CodeNomad, не проектам удалённого сервера. Сохранение URL не запускает сервер и не предоставляет доступ. **Удалить сохраненный сервер** удаляет закладку, не удалённые разговоры, серверную часть или её учётные данные. Сохранённые серверы также открываются из **Удалённые серверы** меню нового экземпляра.
Закладки принадлежат этой конфигурации CodeNomad, не проектам удалённого сервера. Сохранение URL не запускает сервер и не предоставляет доступ. **Удалить сохраненный сервер** удаляет закладку, не удалённые разговоры, серверную часть или её учётные данные.
Для удалённого управления вместо этого вставьте свежую ссылку сопряжения в **URL сервера**. Закладка хранит только origin туннеля, никогда одноразовый код; повторное сопряжение после потери cookie устройства описано в [удалённом управлении](remote-control.md).

View file

@ -45,7 +45,7 @@ Yerel mobil şablonlar Android/Chromium kimliğini, yönelimini, dokunuşunu ve
1. Web aracını CodeNomad arka ucunun ana bilgisayarında ayrı olarak başlatıp `127.0.0.1:<port>` adresinde dinlemesini sağlayın.
2. **Ayarlar → SideCars** öğesini açın. **Ad** ve **Port** öğesini girin; türetilen **Temel yol** öğesini (`/sidecars/<id>`) kontrol edin.
3. **Protokol** öğesini yukarı akış hizmetiyle eşleşecek şekilde ayarlayın ve **Ön ek modu** öğesini seçin.
4. Sekmesini açmak için ana sayfadan veya yeni örnek başlatıcıdan **SideCar ekle** öğesini ve ardından **SideCar'ı Aç** öğesini seçin.
4. Sekmesini açmak için ana sayfadan **SideCar ekle** öğesini ve ardından **SideCar'ı Aç** öğesini seçin.
| Seçim | Kesin etki |
| --- | --- |

View file

@ -18,7 +18,6 @@ Son kullanılan kayıtlar birer kısayoldur. **Workspace'i yeniden adlandır** g
1. `+`'ye uzun basın veya sağ tıklayın; Klavye odağı oradayken **Alt+Down**, **Shift+F10** veya Menü tuşunu kullanın.
2. Son projeleri arayın veya **Gözat…**, **Geçici** veya **Klonla…** öğesini seçin.
3. Diğer hedefler için **Uzak sunucular**, **SideCar** veya **Tam ana sayfa** öğesini kullanın.
Normal bir tıklama yine de tam ana sayfayı açar. Başlatıcı, yönetim yüzeyi değil, bir kısayoldur: ana sayfadaki son projeleri yeniden adlandırın/kaldırın ve sunucu yer imlerini yönetin. Escape, başlatıcıyı kapatır ve odağı `+`'ye döndürür.

View file

@ -85,6 +85,6 @@ Bir istemci bir sertifika uyarısı bildirirse, durdurun ve ana bilgisayar adın
2. Güvenilir bir HTTPS kaynağını kullanarak **Sunucu adı** ve **Sunucu URL'si** öğesini girin. **Kendinden imzalı sertifikalar için TLS doğrulamasını atla.** öğesini işaretlemeden bırakın; bunun yerine güven oluşturun.
3. Yer imlerine eklemek için **Kaydet** öğesini veya kaydedip açmak için **Bağlan** öğesini seçin. Bu sunucuda oturum açın.
Yer imleri uzak sunucunun projelerine değil, bu CodeNomad yapılandırmasına aittir. Bir URL'nin kaydedilmesi sunucuyu başlatmaz veya erişim sağlamaz. **Kayıtlı sunucuyu kaldır**, uzak konuşmaları, arka ucu veya kimlik bilgilerini değil, yer işaretini kaldırır. Kaydedilen sunucular, yeni örnek başlatıcının **Uzak sunucular** listesinden de açılabilir.
Yer imleri uzak sunucunun projelerine değil, bu CodeNomad yapılandırmasına aittir. Bir URL'nin kaydedilmesi sunucuyu başlatmaz veya erişim sağlamaz. **Kayıtlı sunucuyu kaldır**, uzak konuşmaları, arka ucu veya kimlik bilgilerini değil, yer işaretini kaldırır.
Uzaktan Kontrol için bunun yerine **Sunucu URL'si** öğesine yeni bir eşleştirme bağlantısı yapıştırın. Yer imi yalnızca tünelin kökenini korur, asla tek kullanımlık eşleştirme kodunu içermez; Cihaz çerezi kaybından sonra yeniden eşleştirme için bkz. [Uzaktan Kontrol](remote-control.md).

View file

@ -45,7 +45,7 @@
1. 在 CodeNomad 后端主机上单独启动网页工具,监听 `127.0.0.1:<port>`。
2. 打开**设置 → SideCars**。输入**名称**和**端口**;检查推导出的**基础路径**(`/sidecars/<id>`)。
3. 将**协议**设为匹配上游服务,并选择**前缀模式**。
4. 选择**添加 SideCar**,然后从主页或新建实例启动器选择**打开 SideCar**打开其标签页。
4. 选择**添加 SideCar**,然后从主页选择**打开 SideCar**打开其标签页。
| 选择 | 精确影响 |
| --- | --- |

View file

@ -18,7 +18,6 @@
1. 长按或右键点击 `+`;键盘焦点位于此处时,使用 **Alt+Down**、**Shift+F10** 或 Menu 键。
2. 搜索最近项目,或选择**浏览…**、**临时**或**克隆…**。
3. 使用**远程服务器**、**SideCar** 或**完整主页**前往其他位置。
普通点击仍会打开完整主页。启动器是快捷入口,而非管理界面:请在主页重命名/移除最近项目和管理服务器书签。Escape 会关闭启动器并将焦点返回 `+`。

View file

@ -85,6 +85,6 @@ codenomad --host 127.0.0.1 --https=false --http=true
2. 输入**服务器名称**和使用可信 HTTPS 来源的**服务器 URL**。不要勾选**为自签名证书跳过 TLS 验证。**;应建立信任。
3. 选择**保存**添加书签,或选择**连接**保存并打开。在该服务器上登录。
书签属于此 CodeNomad 配置,而非远程服务器项目。保存 URL 不会启动服务器或授予访问。**删除已保存服务器**移除书签,而非远程对话、后端或凭据。已保存服务器也可从新建实例启动器的**远程服务器**列表打开。
书签属于此 CodeNomad 配置,而非远程服务器项目。保存 URL 不会启动服务器或授予访问。**删除已保存服务器**移除书签,而非远程对话、后端或凭据。
使用远程控制时,改为将新配对链接粘贴到**服务器 URL**。书签仅保留隧道来源,绝不保留一次性配对码;设备 cookie 丢失后的重新配对请参阅[远程控制](remote-control.md)。

View file

@ -892,8 +892,6 @@ const App: Component = () => {
onSelectFolder={(path) => void handleSelectFolder(path)}
onSelectExistingInstance={handleSelectExistingInstance}
onNewTemporaryInstance={() => void handleNewTemporaryInstance()}
onOpenSidecar={handleOpenSidecarPicker}
onOpenHome={handleNewInstanceRequest}
/>
<SettingsScreen />
<SideCarPickerDialog open={sidecarPickerOpen()} onClose={() => setSidecarPickerOpen(false)} onOpenSidecar={handleOpenSidecar} />

View file

@ -12,12 +12,13 @@ interface NewInstanceDialogsOptions {
defaultPath: () => string | undefined
isLoading: () => boolean
onSelectFolder: (path: string) => void
onSaveServer: ComponentProps<typeof RemoteServerDialog>["onSubmit"]
/** Only the home page offers remote-server setup. */
onSaveServer?: ComponentProps<typeof RemoteServerDialog>["onSubmit"]
}
/**
* Browse, clone and add-server flows shared by the home page and the
* new-instance launcher. Mount `Dialogs` where it outlives the trigger.
* Browse and clone flows shared by the home page and the new-instance
* launcher, plus optional home-page server setup. Mount `Dialogs` where it outlives the trigger.
*/
export function createNewInstanceDialogs(options: NewInstanceDialogsOptions) {
const { t } = useI18n()
@ -237,7 +238,9 @@ export function createNewInstanceDialogs(options: NewInstanceDialogsOptions) {
</Dialog.Portal>
</Dialog>
<RemoteServerDialog open={isServerDialogOpen()} onOpenChange={setIsServerDialogOpen} onSubmit={options.onSaveServer} />
<Show when={options.onSaveServer}>
{(onSubmit) => <RemoteServerDialog open={isServerDialogOpen()} onOpenChange={setIsServerDialogOpen} onSubmit={onSubmit()} />}
</Show>
</>
)
}

View file

@ -1,11 +1,9 @@
import { Popover } from "@kobalte/core/popover"
import { For, Show, createEffect, createMemo, createSignal, on } from "solid-js"
import { ChevronRight, Folder, FolderPlus, GitBranch, Globe, Home, Hourglass, MonitorUp, Plus, Search } from "lucide-solid"
import { Folder, FolderPlus, GitBranch, Hourglass, Search } from "lucide-solid"
import { createNewInstanceDialogs } from "./new-instance-dialogs"
import { useI18n } from "../lib/i18n"
import { useRemoteServerProfiles } from "../lib/hooks/use-remote-server-profiles"
import { formatRelativeTime, getDisplayPath, getProjectDisplayName } from "../lib/recent-folder-display"
import { canOpenRemoteWindows } from "../lib/runtime-env"
import { getExistingInstanceForFolder } from "../stores/instances"
import { useConfig } from "../stores/preferences"
@ -18,30 +16,25 @@ interface NewInstanceLauncherProps {
onSelectFolder: (path: string) => void
onSelectExistingInstance: (instanceId: string, recentPath: string) => void
onNewTemporaryInstance: () => void
onOpenSidecar?: () => void
onOpenHome: () => void
}
/**
* Compact launcher opened by a long press, right-click or Alt+Down on the
* new-instance button: recent projects first (type to filter, Enter to open),
* then creation actions, then secondary destinations. Management (rename,
* remove, settings) stays on the full home page.
* then project creation actions. Management (rename, remove, settings) stays
* on the full home page.
*/
export default function NewInstanceLauncher(props: NewInstanceLauncherProps) {
const { t } = useI18n()
const { recentFolders } = useConfig()
const { remoteServers, saveServer, connectSavedServer } = useRemoteServerProfiles()
const [query, setQuery] = createSignal("")
const [activeIndex, setActiveIndex] = createSignal(0)
const [serversExpanded, setServersExpanded] = createSignal(false)
let listRef: HTMLDivElement | undefined
const dialogs = createNewInstanceDialogs({
defaultPath: () => recentFolders()[0]?.path,
isLoading: () => props.isLoading,
onSelectFolder: (path) => props.onSelectFolder(path),
onSaveServer: saveServer,
})
const matches = createMemo(() => {
@ -63,7 +56,6 @@ export default function NewInstanceLauncher(props: NewInstanceLauncherProps) {
if (!open) return
setQuery("")
setActiveIndex(0)
setServersExpanded(false)
returnFocus = false
}))
createEffect(on(matches, () => setActiveIndex(0), { defer: true }))
@ -209,45 +201,6 @@ export default function NewInstanceLauncher(props: NewInstanceLauncherProps) {
<span>{t("newInstanceLauncher.clone")}</span>
</button>
</div>
<Show when={serversExpanded()}>
<div class="new-instance-launcher-servers" role="group" aria-label={t("newInstanceLauncher.servers")}>
<For each={remoteServers()}>
{(server) => (
<button type="button" class="new-instance-launcher-server" title={server.baseUrl} disabled={props.isLoading}
onClick={() => run(() => void connectSavedServer(server.id))}>
<Globe class="w-4 h-4 icon-muted shrink-0" aria-hidden="true" />
<span class="new-instance-launcher-recent-name">{server.name}</span>
</button>
)}
</For>
<button type="button" class="new-instance-launcher-server" disabled={props.isLoading} onClick={() => run(dialogs.addServer)}>
<Plus class="w-4 h-4 icon-muted shrink-0" aria-hidden="true" />
<span class="new-instance-launcher-recent-name">{t("newInstanceLauncher.addServer")}</span>
</button>
</div>
</Show>
<footer class="window-footer new-instance-launcher-footer">
<Show when={canOpenRemoteWindows()}>
<button type="button" class="window-action" disabled={props.isLoading} aria-expanded={serversExpanded()}
onClick={() => setServersExpanded((value) => !value)}>
<Globe class="w-4 h-4" aria-hidden="true" />
<span>{t("newInstanceLauncher.servers")}</span>
<ChevronRight class="disclosure-chevron w-3.5 h-3.5" aria-hidden="true" />
</button>
</Show>
<Show when={props.onOpenSidecar}>
<button type="button" class="window-action" disabled={props.isLoading} onClick={() => run(() => props.onOpenSidecar?.())}>
<MonitorUp class="w-4 h-4" aria-hidden="true" />
<span>{t("newInstanceLauncher.sidecar")}</span>
</button>
</Show>
<button type="button" class="window-action new-instance-launcher-home" disabled={props.isLoading} onClick={() => run(props.onOpenHome)}>
<Home class="w-4 h-4" aria-hidden="true" />
<span>{t("newInstanceLauncher.fullHome")}</span>
</button>
</footer>
</Popover.Content>
</Popover.Portal>
</Popover>

View file

@ -18,7 +18,7 @@ export const folderSelectionMessages = {
"folderSelection.recent.remove": "Aus Liste entfernen",
"folderSelection.recent.openBadge": "Offen",
"newInstanceLauncher.title": "Neue Instanz",
"newInstanceLauncher.description": "Ein zuletzt verwendetes Projekt öffnen, ein neues beginnen oder andere Ziele erreichen.",
"newInstanceLauncher.description": "Ein zuletzt verwendetes Projekt öffnen oder ein neues beginnen.",
"newInstanceLauncher.searchLabel": "Zuletzt verwendete Projekte durchsuchen",
"newInstanceLauncher.searchPlaceholder": "Zuletzt verwendete Projekte durchsuchen…",
"newInstanceLauncher.recentTitle": "Zuletzt verwendet",
@ -28,10 +28,6 @@ export const folderSelectionMessages = {
"newInstanceLauncher.browse": "Durchsuchen…",
"newInstanceLauncher.temporary": "Temporär",
"newInstanceLauncher.clone": "Klonen…",
"newInstanceLauncher.servers": "Remote-Server",
"newInstanceLauncher.addServer": "Server hinzufügen…",
"newInstanceLauncher.sidecar": "SideCar",
"newInstanceLauncher.fullHome": "Vollständige Startseite",
"folderSelection.recent.openNewInstance": "Neue Instanz öffnen",
"folderSelection.recent.switchToOpenProject": "Zum offenen Projekt wechseln",

View file

@ -18,7 +18,7 @@ export const folderSelectionMessages = {
"folderSelection.recent.remove": "Remove from recent",
"folderSelection.recent.openBadge": "Open",
"newInstanceLauncher.title": "New instance",
"newInstanceLauncher.description": "Open a recent project, start a new one or reach other destinations.",
"newInstanceLauncher.description": "Open a recent project or start a new one.",
"newInstanceLauncher.searchLabel": "Search recent projects",
"newInstanceLauncher.searchPlaceholder": "Search recent projects…",
"newInstanceLauncher.recentTitle": "Recent",
@ -28,10 +28,6 @@ export const folderSelectionMessages = {
"newInstanceLauncher.browse": "Browse…",
"newInstanceLauncher.temporary": "Temporary",
"newInstanceLauncher.clone": "Clone…",
"newInstanceLauncher.servers": "Remote servers",
"newInstanceLauncher.addServer": "Add a server…",
"newInstanceLauncher.sidecar": "SideCar",
"newInstanceLauncher.fullHome": "Full home page",
"folderSelection.recent.openNewInstance": "Open new instance",
"folderSelection.recent.switchToOpenProject": "Switch to open project",

View file

@ -18,7 +18,7 @@ export const folderSelectionMessages = {
"folderSelection.recent.remove": "Eliminar de recientes",
"folderSelection.recent.openBadge": "Abierta",
"newInstanceLauncher.title": "Nueva instancia",
"newInstanceLauncher.description": "Abre un proyecto reciente, empieza uno nuevo o ve a otros destinos.",
"newInstanceLauncher.description": "Abre un proyecto reciente o empieza uno nuevo.",
"newInstanceLauncher.searchLabel": "Buscar proyectos recientes",
"newInstanceLauncher.searchPlaceholder": "Buscar proyectos recientes…",
"newInstanceLauncher.recentTitle": "Recientes",
@ -28,10 +28,6 @@ export const folderSelectionMessages = {
"newInstanceLauncher.browse": "Examinar…",
"newInstanceLauncher.temporary": "Temporal",
"newInstanceLauncher.clone": "Clonar…",
"newInstanceLauncher.servers": "Servidores remotos",
"newInstanceLauncher.addServer": "Añadir un servidor…",
"newInstanceLauncher.sidecar": "SideCar",
"newInstanceLauncher.fullHome": "Página de inicio completa",
"folderSelection.recent.openNewInstance": "Abrir una nueva instancia",
"folderSelection.recent.switchToOpenProject": "Cambiar al proyecto abierto",

View file

@ -18,7 +18,7 @@ export const folderSelectionMessages = {
"folderSelection.recent.remove": "Retirer des récents",
"folderSelection.recent.openBadge": "Ouvert",
"newInstanceLauncher.title": "Nouvelle instance",
"newInstanceLauncher.description": "Ouvrez un projet récent, démarrez-en un nouveau ou accédez à d’autres destinations.",
"newInstanceLauncher.description": "Ouvrez un projet récent ou démarrez-en un nouveau.",
"newInstanceLauncher.searchLabel": "Rechercher un projet récent",
"newInstanceLauncher.searchPlaceholder": "Rechercher un projet récent…",
"newInstanceLauncher.recentTitle": "Récents",
@ -28,10 +28,6 @@ export const folderSelectionMessages = {
"newInstanceLauncher.browse": "Parcourir…",
"newInstanceLauncher.temporary": "Temporaire",
"newInstanceLauncher.clone": "Cloner…",
"newInstanceLauncher.servers": "Serveurs distants",
"newInstanceLauncher.addServer": "Ajouter un serveur…",
"newInstanceLauncher.sidecar": "SideCar",
"newInstanceLauncher.fullHome": "Accueil complet",
"folderSelection.recent.openNewInstance": "Ouvrir une nouvelle instance",
"folderSelection.recent.switchToOpenProject": "Basculer vers le projet ouvert",

View file

@ -18,7 +18,7 @@ export const folderSelectionMessages = {
"folderSelection.recent.remove": "הסר מהרשימה האחרונה",
"folderSelection.recent.openBadge": "פתוח",
"newInstanceLauncher.title": "מופע חדש",
"newInstanceLauncher.description": "פתח פרויקט אחרון, התחל פרויקט חדש או עבור ליעדים אחרים.",
"newInstanceLauncher.description": "פתח פרויקט אחרון או התחל פרויקט חדש.",
"newInstanceLauncher.searchLabel": "חיפוש בפרויקטים אחרונים",
"newInstanceLauncher.searchPlaceholder": "חיפוש בפרויקטים אחרונים…",
"newInstanceLauncher.recentTitle": "אחרונים",
@ -28,10 +28,6 @@ export const folderSelectionMessages = {
"newInstanceLauncher.browse": "עיון…",
"newInstanceLauncher.temporary": "זמני",
"newInstanceLauncher.clone": "שכפול…",
"newInstanceLauncher.servers": "שרתים מרוחקים",
"newInstanceLauncher.addServer": "הוספת שרת…",
"newInstanceLauncher.sidecar": "SideCar",
"newInstanceLauncher.fullHome": "דף הבית המלא",
"folderSelection.recent.openNewInstance": "פתיחת מופע חדש",
"folderSelection.recent.switchToOpenProject": "עבור לפרויקט הפתוח",

View file

@ -18,7 +18,7 @@ export const folderSelectionMessages = {
"folderSelection.recent.remove": "履歴から削除",
"folderSelection.recent.openBadge": "開いています",
"newInstanceLauncher.title": "新しいインスタンス",
"newInstanceLauncher.description": "最近のプロジェクトを開く、新しく始める、または他の移動先へ。",
"newInstanceLauncher.description": "最近のプロジェクトを開く、または新しく始める。",
"newInstanceLauncher.searchLabel": "最近のプロジェクトを検索",
"newInstanceLauncher.searchPlaceholder": "最近のプロジェクトを検索…",
"newInstanceLauncher.recentTitle": "最近",
@ -28,10 +28,6 @@ export const folderSelectionMessages = {
"newInstanceLauncher.browse": "参照…",
"newInstanceLauncher.temporary": "一時",
"newInstanceLauncher.clone": "クローン…",
"newInstanceLauncher.servers": "リモートサーバー",
"newInstanceLauncher.addServer": "サーバーを追加…",
"newInstanceLauncher.sidecar": "SideCar",
"newInstanceLauncher.fullHome": "ホームページ全体",
"folderSelection.recent.openNewInstance": "新しいインスタンスを開く",
"folderSelection.recent.switchToOpenProject": "開いているプロジェクトに切り替え",

View file

@ -18,7 +18,7 @@ export const folderSelectionMessages = {
"folderSelection.recent.remove": "भर्खरको सूचीबाट हटाउनुहोस्",
"folderSelection.recent.openBadge": "खोल्नुहोस्",
"newInstanceLauncher.title": "नयाँ उदाहरण",
"newInstanceLauncher.description": "हालको परियोजना खोल्नुहोस्, नयाँ सुरु गर्नुहोस् वा अन्य गन्तव्यमा जानुहोस्।",
"newInstanceLauncher.description": "हालको परियोजना खोल्नुहोस् वा नयाँ सुरु गर्नुहोस्।",
"newInstanceLauncher.searchLabel": "हालका परियोजनाहरू खोज्नुहोस्",
"newInstanceLauncher.searchPlaceholder": "हालका परियोजनाहरू खोज्नुहोस्…",
"newInstanceLauncher.recentTitle": "हालका",
@ -28,10 +28,6 @@ export const folderSelectionMessages = {
"newInstanceLauncher.browse": "ब्राउज…",
"newInstanceLauncher.temporary": "अस्थायी",
"newInstanceLauncher.clone": "क्लोन…",
"newInstanceLauncher.servers": "रिमोट सर्भरहरू",
"newInstanceLauncher.addServer": "सर्भर थप्नुहोस्…",
"newInstanceLauncher.sidecar": "SideCar",
"newInstanceLauncher.fullHome": "पूर्ण गृह पृष्ठ",
"folderSelection.recent.openNewInstance": "नयाँ इन्स्ट्यान्स खोल्नुहोस्",
"folderSelection.recent.switchToOpenProject": "खुला परियोजनामा जानुहोस्",

View file

@ -18,7 +18,7 @@ export const folderSelectionMessages = {
"folderSelection.recent.remove": "Убрать из недавних",
"folderSelection.recent.openBadge": "Открыта",
"newInstanceLauncher.title": "Новый экземпляр",
"newInstanceLauncher.description": "Откройте недавний проект, начните новый или перейдите в другие разделы.",
"newInstanceLauncher.description": "Откройте недавний проект или начните новый.",
"newInstanceLauncher.searchLabel": "Поиск по недавним проектам",
"newInstanceLauncher.searchPlaceholder": "Поиск по недавним проектам…",
"newInstanceLauncher.recentTitle": "Недавние",
@ -28,10 +28,6 @@ export const folderSelectionMessages = {
"newInstanceLauncher.browse": "Обзор…",
"newInstanceLauncher.temporary": "Временный",
"newInstanceLauncher.clone": "Клонировать…",
"newInstanceLauncher.servers": "Удалённые серверы",
"newInstanceLauncher.addServer": "Добавить сервер…",
"newInstanceLauncher.sidecar": "SideCar",
"newInstanceLauncher.fullHome": "Полная главная страница",
"folderSelection.recent.openNewInstance": "Открыть новый экземпляр",
"folderSelection.recent.switchToOpenProject": "Перейти к открытому проекту",

View file

@ -14,7 +14,7 @@ export const folderSelectionMessages = {
"folderSelection.recent.remove": "Son kullanılanlardan kaldır",
"folderSelection.recent.openBadge": "Aç",
"newInstanceLauncher.title": "Yeni instance",
"newInstanceLauncher.description": "Son kullanılan bir projeyi açın, yenisini başlatın veya diğer hedeflere gidin.",
"newInstanceLauncher.description": "Son kullanılan bir projeyi açın veya yenisini başlatın.",
"newInstanceLauncher.searchLabel": "Son projelerde ara",
"newInstanceLauncher.searchPlaceholder": "Son projelerde ara…",
"newInstanceLauncher.recentTitle": "Son kullanılanlar",
@ -24,10 +24,6 @@ export const folderSelectionMessages = {
"newInstanceLauncher.browse": "Gözat…",
"newInstanceLauncher.temporary": "Geçici",
"newInstanceLauncher.clone": "Klonla…",
"newInstanceLauncher.servers": "Uzak sunucular",
"newInstanceLauncher.addServer": "Sunucu ekle…",
"newInstanceLauncher.sidecar": "SideCar",
"newInstanceLauncher.fullHome": "Tam ana sayfa",
"folderSelection.recent.openNewInstance": "Yeni instance aç",
"folderSelection.recent.switchToOpenProject": "Açık projeye geç",
"folderSelection.browse.title": "Klasöre Göz At",

View file

@ -18,7 +18,7 @@ export const folderSelectionMessages = {
"folderSelection.recent.remove": "从最近列表移除",
"folderSelection.recent.openBadge": "已打开",
"newInstanceLauncher.title": "新建实例",
"newInstanceLauncher.description": "打开最近的项目、新建项目或前往其他位置。",
"newInstanceLauncher.description": "打开最近的项目或新建项目。",
"newInstanceLauncher.searchLabel": "搜索最近的项目",
"newInstanceLauncher.searchPlaceholder": "搜索最近的项目…",
"newInstanceLauncher.recentTitle": "最近",
@ -28,10 +28,6 @@ export const folderSelectionMessages = {
"newInstanceLauncher.browse": "浏览…",
"newInstanceLauncher.temporary": "临时",
"newInstanceLauncher.clone": "克隆…",
"newInstanceLauncher.servers": "远程服务器",
"newInstanceLauncher.addServer": "添加服务器…",
"newInstanceLauncher.sidecar": "SideCar",
"newInstanceLauncher.fullHome": "完整主页",
"folderSelection.recent.openNewInstance": "打开新实例",
"folderSelection.recent.switchToOpenProject": "切换到已打开的项目",

View file

@ -51,8 +51,7 @@
padding: var(--space-xs);
}
.new-instance-launcher-recent,
.new-instance-launcher-server {
.new-instance-launcher-recent {
display: grid;
grid-template-columns: auto minmax(0, max-content) minmax(0, 1fr) auto;
align-items: center;
@ -72,12 +71,6 @@
box-shadow: inset 0 0 0 1px var(--list-item-highlight-border);
}
.new-instance-launcher-server:hover,
.new-instance-launcher-server:focus-visible {
background-color: var(--surface-hover-overlay);
outline: none;
}
.new-instance-launcher-recent[aria-disabled="true"] {
cursor: default;
opacity: 0.6;
@ -144,27 +137,6 @@
text-overflow: ellipsis;
}
.new-instance-launcher-servers {
display: flex;
flex-direction: column;
padding: var(--space-xs);
border-top: 1px solid color-mix(in srgb, var(--border-base) 50%, transparent);
}
.new-instance-launcher-server {
grid-template-columns: auto minmax(0, 1fr) auto;
}
.new-instance-launcher-footer {
flex-wrap: wrap;
justify-content: flex-start;
gap: var(--space-xs);
}
.new-instance-launcher-home {
margin-inline-start: auto;
}
/* Long press must not select text or open the touch callout on the button. */
.new-instance-button {
user-select: none;

View file

@ -9,5 +9,6 @@ export const recentFolders = [
...Array.from({ length: 12 }, (_, i) => ({ path: `D:/archive/project-${i}`, lastAccessed: now - (8 + i) * 86_400_000 })),
]
serverApi.fetchConfigOwner = async () => ({ settings: { locale: "en" } }) as any
serverApi.fetchStateOwner = async () => ({ recentFolders }) as any
serverApi.patchStateOwner = async () => ({ recentFolders }) as any
const state = { recentFolders, remoteServers: [{ id: "saved-server", name: "Saved remote", baseUrl: "https://remote.example.test", skipTlsVerify: false, createdAt: now, updatedAt: now }] }
serverApi.fetchStateOwner = async () => state as any
serverApi.patchStateOwner = async () => state as any

View file

@ -1,5 +1,5 @@
import "./new-instance-launcher-state"
import { createSignal } from "solid-js"
import { createSignal, lazy, Show } from "solid-js"
import { render } from "solid-js/web"
import InstanceTabs from "../../../src/components/instance-tabs"
import NewInstanceLauncher from "../../../src/components/new-instance-launcher"
@ -15,22 +15,24 @@ addInstance(open)
const tabs: AppTabRecord[] = [{ id: `instance:${open.id}`, kind: "instance", instance: open }]
const calls: string[] = []
const Home = lazy(() => import("../../../src/components/folder-selection-view"))
const [launcherOpen, setLauncherOpen] = createSignal(false)
function Fixture() {
const [anchor, setAnchor] = createSignal<HTMLElement>()
return <>
return <Show when={new URLSearchParams(location.search).has("home")} fallback={<>
<InstanceTabs tabs={tabs} activeTabId={tabs[0].id} onSelect={() => {}} onClose={() => {}}
onNew={() => calls.push("home")} onMoveTab={() => {}}
onOpenLauncher={(element) => { setAnchor(element); setLauncherOpen(true) }} launcherOpen={launcherOpen()} />
<NewInstanceLauncher open={launcherOpen()} anchor={anchor()} isLoading={false} onOpenChange={setLauncherOpen}
onSelectFolder={(path) => calls.push(`folder:${path}`)}
onSelectExistingInstance={(id, path) => calls.push(`existing:${id}:${path}`)}
onNewTemporaryInstance={() => calls.push("temporary")}
onOpenSidecar={() => calls.push("sidecar")}
onOpenHome={() => calls.push("full-home")} />
onNewTemporaryInstance={() => calls.push("temporary")} />
{/* Stands in for another window (command palette, home page) taking focus. */}
<input id="other-surface" aria-label="Other surface" />
</>
</>}>
<Home onSelectFolder={(path) => calls.push(`folder:${path}`)} onSelectExistingInstance={() => {}}
onNewTemporaryInstance={() => calls.push("temporary")} onOpenSidecar={() => calls.push("sidecar")} />
</Show>
}
render(() => <ConfigProvider><I18nProvider><Fixture /></I18nProvider></ConfigProvider>, document.getElementById("root")!)

View file

@ -4,10 +4,13 @@ import { fileURLToPath } from "node:url"
import { chromium, type Browser, type Page } from "playwright"
import { createServer, type ViteDevServer } from "vite"
import solid from "vite-plugin-solid"
import { createFixtureCache } from "./fixture-cache"
let server: ViteDevServer, browser: Browser, url: string
let cache: Awaited<ReturnType<typeof createFixtureCache>>
before(async () => {
server = await createServer({ configFile: false, root: fileURLToPath(new URL("../..", import.meta.url)), logLevel: "error",
cache = await createFixtureCache()
server = await createServer({ configFile: false, cacheDir: cache.cacheDir, root: fileURLToPath(new URL("../..", import.meta.url)), logLevel: "error",
plugins: [solid(), { name: "new-instance-launcher-fixture", configureServer(s) {
s.middlewares.use("/fixture", async (_req, res) => {
res.setHeader("Content-Type", "text/html")
@ -20,16 +23,25 @@ before(async () => {
url = `http://127.0.0.1:${(server.httpServer!.address() as { port: number }).port}/fixture`
browser = await chromium.launch({ executablePath: process.env.CODENOMAD_BROWSER_PATH || undefined })
})
after(async () => { await browser?.close(); await server?.close() })
after(async () => { await browser?.close(); await server?.close(); await cache?.dispose() })
async function prepare(width = 1100) {
async function prepare(width = 1100, home = false) {
const page = await browser.newPage({ viewport: { width, height: 760 }, locale: "en-US" })
const errors: string[] = []
const remoteRequests: string[] = []
page.on("pageerror", error => errors.push(error.message))
page.on("request", request => {
if (/\/api\/remote-/.test(request.url())) remoteRequests.push(request.url())
})
// Local native-window capabilities, without any real host or user state.
await page.addInitScript(() => {
(window as any).__CODENOMAD_RUNTIME_HOST__ = "electron"
window.__CODENOMAD_WINDOW_CONTEXT__ = "local"
})
await page.route("**/api/**", route => route.fulfill({ contentType: "application/json", body: "{}" }))
await page.goto(url)
await page.goto(home ? `${url}?home` : url)
await page.waitForFunction(() => Boolean((window as any).launcherFixture))
return { page, errors, calls: () => page.evaluate(() => [...(window as any).launcherFixture.calls]) }
return { page, errors, remoteRequests, calls: () => page.evaluate(() => [...(window as any).launcherFixture.calls]) }
}
const plus = (page: Page) => page.locator(".new-instance-button")
const launcher = (page: Page) => page.locator(".new-instance-launcher")
@ -41,7 +53,7 @@ const openedWithFocus = async (page: Page) => {
}
test("a click keeps the full home page while a long press opens the launcher", async () => {
const { page, errors, calls } = await prepare()
const { page, errors, remoteRequests, calls } = await prepare()
try {
await plus(page).click()
assert.deepEqual(await calls(), ["home"])
@ -61,6 +73,10 @@ test("a click keeps the full home page while a long press opens the launcher", a
assert.equal(await options(page).first().getAttribute("aria-selected"), "true")
assert.match(await options(page).first().innerText(), /CodeNomad[\s\S]*open/)
assert.match(await options(page).nth(2).innerText(), /Blender tools/)
assert.equal(await launcher(page).locator("footer, .new-instance-launcher-servers").count(), 0)
assert.equal(await launcher(page).getByRole("button", { name: /Remote servers|SideCar|Full home page|Add a server/ }).count(), 0)
assert.deepEqual(await launcher(page).getByRole("button").allTextContents(), ["Browse…", "Temporary", "Clone…"])
assert.deepEqual(remoteRequests, [], "saved profiles do not trigger launcher remote reads or probes")
if (process.env.CODENOMAD_LAUNCHER_CAPTURE) await page.screenshot({ path: process.env.CODENOMAD_LAUNCHER_CAPTURE })
assert.deepEqual(errors, [])
} finally { await page.close() }
@ -99,17 +115,26 @@ test("typing filters recent projects and the keyboard opens or switches to them"
} finally { await page.close() }
})
test("creation and secondary actions close the launcher before running", async () => {
test("project actions stay available in a narrow launcher and close it before running", async () => {
const { page, errors, calls } = await prepare(390)
try {
for (const [name, call] of [["Temporary", "temporary"], ["SideCar", "sidecar"], ["Full home page", "full-home"]] as const) {
// Exercise the real browser folder picker rather than a native OS dialog.
await page.evaluate(() => { (window as any).__CODENOMAD_RUNTIME_HOST__ = "web" })
for (const name of ["Browse…", "Temporary", "Clone…"]) {
await plus(page).click({ button: "right" })
await launcher(page).waitFor()
const width = await launcher(page).evaluate(el => el.getBoundingClientRect().width)
assert.ok(width <= 390, `launcher fits a narrow window (${width}px)`)
assert.equal(await launcher(page).locator("footer").count(), 0)
if (name === "Temporary" && process.env.CODENOMAD_LAUNCHER_NARROW_CAPTURE) await page.screenshot({ path: process.env.CODENOMAD_LAUNCHER_NARROW_CAPTURE })
await launcher(page).getByRole("button", { name, exact: true }).click()
await launcher(page).waitFor({ state: "detached" })
assert.equal((await calls()).at(-1), call)
if (name === "Temporary") assert.equal((await calls()).at(-1), "temporary")
else {
await page.getByRole("dialog").getByRole("heading", { name: name === "Browse…" ? "Select Workspace" : "Clone Git Repository", exact: true }).waitFor()
await page.keyboard.press("Escape")
await page.getByRole("dialog").waitFor({ state: "detached" })
}
}
assert.deepEqual(errors, [])
} finally { await page.close() }
@ -118,12 +143,14 @@ test("creation and secondary actions close the launcher before running", async (
test("dismissal returns focus, aborted long presses keep keyboard activation, and the active row stays visible", async () => {
const { page, errors, calls } = await prepare()
try {
await plus(page).focus()
await page.keyboard.press("Alt+ArrowDown")
await openedWithFocus(page)
await page.keyboard.press("Escape")
await launcher(page).waitFor({ state: "detached" })
await page.waitForFunction(() => document.activeElement?.classList.contains("new-instance-button"))
for (const key of ["Alt+ArrowDown", "Shift+F10", "ContextMenu"]) {
await plus(page).focus()
await page.keyboard.press(key)
await openedWithFocus(page)
await page.keyboard.press("Escape")
await launcher(page).waitFor({ state: "detached" })
await page.waitForFunction(() => document.activeElement?.classList.contains("new-instance-button"))
}
// Long press, then release away from the button: no click ends it.
const box = (await plus(page).boundingBox())!
@ -213,4 +240,18 @@ test("the new-instance button follows the last tab like a browser", async () =>
if (process.env.CODENOMAD_LAUNCHER_TABS_CAPTURE) await page.screenshot({ path: process.env.CODENOMAD_LAUNCHER_TABS_CAPTURE, clip: { x: 0, y: 0, width: 1100, height: 60 } })
assert.deepEqual(errors, [])
} finally { await page.close() }
})
})
test("the full home page keeps saved servers, server setup and SideCar actions", async () => {
const { page, errors, calls } = await prepare(1100, true)
try {
await page.getByRole("button", { name: "Servers 1 Servers", exact: true }).click()
await page.getByRole("button", { name: /^Saved remote/ }).waitFor()
await page.getByRole("button", { name: "Connect CodeNomad Server", exact: true }).click()
await page.getByRole("dialog", { name: "Connect to Server", exact: true }).waitFor()
await page.getByRole("button", { name: "Cancel", exact: true }).click()
await page.getByRole("button", { name: "Open SideCar", exact: true }).click()
assert.deepEqual(await calls(), ["sidecar"])
assert.deepEqual(errors, [])
} finally { await page.close() }
})