docs(memory): notes projet du sprint UI rework et des tickets #7/#14/#16/#17/#18/#25/#28
Capitalise les notes durables produites pendant le sprint UI rework et les tickets livrés depuis. Commit séparé du code : `.ideai/memory/` est le store durable versionné, il ne doit jamais être mélangé aux commits de feature. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -59,3 +59,17 @@
|
||||
- [checkpoint-t1-wake-delivery-bug-fixed-rebuild-pending](checkpoint-t1-wake-delivery-bug-fixed-rebuild-pending.md) — memory note checkpoint-t1-wake-delivery-bug-fixed-rebuild-pending
|
||||
- [workstate-background-tasks-projection-fix](workstate-background-tasks-projection-fix.md) — Décision d'archi pour rendre visibles Cancel/Retry dans le panneau Work — extension du read-model GetProjectWorkState, contrat DTO et borne de livraison.
|
||||
- [tickets-t3-frontend-validation-verdict](tickets-t3-frontend-validation-verdict.md) — Verdict de validation frontend du fix T3 (projection backgroundTasks par agent) sur feature/background-tasks-first-class — vert, avec 3 désalignements de contrat mineurs.
|
||||
- [ticket4-restart-on-develop-ebd992e](ticket4-restart-on-develop-ebd992e.md) — Cadrage du redémarrage des annonces inter-agent sur la base pré-canonique develop ebd992e — ce qui existe, l'écart live, la réutilisabilité du backup et le découpage B/F.
|
||||
- [ticket4-overlay-composition-leafview](ticket4-overlay-composition-leafview.md) — Règle de composition des deux voiles plein-cellule de LeafView (write-portal injection PTY vs F3 annonces busy) — exclusion mutuelle avec priorité F3, arbitrée pour éviter le double voile.
|
||||
- [ticket7-f2-delegated-limit-no-agentratelimited](ticket7-f2-delegated-limit-no-agentratelimited.md) — Écart backend ticket #7/F2 : le rendez-vous délégué n'émet pas AgentRateLimited pour la cible limitée.
|
||||
- [ui-rework-sprint-scoping-contracts](ui-rework-sprint-scoping-contracts.md) — Frontières hexagonales et contrats figés du sprint UI rework — les 4 tickets sont frontend-purs, la popup TicketPicker #18, l'échelle z-index et le curseur opaque.
|
||||
- [ticket18-picker-delivered-devfrontend](ticket18-picker-delivered-devfrontend.md) — memory note ticket18-picker-delivered-devfrontend
|
||||
- [ticket16-menus-floating-delivered-devfrontend](ticket16-menus-floating-delivered-devfrontend.md) — memory note ticket16-menus-floating-delivered-devfrontend
|
||||
- [floatingwindow-focus-trap-mount-only](floatingwindow-focus-trap-mount-only.md) — Le focus-trap de FloatingWindow ne doit dépendre que du mount ; sinon un onClose instable vole le focus à chaque frappe.
|
||||
- [ticket17-focus-trap-fix-merged-develop](ticket17-focus-trap-fix-merged-develop.md) — Correctif de la perte de focus dans l'édition de ticket (#17) livré sur develop via feature branch dédiée.
|
||||
- [ui-rework-sprint-tickets-21-22-23-scoping](ui-rework-sprint-tickets-21-22-23-scoping.md) — memory note ui-rework-sprint-tickets-21-22-23-scoping
|
||||
- [ui-rework-sprint-delivered-tickets-21-22-23](ui-rework-sprint-delivered-tickets-21-22-23.md) — memory note ui-rework-sprint-delivered-tickets-21-22-23
|
||||
- [ticket25-assistant-sandbox-eacces-rootcause](ticket25-assistant-sandbox-eacces-rootcause.md) — Root cause et contrat de fix du EACCES (os error 13) au spawn de la CLI d'un assistant de ticket — le preset SandboxPlan::project_read_only handle la classe exec Landlock.
|
||||
- [ticket14-local-lan-openai-adapter-scoping](ticket14-local-lan-openai-adapter-scoping.md) — memory note ticket14-local-lan-openai-adapter-scoping
|
||||
- [ticket28-firstrun-detect-hang-scoping](ticket28-firstrun-detect-hang-scoping.md) — Cadrage hexagonal du bug bloquant first-run (#28) — panic block_on imbriqué dans detect + découplage busy/détection, contrats et point de vérité QA.
|
||||
- [ticket28-f1-frontend-delivered](ticket28-f1-frontend-delivered.md) — Lot F1 du ticket #28 livré sur feature/ticket28-firstrun-detect-hang — invariant busy/detectProfiles, flag detecting, timeout UI, test de non-régression.
|
||||
|
||||
11
.ideai/memory/floatingwindow-focus-trap-mount-only.md
Normal file
11
.ideai/memory/floatingwindow-focus-trap-mount-only.md
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
name: floatingwindow-focus-trap-mount-only
|
||||
description: Le focus-trap de FloatingWindow ne doit dépendre que du mount ; sinon un onClose instable vole le focus à chaque frappe.
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
Le primitif `frontend/src/shared/ui/FloatingWindow.tsx` a un `useEffect` focus-trap qui appelle `(first ?? node)?.focus()`. Sa dépendance doit rester `[]` (mount-only), et `onClose` doit être lu via un `onCloseRef` maintenu à jour.
|
||||
|
||||
**Pourquoi** : les appelants (ex. `TicketDetail.requestClose`) passent souvent un `onClose` recréé à chaque render. Si l'effet dépend de `[onClose]`, chaque frappe dans un input contrôlé de la fenêtre → re-render → nouvelle identité `onClose` → l'effet se ré-arme → focus renvoyé au premier focusable → l'utilisateur ne peut taper qu'UNE lettre à la fois (bug #17, corrigé 2026-07-07).
|
||||
|
||||
**Comment l'appliquer** : ne jamais remettre `onClose` (ni une autre prop changeante) dans les deps de cet effet ; router les callbacks via une ref. Régression couverte par le test « #17 focus-loss bug » dans FloatingWindow.test.tsx. Voir [[ticket16-menus-floating-delivered-devfrontend]].
|
||||
64
.ideai/memory/ticket14-local-lan-openai-adapter-scoping.md
Normal file
64
.ideai/memory/ticket14-local-lan-openai-adapter-scoping.md
Normal file
@ -0,0 +1,64 @@
|
||||
---
|
||||
name: ticket14-local-lan-openai-adapter-scoping
|
||||
description: memory note ticket14-local-lan-openai-adapter-scoping
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Ticket #14 — cadrage figé : adapter IA local/LAN OpenAI-compatible canonique
|
||||
|
||||
Périmètre figé (review agent-user faite) : canal = **adapter HTTP natif** parlant une API compatible OpenAI/Ollama ; usage **transparent** vs Claude/Codex (mêmes cellules, conversation headless canonique, historique, reprise, délégation, vivacité) ; **PUREMENT ADDITIF** (zéro régression Claude Code / Codex CLI) ; **parité tool-calling/MCP** dès ce ticket avec **dégradation propre** si le modèle n'expose pas les outils.
|
||||
|
||||
## 0. Asymétrie structurelle (commande TOUT le design)
|
||||
| | Claude/Codex (existant) | Modèle local HTTP (#14) |
|
||||
|---|---|---|
|
||||
| Nature | binaire CLI spawné | serveur HTTP appelé |
|
||||
| Boucle agentique | conduite par la CLI | conduite par **IdeA** |
|
||||
| Rôle MCP d'IdeA | serveur ; CLI = client | pas de client ⇒ IdeA = orchestrateur d'outils |
|
||||
| Contexte .md | la CLI lit son convention file | injecté en **system message** par l'adapter |
|
||||
| Conversation | id moteur (session_id/thread_id) | API **stateless** ⇒ transcript possédé par IdeA |
|
||||
Conséquence : le nouvel adapter **n'utilise PAS** `session/process.rs` (run_turn/spawn/drain). Il ne partage avec claude.rs/codex.rs que les **types de port** (`AgentSession`, `ReplyEvent`). Cette absence de code partagé mutable EST la garantie d'isolation.
|
||||
|
||||
## 1. Cartographie hexagonale
|
||||
**Domaine (crates/domain)**
|
||||
- Variante `StructuredAdapter::OpenAiCompatible` (profile.rs:244). `provider_key()` ⇒ `"openai-compatible"` (contrat persistance providers.json). Sérialise camelCase `"openAiCompatible"`.
|
||||
- Nouveau VO validé (parse-don't-validate) `HttpChatConfig { endpoint:String (http/https non vide), model:String (non vide), api_key_env:Option<String> (nom de var env valide, JAMAIS la clé), request_timeout_ms:Option<u32>, connect_timeout_ms:Option<u32>, max_tool_iterations:Option<u16> (garde-fou boucle, défaut ~16) }`. Le domaine ne résout jamais la clé (pas d'I/O env), il porte le **nom** de variable.
|
||||
- Rattachement `AgentProfile.chat_http: Option<HttpChatConfig>` avec `#[serde(default, skip_serializing_if="Option::is_none")]` (miroir exact de `mcp`/`liveness`/`rate_limit_pattern`, profile.rs:570-604) ⇒ **zéro régression de sérialisation** (profils Claude/Codex bit-identiques). Test round-trip obligatoire (miroir profile_without_mcp_round_trips_identically).
|
||||
|
||||
**Nouveau port tool-calling (le seam clé — le pont MCP .mcp.json/config.toml + bridge `idea mcp-server` NE s'applique pas, pas de client CLI)**
|
||||
```rust
|
||||
pub struct ToolSpec { name:String, description:String, input_schema:serde_json::Value }
|
||||
#[async_trait] pub trait ToolInvoker: Send+Sync {
|
||||
fn tools(&self) -> Vec<ToolSpec>;
|
||||
async fn call(&self, name:&str, args_json:&str) -> Result<String, ToolInvocationError>;
|
||||
}
|
||||
```
|
||||
Implémenté en **app-tauri** en déléguant à la **même** `OrchestratorService::dispatch` (orchestrator/service.rs:1082) que le endpoint MCP ⇒ parité délégation/rendez-vous (idea_ask_agent/idea_reply), gating permissions/sandbox conservé. Injecté dans la factory via `with_tool_invoker(Arc<dyn ToolInvoker>)` (jumeau de `with_sandbox_enforcer`). `None` ⇒ tool-calling désactivé proprement (chat nu).
|
||||
|
||||
**Infra** : nouveau fichier `crates/infrastructure/src/session/openai_compat.rs` (jumeau structurel de codex.rs SANS process). Client HTTP **reqwest + rustls-tls** (jamais native-tls — spike AppImage §13-2, confiné au crate infra). Parsing isolé pur `parse_chat_delta`/`parse_completion` (miroir de parse_event) — seul endroit qui connaît le schéma OpenAI (choices[].message, tool_calls[], SSE data:).
|
||||
|
||||
**Routing — seul contact avec l'existant** : `StructuredSessionFactory::start` (factory.rs:111) = **UN bras de match ajouté** ; bras Claude/Codex INTOUCHÉS ; `supports()` reste `profile.is_selectable()` (=structured_adapter.is_some(), profile.rs:857) ⇒ sélectionnable sans changer le prédicat.
|
||||
|
||||
## 2. Contrat de session headless
|
||||
`send(prompt)` : 1) transcript.push(user) ; 2) émettre Heartbeat ; 3) POST {endpoint}/chat/completions {model, messages:transcript, tools?:invoker.tools(), stream:true} ; 4) si tool_calls ⇒ pour chaque: ToolActivity{label=name} + result=invoker.call(name,args) + push(assistant tool_call)+push(tool result), reboucler (borné max_tool_iterations) ; 5) sinon streamer chunks ⇒ TextDelta (+tap→Announcement, cf claude.rs:337) ; 6) réponse complète ⇒ push(assistant) + **Final{content}** ; 7) persister transcript run dir. Contrat de flux universel respecté (un seul Final terminal) ⇒ send_blocking/drain_with_readiness marchent tels quels.
|
||||
|
||||
**Erreurs (jamais de corps HTTP brut propagé)** : endpoint injoignable 1er contact/probe ⇒ `AgentSessionError::Start` (UI erreur, IdeA non bloqué) ; réseau/coupure en tour ⇒ `Io` ; JSON illisible/schéma ⇒ `Decode` (diagnostic court) ; timeout ⇒ `Timeout` via send_blocking, session non tuée ; modèle absent (404/400) ⇒ `Start` dédié.
|
||||
|
||||
**Reprise sans mélange d'ids (invariant du ticket)** : /chat/completions **stateless** ⇒ AUCUN id provider ⇒ `conversation_id()` retourne **None** (rien à mélanger). État = transcript provider-shaped (roles+tool_calls) **possédé par IdeA**, persisté dans le **run dir stable** `.ideai/run/<agent-id>/chat-transcript.json` (clé = agent, jamais id provider). Reprise = ré-instanciation factory ⇒ rechargement transcript ; `seed_conversation_id` (factory.rs:63) **ignoré** par cet adapter (documenter). DISTINCT du conversation-log LS6 (log.jsonl, dérivé ReplyEvent, surface humaine, inchangé) — deux artefacts, aucun mélange.
|
||||
|
||||
**Contexte** : `start` reçoit `ctx:&PreparedContext` ; `ctx.content` (ports.rs:120) = Markdown rendu ⇒ injecté comme premier message **system** (le serveur HTTP ne lit pas de convention file).
|
||||
|
||||
## 3. Seam tool-calling / dégradation
|
||||
Parité : ToolInvoker câblé ⇒ modèle local voit idea_* via la même OrchestratorService (délégation/rendez-vous/contexte/mémoire/tickets identiques, gating conservé). Dégradation 3 niveaux : (1) ToolInvoker None ⇒ chat nu ; (2) profil opte sans outils ⇒ idem ; (3) endpoint rejette param `tools` ⇒ détecter, retirer tools, rejouer chat nu, 1 diagnostic — l'agent converse toujours, sans déléguer. Garde-fou max_tool_iterations (modèles locaux moins fiables) ⇒ au plafond, Final + note.
|
||||
|
||||
## 4. Lots B/F
|
||||
Backend : **B1** domaine (variante+provider_key, VO HttpChatConfig, champ chat_http, port ToolInvoker/ToolSpec/ToolInvocationError ; tests sérialisation zéro-régression, round-trip, invariants ; zéro I/O). **B2** adapter infra openai_compat (reqwest/rustls, parse_* purs, mapping ReplyEvent+erreurs, résolution clé env ; tests fixtures OpenAI/Ollama, erreurs via wiremock, pas de fuite payload). **B3** boucle outils bornée + persistance/rechargement transcript run-dir (reprise) + dégradation tools non supporté (tests ToolInvoker fake, reprise=rechargement, plafond). **B4** routing (bras match) + composition root (with_tool_invoker, impl ToolInvoker→OrchestratorService) + profil de référence "Ollama / OpenAI-compatible local model" éditable (tests factory route, supports true, probe indispo→Start).
|
||||
Frontend : **F1** types TS HttpChatConfig + champ chatHttp? + adapter "openAiCompatible". **F2** wizard/settings : sélectionnable comme Claude/Codex, form endpoint/model/apiKeyEnv/timeouts, validation miroir backend (first-run/profile.ts isValidEnvVar, URL, non-vide), apiKeyEnv = nom de var jamais clé. **F3** cellule = chemin chat structuré existant (dérivé de is_selectable, "gratuit" si DTO respecté) + erreur "endpoint indisponible" propre sans bloquer UI (tests RTL gateways mock).
|
||||
Frontière B↔F : **DTO AgentProfile** (déjà le pont IPC) porte structuredAdapter:"openAiCompatible" + chatHttp{endpoint,model,apiKeyEnv?,requestTimeoutMs?,connectTimeoutMs?,maxToolIterations?}. Sélectionnabilité + rendu chat dérivés du DTO existant, pas de nouveau canal ni commande Tauri au cœur (le seed du profil de référence peut passer par le CRUD profils existant — à confirmer B4/F3).
|
||||
|
||||
## 5. Vigilance régression / invariants
|
||||
1. Zéro régression sérialisation (skip_serializing_if=Option::is_none ; round-trip Claude/Codex bit-identique — BLOQUANT). 2. Isolation code : nouvel adapter = fichier neuf ; INTERDIT de toucher claude.rs/codex.rs/process.rs ; seul diff existant = 1 bras match factory.rs + champs optionnels domaine. 3. Contrat flux : exactement un Final terminal ; Heartbeat/ToolActivity/TextDelta/RateLimited non terminaux (conformance.rs doit couvrir le nouvel adapter). 4. Frontière domaine : reqwest UNIQUEMENT infra ; aucun type HTTP en domaine/application ; ToolInvoker port pur. 5. Nouvelle dépendance AppImage : reqwest rustls-tls (pas OpenSSL, spike §13-2). 6. Non-mélange ids : conversation_id()=None ; transcript clé-agent ; distinct de LS6. 7. Sandbox : pas de process spawné ⇒ SandboxEnforcer OS = no-op pour le tour (pas une régression) mais les outils via ToolInvoker gardent le gating orchestrateur.
|
||||
|
||||
## 6. Décisions produit remontées à Main (NON tranchées par Architect)
|
||||
1. Surface d'outils v1 : parité totale immédiate vs sous-ensemble curaté (fiabilité tool-calling des modèles locaux). Reco : parité + garde-fou max_tool_iterations.
|
||||
2. Streaming SSE dès v1 (reco, parité observabilité live) vs non-streaming (repli si endpoint ne stream pas).
|
||||
3. Nom canonique variante : `OpenAiCompatible` (reco, décrit le protocole ; provider_key figé) vs `LocalChat` (formulation ticket).
|
||||
@ -0,0 +1,29 @@
|
||||
---
|
||||
name: ticket16-menus-floating-delivered-devfrontend
|
||||
description: memory note ticket16-menus-floating-delivered-devfrontend
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Lot A / #16 — MenuBar + FloatingWindow livré (DevFrontend)
|
||||
|
||||
Branche `feature/ui-menus-floating` (base develop 0218e32), non commité, avant merge Git. `vitest` **vert** : 54 fichiers / 527 tests. `tsc --noEmit` exit 0.
|
||||
|
||||
## UX pass validée utilisateur (barre unique + sélecteur projet) — APPLIQUÉE
|
||||
- **Barre unique** : plus de MenuBar séparée dans App.tsx. Une seule MenuBar dans `ProjectsView` regroupant `[ Projet: <actif> ▾ ] File ▾ View ▾ Settings ▾`.
|
||||
- **Sélecteur de projet permanent** : premier menu de la barre, label = projet actif, items = tous les projets connus (`vm.projects`) → `selectProject(id)` (active l'onglet si ouvert, sinon `openProject`). Accessible même sous projet actif, sans ouvrir File→Projects….
|
||||
- **Settings → AI Profiles** déplacé dans la barre unique : toggle `showSettings` LOCAL à ProjectsView ; le main area swap vers `<ProfilesSettings/>` pendant que la barre reste visible (permet de refermer). App.tsx ne gère plus showSettings ni ProfilesSettings (juste firstRun ? wizard : ProjectsView).
|
||||
|
||||
## Fichiers (état final)
|
||||
- **Créés (lot A initial)** : `shared/ui/FloatingWindow.tsx`, `shared/ui/MenuBar.tsx`, `shared/ui/FloatingWindow.test.tsx`.
|
||||
- **Modifiés** : `shared/index.ts`, `shared/ui/zIndex.ts` (réutilisé), `app/App.tsx` (barre + showSettings + ProfilesSettings retirés), `features/projects/ProjectsView.tsx` (barre unique : projectMenu+File+View+Settings, showSettings local, ProfilesSettings dans main), `features/projects/projects.test.tsx` (+2 tests : sélecteur projet, toggle AI Profiles), `features/projects/ProjectsView.ls7.test.tsx`.
|
||||
|
||||
## Invariants conservés
|
||||
- FloatingWindow (focus-trap+Échap+backdrop, z=floatingWindow 50), MenuBar dropdowns z=menuDropdown 40, toasts z=toast 70, zIndex canonique réutilisé.
|
||||
- TOUS les panneaux atteignables via **View** (context, work, tickets, agents, templates, skills, perms, memory, git) + File→Projects…. Critère d'acceptation respecté.
|
||||
- Non touché : `shouldShowWritePortalVeil`, exclusion write-portal↔F3, overlays in-cell, TicketPicker (#18).
|
||||
|
||||
## Notes
|
||||
- ProfilesSettings rendu DANS le main de ProjectsView (barre + onglets projet restent visibles au-dessus) au lieu du swap plein-écran App précédent — meilleure continuité, barre unique toujours accessible.
|
||||
- Pas de run live Tauri (couverture RTL mock-driven). DoD vitest satisfaite.
|
||||
|
||||
QA/utilisateur revalident avant merge Git.
|
||||
9
.ideai/memory/ticket17-focus-trap-fix-merged-develop.md
Normal file
9
.ideai/memory/ticket17-focus-trap-fix-merged-develop.md
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
name: ticket17-focus-trap-fix-merged-develop
|
||||
description: Correctif de la perte de focus dans l'édition de ticket (#17) livré sur develop via feature branch dédiée.
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
Bug #17 : saisie une lettre à la fois dans le formulaire d'édition de ticket, cause = focus-trap `FloatingWindow` avec `useEffect` dépendant de `[onClose]` (closure recréée à chaque render). Fix DevFrontend : `onCloseRef` + effet focus-trap mount-only (`[]`).
|
||||
|
||||
Topologie Git : branche `feature/ticket17-focus-trap-fix` depuis `develop`, commit `4cccd40` (2 fichiers frontend seulement), merge `--no-ff` → `develop` = `2f8467e`, branche supprimée. Vert : tsc clean, 531/531 + test de régression. Voir [[ticket16-menus-floating-delivered-devfrontend]], [[ui-rework-sprint-scoping-contracts]].
|
||||
28
.ideai/memory/ticket18-picker-delivered-devfrontend.md
Normal file
28
.ideai/memory/ticket18-picker-delivered-devfrontend.md
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
name: ticket18-picker-delivered-devfrontend
|
||||
description: memory note ticket18-picker-delivered-devfrontend
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# Lot B / #18 — TicketPicker livré (DevFrontend)
|
||||
|
||||
Branche `feature/ticket-picker-popup` (base develop d97abb1). `vitest` **vert** : 53 fichiers / 515 tests, dont 8 nouveaux (`TicketPicker.test.tsx`) + 29 `tickets.test.tsx` inchangés (valide la non-régression du listing après extraction). `tsc --noEmit` clean.
|
||||
|
||||
## Fichiers
|
||||
- **Créés** : `shared/ui/zIndex.ts`, `features/tickets/TicketFacetsBar.tsx`, `features/tickets/useTicketSearch.ts`, `features/tickets/TicketPicker.tsx`, `features/tickets/TicketPicker.test.tsx`.
|
||||
- **Modifiés** : `shared/index.ts` (export zIndex), `features/tickets/index.ts` (barrel), `features/tickets/TicketsPanel.tsx` (consomme `TicketFacetsBar`).
|
||||
|
||||
## Écarts de contrat à trancher (signalés à Main)
|
||||
1. **Emplacement TicketPicker** : placé sous `features/tickets/`, PAS `shared/ui`, car il dépend du domaine ticket + gateways DI (shared/ui = design system pur). Conforme à tous les autres overlays (SprintManager, MemoryEditor…). Seul `zIndex.ts` (niveau design-system) est dans shared/ui.
|
||||
2. **`TicketPickerResult.id`/`number`** : `ticket_list`/`TicketSummary` ne portent NI `id` NI `number`. Résolus par un `ticket_read` unique **à la sélection** (`useTicketSearch.resolve`). `number` serait dérivable du ref mais `id` exige la lecture. → contrat `{ref,id,number,title,status,priority}` respecté sans nouveau endpoint. Écart backend candidat : ajouter id/number au summary (dette basse).
|
||||
3. **`zIndex.ts` provisoire** : lot A (#16) introduit le canonique en parallèle. Valeurs alignées sur l'échelle figée (menuDropdown=40, floatingWindow=50, floatingWindowNested=60, toast=70). **À réconcilier au merge** (garder un seul module). Picker monté à `floatingWindowNested=60`.
|
||||
|
||||
## Notes d'impl
|
||||
- G3-bis respecté : `cursor` traité en token opaque (jamais construit/incrémenté ; relayé verbatim via `loadMore`).
|
||||
- `excludeRefs` filtré client-side après réception de page.
|
||||
- `useTicketSearch` léger (pas de groupement sprint, pas de souscription events, debounce 200ms) — ne réutilise pas `useTickets`.
|
||||
- `TicketFacetsBar` = recherche + facettes statut/priorité (labels aria préservés : `search tickets`, `filter status …`, `filter priority …`, `clear filters`). Le select assignee reste dans `TicketsPanel` (hors périmètre picker).
|
||||
- Focus-trap inline (Tab cycling + Escape + restore focus) car aucun util partagé n'existe ; G5 respecté (montage niveau chrome par le consommateur #17/#19).
|
||||
- `selectionMode:"multi"` câblé de façon extensible (footer réservé) mais non actif en V1.
|
||||
|
||||
QA repasse derrière. Consommateurs #17 (link) et #19 (sprint) importent `TicketPicker` depuis `@/features/tickets`.
|
||||
11
.ideai/memory/ticket25-assistant-sandbox-eacces-rootcause.md
Normal file
11
.ideai/memory/ticket25-assistant-sandbox-eacces-rootcause.md
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
name: ticket25-assistant-sandbox-eacces-rootcause
|
||||
description: Root cause et contrat de fix du EACCES (os error 13) au spawn de la CLI d'un assistant de ticket — le preset SandboxPlan::project_read_only handle la classe exec Landlock.
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
**Symptôme** : ouvrir un assistant de ticket (Codex ou Claude), premier message ⇒ `process error: agent session start failed: <cli>: Permission non accordée (os error 13)`. Indépendant de la CLI.
|
||||
|
||||
**Root cause** : `OpenTicketAssistant::execute` (`application/src/ticket_assistant.rs:97`) fabrique un plan bespoke via `SandboxPlan::project_read_only` (`domain/src/sandbox.rs:153`) = grant **RO** sur `project_root` + RW sur le run_dir, posture `Deny`. Dans l'adapter Landlock (`infrastructure/src/sandbox/landlock.rs:70`), un grant RO *handle* la classe read, et `AccessFs::from_read(V1)` **inclut `AccessFs::Execute`**. Le binaire CLI vit hors du project root ⇒ `execve` refusé ⇒ EACCES au `spawn()` sandboxé (`process.rs:248`). Le chemin workspace n'a pas le bug car son plan vient de `compile_sandbox_plan` sous posture `Allow`/write-only ⇒ l'adapter no-op ou ne handle que write, read/exec restent ouverts. Piège déjà documenté dans l'adapter (« enrichment is the launch-path's concern LP4-2 »).
|
||||
|
||||
**Fix (backend-pur, zéro port/DTO)** : remplacer le preset par un **write-fence** — ne jamais handle read/exec (pas de grant RO), handle write seul, RW-grant = run_dir + state/home CLI + temp, posture Deny ⇒ écriture projet refusée, binaire/libs/home lisibles-exécutables. Fallback sûr : passer `None` à `factory.start` pour la surface ticket (garde policy MCP + projection advisory). Garde-fous fonctionnels réels = policy MCP `idea_ticket_read/update*` + LP3, pas l'OS-sandbox. Lié à [[ui-rework-sprint-scoping-contracts]].
|
||||
17
.ideai/memory/ticket28-f1-frontend-delivered.md
Normal file
17
.ideai/memory/ticket28-f1-frontend-delivered.md
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
name: ticket28-f1-frontend-delivered
|
||||
description: Lot F1 du ticket #28 livré sur feature/ticket28-firstrun-detect-hang — invariant busy/detectProfiles, flag detecting, timeout UI, test de non-régression.
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
Lot **F1** (frontend) du bug #28 implémenté sur `feature/ticket28-firstrun-detect-hang` (non committé, Git s'en charge).
|
||||
|
||||
**Invariant posé** : dans `frontend/src/features/first-run/useFirstRun.ts`, le retour de `busy` à `false` ne dépend d'aucun appel à `detectProfiles`. `busy` n'entoure plus que `firstRunState()` (reload) et `configureProfiles()` (finish). Toute future régression qui remet un `await profile.detectProfiles(...)` sur un chemin gouvernant `busy` re-gèle le wizard.
|
||||
|
||||
**Mécanisme** : `runDetection(candidates, { preselect, reportError })` factorise auto-détection et bouton manuel. Le flag `detecting` est relâché par un `setTimeout(DETECT_TIMEOUT_MS = 3_000)`, **jamais** par la promesse — car le `finally` d'un `await` sur une promesse jamais settled (panic Rust ⇒ pas de réponse IPC) n'est jamais atteint. Un compteur `detectRun` invalide les rounds périmés (re-clic, unmount).
|
||||
|
||||
**Piège UI à retenir** : `<Button loading>` implique `disabled` dans le design system (`shared/ui/Button.tsx:46`). Donc `detecting` ne peut pas être surfacé via `loading` sur les boutons Save/Detect sans réintroduire le grisage — il est rendu comme un `role="status"` « Detecting… » séparé.
|
||||
|
||||
**Point de vérité QA** : `FirstRunWizard.test.tsx`, describe « detection that never answers (ticket #28) », gateway `HangingDetectGateway` dont `detectProfiles` renvoie `new Promise(() => {})`. Vérifié à teeth : en réintroduisant `setBusy(true); await runDetection(...)` dans `reload`, 2 des 3 tests échouent avec exactement le symptôme du ticket.
|
||||
|
||||
Vert : `npx vitest run` 59 fichiers / 569 tests, `npx tsc --noEmit` exit 0. Pas de lint dans ce projet (aucun eslint config ni script). F1 débloque l'UI mais **B1 (DevBackend, `AgentRuntime::detect` sync→async) reste requis** pour que la détection remonte réellement un résultat.
|
||||
19
.ideai/memory/ticket28-firstrun-detect-hang-scoping.md
Normal file
19
.ideai/memory/ticket28-firstrun-detect-hang-scoping.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
name: ticket28-firstrun-detect-hang-scoping
|
||||
description: Cadrage hexagonal du bug bloquant first-run (#28) — panic block_on imbriqué dans detect + découplage busy/détection, contrats et point de vérité QA.
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
Bug high #28 (build 0.3.0 / develop 3c6cd04) : au first-run, « Save and continue » ET « Detect installed CLIs » restent grisés → wizard impossible à terminer.
|
||||
|
||||
**Cause racine unique (bloquante) — CONFIRMÉE source :** `AgentRuntime::detect` est resté `fn` synchrone (`domain/src/ports.rs:754`, impl `infrastructure/src/runtime/mod.rs:182`) alors que le trait est déjà `#[async_trait]`. L'impl bridge le spawner async via `futures_block_on`/`block_on` (mod.rs:228-237), appelé DANS la commande async Tauri `detect_profiles` → `DetectProfiles::execute` (async) → `detect()` sync sur la même task → tokio panic « Cannot start a runtime from within a runtime ». Un panic dans une commande async Tauri = **aucune réponse IPC** → promesse `invoke` jamais résolue/rejetée → le try/catch interne (`useFirstRun.ts:79-90`, n'attrape que les rejets) ne l'attrape pas → `finally` jamais atteint → `busy` figé true → boutons `disabled: vm.busy` grisés à vie. Se déclenche au 1er profil sondé, toute machine. Déclencheur nouveau de #14 : auto-détection à l'ouverture dans `reload`.
|
||||
|
||||
**Cause secondaire (NON bloquante) :** profil `ollama-openai-compatible` (command `openai-compatible`, `detect:None`) → fallback `openai-compatible --version` (binaire absent) → `available:false` erroné (spawn échoue vite, pas de hang). Endpoint HTTP sondé comme un CLI = catégorie-fausse. base_url dispo via `HttpChatConfig` (http://localhost:11434/v1). Précédent réutilisable : `infrastructure/src/store/embedder.rs:401 detect_ollama` (reqwest, timeout 800ms, feature vector-http).
|
||||
|
||||
**Fix figé — Backend (DevBackend) :** B1 promouvoir `detect` en `async fn` (supprimer futures_block_on entièrement), maj `DetectProfiles::execute` (.await) + commande. B2 `tokio::time::timeout` borné par probe. B3 brancher endpoint-kind (chat_http / StructuredAdapter::OpenAiCompatible) → probe HTTP best-effort borné de base_url (jamais d'erreur dure) ; CLI-kind = `detection_spec` INCHANGÉ (zéro-régression Claude/Codex). Aucun nouveau port/adapter.
|
||||
|
||||
**Fix figé — Frontend (DevFrontend) :** F1 découpler `busy` de la détection : `busy` n'entoure que `firstRunState()`, relâché dès rendu des lignes. Auto-détection = étape détachée non bloquante suivie par un flag distinct `detecting` (ne grise pas Save), avec timeout UI (~3s). Invariant : retour de busy à false ne dépend JAMAIS de detectProfiles.
|
||||
|
||||
**Contrats :** port `AgentRuntime::detect` sync→async (validé Architecture). DTO detect_profiles INCHANGÉ (available:bool). Catalogue ollama : detect reste None, détection décidée par nature endpoint.
|
||||
|
||||
**Point de vérité QA :** Backend = test `#[tokio::test]` multi-thread sur DetectProfiles::execute (échoue au panic sur code actuel, passe après B1) + endpoint sans spawn CLI + CLI Claude/Codex inchangé. Frontend = RTL avec mock detectProfiles jamais résolu → Save+Detect actifs dès firstRunState. Live = first-run machine sans Claude/Codex, Ollama coché, boutons actifs, Save termine (rebuild AppImage obligatoire). F1 seul débloque déjà ; B1 supprime la cause racine ; les deux requis pour vert.
|
||||
@ -0,0 +1,16 @@
|
||||
---
|
||||
name: ticket7-f2-delegated-limit-no-agentratelimited
|
||||
description: Écart backend ticket #7/F2 — le rendez-vous délégué n'émet pas AgentRateLimited ni n'arme la reprise pour la cible limitée.
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
|
||||
Ticket #7, volet inter-agent. Constat F2 (frontend DevFrontend) sur `feature/session-limit-interagent`.
|
||||
|
||||
Le chemin délégué (rendez-vous headless) **n'appelle jamais** `session_limit_service.on_rate_limited(target, …)`. Dans `crates/application/src/orchestrator/service.rs:1876-1915`, la branche `TargetRateLimited` publie **uniquement** `DomainEvent::DelegationRateLimited{requester, target, resets_at_ms}` puis retourne `AppError::TargetRateLimited`. Les deux seuls sites de `on_rate_limited` sont les chemins humains PTY/chat (`crates/app-tauri/src/commands.rs:1409` et `:1535`).
|
||||
|
||||
**Conséquence** : quand la cible B est limitée **via une délégation**, aucun `AgentRateLimited`/`AgentResumeScheduled` n'est émis pour B ⇒ le badge mono-agent de B (piloté par `limitByAgent` dans `useAgents`) ne s'affiche pas et aucune reprise auto n'est armée pour B. Contredit le commentaire de `crates/app-tauri/src/events.rs:370-372` (« the target resume flow is surfaced by the existing agent limit events »).
|
||||
|
||||
**Why:** F2 supposait « même flux AgentRateLimited ». Le rendu frontend du badge est sain (re-render + bouton Annuler OK) ; l'écart est backend.
|
||||
|
||||
**How to apply:** correction = backend (appeler `on_rate_limited` pour la cible dans la branche rendez-vous, avec node/conversation adéquats). Ne PAS dériver le badge de B depuis `delegationRateLimited` côté front : cet event ne porte ni sémantique de reprise armée ni node_id/conversation_id pour armer le réveil → inventerait un contrat. Le côté DEMANDEUR (A) est, lui, bien couvert par F1 (`suspendedDelegationsByRequester`). Voir [[session-limit-handling-design]].
|
||||
19
.ideai/memory/ui-rework-sprint-delivered-tickets-21-22-23.md
Normal file
19
.ideai/memory/ui-rework-sprint-delivered-tickets-21-22-23.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
name: ui-rework-sprint-delivered-tickets-21-22-23
|
||||
description: memory note ui-rework-sprint-delivered-tickets-21-22-23
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
Sprint « UI rework » (order 1) bouclé le 2026-07-07. Les 4 premiers tickets (#16/#17/#18/#19) étaient déjà mergés ; les 3 derniers viennent d'être produits et intégrés. Cadrage : [[ui-rework-sprint-tickets-21-22-23-scoping]].
|
||||
|
||||
## Livré (tous verts QA par exécution réelle, mergés --no-ff sur develop)
|
||||
- **#21** tri des tickets (B+F) — merge `37bf7d4`. Backend : `TicketListQuery.sort?:{field:"number"|"priority"|"status"|"title";direction:"asc"|"desc"}`, appliqué avant pagination, tie-break systématique par `number` croissant. Ordres sémantiques : priority low<medium<high<critical ; status open<inProgress<QA<closed ; title casse-insensible. `sort` absent = comportement historique. Frontend : contrôle « Trier par… » dans `TicketFacetsBar` (partagé TicketsPanel + TicketPicker), `sort` dans query+queryKey (reset pagination), curseur reste opaque (G3-bis).
|
||||
- **#22** docking de views (F pur) — merge `2323a71`. Nouvelle primitive `shared/ui/DockRegion` + modèle `features/projects/viewPlacement.ts` : `ViewPlacement = "closed"|"floating"|{dock:"left"|"right"}` par PanelId, invariant un-seul-slot. Ne réutilise NI FloatingWindow NI LayoutTree terminaux (monté chrome-level ProjectsView).
|
||||
- **#23** fenêtres système (B+F) — merge `47b1d64` (HEAD develop). Backend app-tauri : commands `open_view_window({panel,projectId})→{label,url,alreadyOpen}` et `close_view_window(...)→{label,closed}`, vraie WebviewWindow chargeant `index.html?panel=&project=`, registre anti-doublon (label `view-{panel}-{projectIdSansTirets}`), event unique `view-window://lifecycle` payload `{kind:"opened"|"focused"|"closed",panel,projectId,label}`, capability étendue à `view-*`. Frontend : port `WindowGateway` (adapter Tauri + mock), entrée panel-only `app/ViewWindow.tsx` routée dans `main.tsx`, `ViewPanelBody` partagé, `ViewPlacement` étendu à `"detached"`, menu Window + bouton ⤢, garde no-direct-invoke.
|
||||
|
||||
## Dette basse restée ouverte (hors périmètre, non bloquante)
|
||||
- Curseur `ticket_list` toujours offset non opaque/stable (cf. [[ui-rework-sprint-scoping-contracts]]) — non soldée par #21.
|
||||
- Persistance restore-au-redémarrage du docking #22 et du placement détaché #23 : follow-up backend différé (V1 = placement éphémère useState).
|
||||
|
||||
## État Git
|
||||
develop @ `47b1d64` ; branches feature/ticket21-22-23 supprimées (mergées). Rien de sortant, `main` intact. Release `develop→main` en attente de validation utilisateur.
|
||||
23
.ideai/memory/ui-rework-sprint-scoping-contracts.md
Normal file
23
.ideai/memory/ui-rework-sprint-scoping-contracts.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
name: ui-rework-sprint-scoping-contracts
|
||||
description: Frontières hexagonales et contrats figés du sprint UI rework — les 4 tickets sont frontend-purs, la popup TicketPicker #18, l'échelle z-index et le curseur opaque.
|
||||
metadata:
|
||||
type: reference
|
||||
---
|
||||
Sprint « UI rework » (order 1), cadré 2026-07-06. Les **4 tickets sont frontend-purs** : aucun nouveau port/read-model/endpoint.
|
||||
|
||||
**Fait clé** : filtrage/recherche tickets déjà backend-driven via `TicketGateway.list(projectId, TicketListQuery{statuses[],priorities[],assignedAgentId,text,limit,cursor})` (ports/index.ts:717) → adapter transmet à `ticket_list`. Client ne fait que le groupement par sprint. #18 réutilise ce contrat, pas d'endpoint dédié.
|
||||
|
||||
**G4 (contrat ticket_list) levé pour B** après revue DevBackend : statuses/priorities OK (OR intra / AND inter). Deux écarts en DETTE BASSE : (a) `text` ne matche pas `#ref` (tickets.rs:681, issues.rs:322/326/465) ; (b) `cursor` = offset numérique non opaque/non stable inter-pages (tickets.rs:1231). Acceptable pour picker single-select court.
|
||||
|
||||
**G3-bis (garde non-breaking)** : TicketPicker/useTicketSearch traitent `cursor` comme token OPAQUE — ne jamais le construire/incrémenter, ne relayer que le cursor de la page précédente. Migration curseur opaque backend = zéro changement frontend.
|
||||
|
||||
**Base de code** : pas de routeur ni store global (navigation = useState local App.tsx + ProjectsView.tsx, sidebar inline ProjectsView.tsx:224). Aucune primitive Modal/Portal partagée ; ~7 features hand-roll `fixed inset-0 role=dialog` z-50/z-[60]. Pas de createPortal. #16 introduit `shared/ui/FloatingWindow` (focus-trap, monté niveau chrome) + `shared/ui/zIndex.ts`.
|
||||
|
||||
**Contrat #18 figé** — `TicketPicker` props `{open, projectId, title?, confirmLabel?, selectionMode?:"single"|"multi" (défaut single), excludeRefs?: TicketRef[] (filtré client-side), initialQuery?: TicketListQuery, onSelect(TicketPickerResult|[]), onClose}`. `TicketPickerResult={ref,id,number,title,status,priority}`. Extraire `TicketFacetsBar` (fieldsets statut/priorité + recherche depuis TicketsPanel.tsx) + hook `useTicketSearch` (ne PAS réutiliser useTickets, trop lourd). #17 et #19 consomment en single.
|
||||
|
||||
**Échelle z-index figée (G2)** : in-cell inchangé (chrome 2-5, F3 z-20, write-portal z-4, resume z-5) ; chrome full-window menuDropdown=40, floatingWindow=50, floatingWindowNested=60, toast=70.
|
||||
|
||||
**Règle intégration (G5)** : fenêtres flottantes/menus montées niveau chrome (ProjectsView/App), JAMAIS dans LayoutGrid/LeafView. Ne pas toucher shouldShowWritePortalVeil ni exclusion write-portal↔F3 (cf. [[ticket4-overlay-composition-leafview]]). Focus-trap obligatoire (xterm garde le focus clavier).
|
||||
|
||||
**Ordre** : A(#16) ∥ B(#18) racines → prioriser B (chemin critique #17+#19) ; D(#19) après B ; C(#17) après A+B. Tout DevFrontend ; DevBackend = ticket dette basse (matching #ref + curseur opaque/stable).
|
||||
39
.ideai/memory/ui-rework-sprint-tickets-21-22-23-scoping.md
Normal file
39
.ideai/memory/ui-rework-sprint-tickets-21-22-23-scoping.md
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
name: ui-rework-sprint-tickets-21-22-23-scoping
|
||||
description: memory note ui-rework-sprint-tickets-21-22-23-scoping
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
Cadrage hexagonal des 3 derniers tickets du sprint « UI rework » (order 1), fait 2026-07-07 par Architect. Les 4 premiers (#16/#17/#18/#19) sont livrés/mergés sur develop — cf. [[ui-rework-sprint-scoping-contracts]]. Ne pas re-cadrer ; produire.
|
||||
|
||||
## État de départ vérifié
|
||||
- Vues (context/work/tickets/agents/templates/skills/permissions/memory/git) = composants React rendus via menu **View**, pilotés par `openPanel: useState<PanelId|null>` (ProjectsView.tsx:127). UNE seule vue à la fois dans un `FloatingWindow` modal centré (ProjectsView.tsx:497).
|
||||
- `FloatingWindow` = overlay MODAL (fixed inset-0, backdrop, focus-trap, z-50), non déplaçable/non redimensionnable, purement présentationnel.
|
||||
- LayoutTree terminaux (cellules/LeafView, layout.json via ProjectStore) = domaine DISTINCT ; ne pas confondre avec le placement des vues.
|
||||
- `TicketListQuery` (ports/index.ts:717) N'A PAS de sort/order. Pagination RÉELLE (useTicketSearch + loadMore, TicketsPanel.tsx:225 charge 100 puis +100).
|
||||
|
||||
## #21 tri tickets (low) — NON frontend-pur : B + F
|
||||
Le tri est une préoccupation de QUERY, symétrique du filtrage déjà backend-driven (TicketGateway.list→ticket_list). Comme la liste est paginée, trier client ne trie que les pages chargées → FAUX. Refuser le « frontend-pur ».
|
||||
- **B** : étendre `TicketListQuery`/`ticket_list` avec `sort?:{field:"number"|"priority"|"status"|"title"; direction:"asc"|"desc"}`. ORDRE TOTAL déterministe obligatoire (tie-break par number croissant, sinon curseur casse). priority/status = ordre sémantique enum explicite, pas alpha. Défaut absent = comportement actuel (champ optionnel, rétro-compat). Companion recommandé non imposé : solder la dette basse curseur offset non opaque/stable (cf. [[ui-rework-sprint-scoping-contracts]]) — peut rester ticket séparé.
|
||||
- **F** : contrôle « Trier par… » dans `TicketFacetsBar` (partagé TicketsPanel + TicketPicker #18). `useTicketSearch` relaie `sort` dans la query, `sort` dans la queryKey (reset pagination). Ne pas toucher le traitement OPAQUE du curseur (G3-bis). Groupement-par-sprint reste client ; tri s'applique intra-groupe.
|
||||
- Dépend #16/#18 (livrés). Indépendant de #22/#23.
|
||||
|
||||
## #22 Anchor/docking views (medium) — frontend-pur V1 ; persistance = B optionnel différé
|
||||
NE PAS réutiliser FloatingWindow (overlay modal) ni LayoutTree terminaux. NOUVELLE primitive de docking chrome-level, en-flux (pas overlay).
|
||||
- Frontière : docking = état présentation chrome-level machine-local, distinct du domaine LayoutTree (réservé cellules PTY). Jamais dans LayoutGrid/LeafView (G5).
|
||||
- Généraliser `openPanel:PanelId|null` en modèle de placement : `ViewPlacement = "closed"|"floating"|{dock:"left"|"right"}` par PanelId — conçu EXTENSIBLE pour #23 (ajoutera "detached"). Invariant : une vue = exactement un slot. V1 reco : 1 vue ancrée par côté, largeur redimensionnable.
|
||||
- **F** : `shared/ui/DockRegion` (colonne verticale resizable gauche/droite du <main>), header titre + bascule flottant↔ancré. ProjectsView remplace openPanel par le placement. Réutilise composants de vue existants tels quels.
|
||||
- **B optionnel différé** : persistance restore-au-redémarrage = préférence UI machine-local → store global settings.json/workspace.json (archi §9.2 via ProjectStore), PAS layout.json projet. V1 éphémère (useState) ; persistance = follow-up ticket dédié.
|
||||
- Autonome. FOURNIT le modèle de placement dont #23 dépend.
|
||||
|
||||
## #23 views en fenêtres système (medium) — impact backend Rust + composition root
|
||||
Cas spécialisé du chantier L10 multi-fenêtre (Tauri v2 WebviewWindow, protocole detach, archi §13.3). Avantage vs detach onglet-terminal : vues gateway/read-model-driven, PAS liées à un Channel PTY → nouvelle WebviewWindow ré-instancie le DI et re-souscrit aux gateways, AUCUN transfert d'état lourd.
|
||||
- **B (app-tauri)** : command `open_view_window({panel,projectId})` créant une WebviewWindow sur entrée dédiée (URL param ?panel=&project=), lifecycle (focus/close/registre anti-doublon), events cycle de vie (fermeture OS → re-bascule placement). Composition root (di.tsx) : supporter un shell « panneau seul » (mêmes adapters/gateways).
|
||||
- **F** : nouveau port `WindowGateway.openViewWindow(panel,projectId)` (+ closeViewWindow) avec adapter Tauri + mock (jamais invoke direct). Entrée panel-only lisant panel/project depuis URL. Menu View : action « Ouvrir dans une nouvelle fenêtre ».
|
||||
- Articulation #22 FIGÉE : `ViewPlacement = "closed"|"floating"|{dock:"left"|"right"}|"detached"`. Une vue = soit ancrée soit flottante soit détachée, jamais deux (même invariant un-seul-slot). #23 = ajoute "detached" + plomberie Tauri.
|
||||
- DÉPEND du modèle de placement de #22.
|
||||
|
||||
## Ordre & dépendances
|
||||
- #21 en parallèle, autonome (petit B+F, le plus rapide, chaîne tickets uniquement).
|
||||
- #22 AVANT #23 : #22 pose ViewPlacement que #23 étend. #23 d'abord = réinventer puis retravailler.
|
||||
- Frontières : #21 seul à toucher domaine tickets (query sort) ; #22 présentation pure (docking ≠ LayoutTree) ; #23 seul à toucher app-tauri/composition root + nouveau port WindowGateway.
|
||||
Reference in New Issue
Block a user