From 5505acc1f678e0c25cfc9c52deb5a603de02873d Mon Sep 17 00:00:00 2001 From: Blomios Date: Wed, 15 Jul 2026 09:50:10 +0200 Subject: [PATCH] docs(ticket13): inventaire transport backend et frontend (B0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lot B0 du chantier server/client mode : cartographie des surfaces de transport existantes, préalable à l'extraction du cœur backend commun. - docs/ticket13-b0-backend-transport-inventory.md : inventaire backend. - docs/ticket13-f0-frontend-transport-inventory.md : inventaire frontend. Co-Authored-By: Claude Opus 4.8 --- ...ticket13-b0-backend-transport-inventory.md | 708 ++++++++++++++++++ ...icket13-f0-frontend-transport-inventory.md | 158 ++++ 2 files changed, 866 insertions(+) create mode 100644 docs/ticket13-b0-backend-transport-inventory.md create mode 100644 docs/ticket13-f0-frontend-transport-inventory.md diff --git a/docs/ticket13-b0-backend-transport-inventory.md b/docs/ticket13-b0-backend-transport-inventory.md new file mode 100644 index 0000000..a8c66d4 --- /dev/null +++ b/docs/ticket13-b0-backend-transport-inventory.md @@ -0,0 +1,708 @@ +# Ticket #13 B0 - Backend transport inventory + +Date: 2026-07-15 +Branch: `feature/ticket13-server-client-mode` + +Scope: read-only inventory of the current Tauri backend transport surface in +`crates/app-tauri`. No production code change is part of B0. + +Architect ticket #13 target: keep desktop Tauri and add server/client mode on a +shared backend core. Future web transport is HTTP request/response plus a +dedicated WebSocket for PTY/live streams. + +## Source anchors + +- Tauri command registration: `crates/app-tauri/src/lib.rs:153` +- Composition root and `AppState`: `crates/app-tauri/src/state.rs:738`, + `crates/app-tauri/src/state.rs:1058` +- PTY Tauri Channel bridge: `crates/app-tauri/src/pty.rs:1` +- Structured chat Tauri Channel bridge: `crates/app-tauri/src/chat.rs:1` +- Domain event relay: `crates/app-tauri/src/events.rs:23`, + `crates/app-tauri/src/events.rs:1107` +- Desktop window commands/events: `crates/app-tauri/src/commands.rs:2325` +- Shutdown/window lifecycle wiring: `crates/app-tauri/src/lib.rs:80` + +## Classification rules + +- `request/response`: one Tauri `invoke` maps to one backend result. HTTP + candidate. +- `stream`: command uses `tauri::ipc::Channel` or controls a live stream/session. + WebSocket candidate, sometimes with a small HTTP control equivalent. +- `event`: backend push currently emitted through Tauri global events. WebSocket + live/event frame candidate unless desktop-specific. +- `desktop-only`: OS windows, Tauri path/lifecycle/dialog composition. Not part + of the web client transport. + +## Exposed Tauri commands + +### Core/projects/permissions + +| Command | Class | Current boundary | +| --- | --- | --- | +| `health` | request/response | `State.health` | +| `create_project` | request/response | `create_project` use case | +| `open_project` | request/response | `open_project`, plus open-time reconciliation/wiring | +| `close_project` | request/response | `close_project` | +| `list_projects` | request/response | `list_projects` | +| `read_project_context` | request/response | project FS read use case | +| `update_project_context` | request/response | project FS write use case | +| `get_project_permissions` | request/response | permission read use case | +| `update_project_permissions` | request/response | permission write use case | +| `update_agent_permissions` | request/response | permission override write use case | +| `resolve_agent_permissions` | request/response | effective permission read use case | + +### PTY terminals + +| Command | Class | Current boundary | +| --- | --- | --- | +| `open_terminal` | stream | Spawns PTY, registers `Channel`, starts output pump | +| `write_terminal` | stream/control | Writes bytes to live PTY | +| `resize_terminal` | stream/control | Resizes live PTY | +| `close_terminal` | stream/control | Kills PTY and unregisters channel | +| `reattach_terminal` | stream | Reads scrollback, replaces `Channel`, starts new pump | + +Notes: + +- `PtyChunk = Vec` today (`crates/app-tauri/src/pty.rs:29`). +- The transport-specific registry is `PtyBridge`, stored in `AppState`. +- High-frequency PTY bytes are explicitly excluded from global Tauri events. + +### Layouts + +| Command | Class | Current boundary | +| --- | --- | --- | +| `load_layout` | request/response | layout read use case | +| `mutate_layout` | request/response | layout mutation use case, emits `LayoutChanged` | +| `list_layouts` | request/response | named layout list | +| `create_layout` | request/response | named layout create | +| `rename_layout` | request/response | named layout rename | +| `delete_layout` | request/response | named layout delete | +| `set_active_layout` | request/response | active layout write | + +### First-run, profiles, local models, embedders + +| Command | Class | Current boundary | +| --- | --- | --- | +| `first_run_state` | request/response | first-run read use case | +| `reference_profiles` | request/response | reference profile catalog | +| `detect_profiles` | request/response | runtime detection use case | +| `list_profiles` | request/response | profile store read | +| `save_profile` | request/response | profile upsert | +| `clone_opencode_profile_from_seed` | request/response | profile clone | +| `delete_profile` | request/response | profile delete | +| `configure_profiles` | request/response | first-run save | +| `list_model_servers` | request/response | local model server config read | +| `save_model_server` | request/response | local model server config write | +| `preview_model_server_command` | request/response | pure command preview | +| `delete_model_server` | request/response | local model server config delete | +| `list_embedder_profiles` | request/response | embedder profile read | +| `save_embedder_profile` | request/response | embedder profile write | +| `delete_embedder_profile` | request/response | embedder profile delete | +| `describe_embedder_engines` | request/response | embedder engine capabilities | +| `dismiss_embedder_suggestion` | request/response | suggestion state write | + +### Agents, live state, chat and resume + +| Command | Class | Current boundary | +| --- | --- | --- | +| `create_agent` | request/response | agent manifest/context creation | +| `list_agents` | request/response | manifest read | +| `get_project_work_state` | request/response | read-model aggregation | +| `read_conversation_page` | request/response | paginated transcript read | +| `list_live_agents` | request/response | in-memory live registry read | +| `attach_live_agent` | request/response | live session rebind use case | +| `stop_live_agent` | request/response | live session stop use case | +| `read_agent_context` | request/response | agent Markdown read | +| `update_agent_context` | request/response | agent Markdown write | +| `delete_agent` | request/response | manifest/context delete | +| `inspect_conversation` | request/response | best-effort conversation inspection | +| `launch_agent` | stream | Spawns/reuses PTY or structured session, may register `Channel` | +| `change_agent_profile` | request/response | hot-swap use case; may affect live session | +| `agent_send` | stream | Structured chat turn, registers `Channel`, pumps reply stream | +| `cancel_resume` | request/response | cancels scheduled auto-resume | +| `set_resume_at` | request/response | human-set resume time | +| `interrupt_agent` | request/response | orchestrator interrupt | +| `delegation_delivered` | request/response | frontend write-portal ack | +| `set_front_attached` | request/response | frontend mounted/unmounted signal | +| `reattach_agent_chat` | stream | Replaces `Channel`, returns chat scrollback | +| `close_agent_session` | request/response | closes live agent/structured state | +| `list_resumable_agents` | request/response | resumable agent inventory | + +Structured chat stream: + +- `ReplyChunk` is tagged by `kind`: `textDelta`, `toolActivity`, `final`, + `error` (`crates/app-tauri/src/dto.rs:1760`). +- `ChatBridge` retains bounded scrollback and the current optional channel. +- `agent_send` is a turn command plus streaming response; in HTTP/WS terms it + should become a WS client frame that starts a turn on an attached session. + +### Templates + +| Command | Class | Current boundary | +| --- | --- | --- | +| `create_template` | request/response | template create | +| `update_template` | request/response | template update, emits drift/update events | +| `list_templates` | request/response | template list | +| `delete_template` | request/response | template delete | +| `create_agent_from_template` | request/response | agent creation from template | +| `detect_agent_drift` | request/response | drift inventory | +| `sync_agent_with_template` | request/response | sync write | + +### Git + +| Command | Class | Current boundary | +| --- | --- | --- | +| `git_status` | request/response | git status use case | +| `git_stage` | request/response | git stage use case | +| `git_unstage` | request/response | git unstage use case | +| `git_commit` | request/response | git commit use case | +| `git_branches` | request/response | branch list | +| `git_checkout` | request/response | branch checkout | +| `git_log` | request/response | log read | +| `git_init` | request/response | repo init | +| `git_graph` | request/response | graph read | + +### Tickets and sprints + +| Command | Class | Current boundary | +| --- | --- | --- | +| `ticket_create` | request/response | ticket create | +| `ticket_read` | request/response | ticket read | +| `ticket_delete` | request/response | ticket delete | +| `open_ticket_chat` | request/response | ephemeral ticket assistant open | +| `close_ticket_chat` | request/response | ephemeral ticket assistant close | +| `ticket_list` | request/response | ticket list | +| `ticket_update` | request/response | ticket update | +| `ticket_read_carnet` | request/response | carnet read | +| `ticket_update_carnet` | request/response | carnet write | +| `ticket_link` | request/response | link tickets | +| `ticket_unlink` | request/response | unlink tickets | +| `ticket_assign` | request/response | assign/unassign ticket agent | +| `sprint_create` | request/response | sprint create | +| `sprint_list` | request/response | sprint list | +| `sprint_rename` | request/response | sprint rename | +| `sprint_reorder` | request/response | sprint reorder | +| `sprint_delete` | request/response | sprint delete | +| `ticket_assign_sprint` | request/response | ticket sprint assignment | +| `ticket_unassign_sprint` | request/response | ticket sprint removal | + +### Skills and memory + +| Command | Class | Current boundary | +| --- | --- | --- | +| `create_skill` | request/response | skill create | +| `update_skill` | request/response | skill update | +| `list_skills` | request/response | skill list | +| `delete_skill` | request/response | skill delete | +| `assign_skill_to_agent` | request/response | skill assignment | +| `unassign_skill_from_agent` | request/response | skill unassignment | +| `create_memory` | request/response | memory create | +| `update_memory` | request/response | memory update | +| `list_memories` | request/response | memory list | +| `get_memory` | request/response | memory read | +| `delete_memory` | request/response | memory delete | +| `read_memory_index` | request/response | memory index read | +| `recall_memory` | request/response | memory recall | +| `resolve_memory_links` | request/response | memory link resolution | + +### Background tasks + +| Command | Class | Current boundary | +| --- | --- | --- | +| `spawn_background_command` | request/response | creates command-backed background task | +| `cancel_background_task` | request/response | cancels task | +| `retry_background_task` | request/response | retries task | +| `list_background_tasks` | request/response | task read-model list | + +Lifecycle is pushed through `BackgroundTaskChanged` domain events; initial list is +request/response. + +### Desktop windows and focus + +| Command | Class | Current boundary | +| --- | --- | --- | +| `set_focused_project` | desktop-only + event | Tauri window/panel focus state; emits `focused-project://changed` | +| `get_focused_project` | desktop-only | Tauri panel state read | +| `list_open_view_windows` | desktop-only | `AppHandle.webview_windows()` | +| `open_view_window` | desktop-only | `WebviewWindowBuilder` | +| `close_view_window` | desktop-only | closes Tauri OS window | +| `move_tab_to_new_window` | desktop-only | use case plus `WebviewWindowBuilder` | + +These do not migrate as-is to web. The shared backend may still keep workspace +topology use cases, but the OS-window adapter remains desktop-only. + +## Tauri Channel usage + +| Location | Channel type | Producer | Consumer | Web mapping | +| --- | --- | --- | --- | --- | +| `open_terminal` | `Channel` | PTY output pump | xterm view | WS `output` frames after `open_terminal` | +| `reattach_terminal` | `Channel` | PTY output pump | xterm view | WS `attached` + `output` frames after `attach_terminal` | +| `launch_agent` | `Channel` | PTY output pump for raw CLI agents | xterm/chat terminal view | WS `output` frames; structured sessions do not use this channel | +| `agent_send` | `Channel` | structured reply pump | chat view | WS `chat.output` or generic live frames | +| `reattach_agent_chat` | `Channel` | structured reply pump | chat view | WS attached/replay for structured session | + +Transport-owned registries: + +- `PtyBridge`: session id -> `(generation, Channel)`. +- `ChatBridge`: session id -> `{generation, Option>, scrollback}`. + +These should not descend into domain/application. Their backend-core equivalent +should be a transport-neutral outbound sink/subscription registry. + +## Events emitted today + +### Global domain event relay + +`events::spawn_relay` subscribes to `TokioBroadcastEventBus` and emits: + +- Tauri event name `domain://event` +- Payload `DomainEventDto`, tagged by `type` +- Skips `DomainEvent::PtyOutput`; PTY bytes use per-session channels + +Current `DomainEventDto` variants: + +- Project/lifecycle: `projectCreated`, `layoutChanged`, `remoteConnected` +- Agents: `agentLaunched`, `agentLaunchFailed`, `agentExited`, + `agentBusyChanged`, `agentProfileChanged`, `agentLivenessChanged`, + `agentRateLimited`, `agentResumeScheduled`, `agentResumeCancelled`, + `agentResumed`, `agentRateLimitSuspected` +- Delegation/orchestration: `delegationReady`, `agentReplied`, + `agentAnnouncement`, `orchestratorRequestProcessed`, `orchestratorChanged` +- Background work: `backgroundTaskChanged`, `agentInboxChanged`, + `agentWakeChanged` +- Templates/skills: `templateUpdated`, `agentDriftDetected`, `agentSynced`, + `skillAssigned` +- Git: `gitStateChanged` +- Tickets/sprints: `issueCreated`, `issueUpdated`, `issueDeleted`, + `issueStatusChanged`, `issuePriorityChanged`, `issueCarnetUpdated`, + `issueLinked`, `issueUnlinked`, `issueAgentAssigned`, + `issueAgentUnassigned`, `sprintCreated`, `sprintRenamed`, + `sprintReordered`, `sprintDeleted`, `issueSprintChanged` +- Memory/embedder: `memorySaved`, `memoryDeleted`, `memoryIndexRebuilt`, + `embedderSuggested` +- Model server: `modelServerStatusChanged` +- PTY placeholder: `ptyOutput` exists in DTO but is not emitted by the global + relay + +### Dedicated global events + +- `model_server_status_changed`: emitted in addition to `domain://event` for + `ModelServerStatusChanged`. +- `agent_launch_failed`: emitted in addition to `domain://event` for + `AgentLaunchFailed`. +- `view-window://lifecycle`: desktop-only, payload `{kind, panel, label}`. +- `focused-project://changed`: desktop-only, payload `{project}`. + +For web, the domain-event set maps naturally to a low-frequency live WS stream. +The duplicate dedicated events should be normalized in the shared core contract +or kept as compatibility aliases only in the Tauri adapter. + +## `tauri::State` composition root inventory + +`AppState::build(app_data_dir)` is the current composition root. It constructs +concrete adapters and use cases, then Tauri stores it through `app.manage`. + +### Concrete adapters and infra state currently built there + +- `TokioBroadcastEventBus` +- `SystemClock` +- `UuidGenerator` +- `LocalFileSystem` +- `FsProjectStore` +- `FsWindowStateStore` +- `PortablePtyAdapter` with sandbox enforcer +- `TerminalSessions` +- `StructuredSessions` +- `StructuredSessionFactory` +- `LocalProcessSpawner` +- `CliAgentRuntime` +- `FsProfileStore` +- local model server manager/use cases +- issue/ticket stores and providers +- `PtyBridge` and `ChatBridge` transport bridges +- profile/template/git/skill/memory/embedder stores and use cases +- orchestrator service, watchers and per-project MCP servers +- session-limit service, resume contexts and turn watcher +- background task runner/store +- focused project mutex +- home/app-data derived paths + +### Boundary that must be extracted + +Current shape: + +```text +React -> @tauri invoke/listen/Channel + -> app-tauri commands/events/bridges + -> AppState concrete composition root + -> application use cases + infrastructure adapters +``` + +Target shape: + +```text +React -> TS gateways + -> Tauri adapter OR HTTP+WS adapter + -> driving adapter calls shared backend core facade + -> application use cases + infrastructure adapters +``` + +The extraction line is inside `app-tauri::state`: + +- Move reusable construction into a shared backend core crate/facade, e.g. + `BackendCore::build(config)`. +- Keep Tauri-only concerns in `app-tauri`: resolving `app_data_dir` via Tauri + path API, `app.manage`, command registration, `AppHandle`/`WebviewWindow`, + shutdown hooks, Tauri event emission, Tauri `Channel` bridges, dialog plugin. +- The future web server adapter should own HTTP route registration, TLS/pairing + integration, WS connection lifecycle, CORS/origin checks and WS sink mapping. +- The core should expose transport-neutral methods for the request/response + use cases and transport-neutral subscription/sink APIs for PTY/chat/live + output. + +Do not move `PtyBridge` or `ChatBridge` as-is. They are Tauri transport +adapters. Move the underlying concept: attach/detach a sink to a live +session, replay bounded scrollback, and deliver ordered output. + +## HTTP DTO first draft + +Naming below keeps current DTO semantics and camelCase fields. Exact paths are a +proposal for B1/B3; they can be namespaced by project where useful. + +### Request/response examples + +```http +GET /api/health +POST /api/projects +GET /api/projects +POST /api/projects/{projectId}/open +POST /api/projects/{projectId}/close +GET /api/projects/{projectId}/context +PUT /api/projects/{projectId}/context +GET /api/projects/{projectId}/layout +POST /api/projects/{projectId}/layout/mutate +GET /api/projects/{projectId}/agents +POST /api/projects/{projectId}/agents +GET /api/projects/{projectId}/agents/{agentId}/context +PUT /api/projects/{projectId}/agents/{agentId}/context +DELETE /api/projects/{projectId}/agents/{agentId} +GET /api/projects/{projectId}/work-state +GET /api/projects/{projectId}/conversation-page +GET /api/projects/{projectId}/git/status +POST /api/projects/{projectId}/git/stage +POST /api/projects/{projectId}/git/commit +GET /api/tickets +POST /api/tickets +GET /api/tickets/{ref} +PATCH /api/tickets/{ref} +GET /api/templates +POST /api/templates +GET /api/background-tasks +POST /api/background-tasks +``` + +Errors should preserve `ErrorDto`: + +```json +{ + "code": "NOT_FOUND", + "message": "project ..." +} +``` + +## WebSocket first draft + +One dedicated WS endpoint is enough for B5 V1: + +```text +GET /ws/live?token=... (token normally via Authorization/cookie, not URL in prod) +``` + +Authentication note from ticket #13: pairing/token must travel only over HTTPS +and not as a URL secret. The query above is only a sketch; production should use +`Authorization: Bearer` during WS upgrade or an HTTPS-only secure cookie. + +### Common envelope + +Client to server: + +```json +{ + "id": "client-msg-uuid", + "kind": "terminal.input", + "payload": {} +} +``` + +Server to client: + +```json +{ + "id": "server-msg-uuid", + "replyTo": "client-msg-uuid", + "kind": "terminal.status", + "payload": {} +} +``` + +`replyTo` is present for command acknowledgements, absent for unsolicited live +events/output. + +### PTY client -> server frames + +`attach_terminal` + +```json +{ + "id": "1", + "kind": "terminal.attach", + "payload": { + "sessionId": "uuid", + "lastSeq": 42 + } +} +``` + +`open_terminal` + +```json +{ + "id": "2", + "kind": "terminal.open", + "payload": { + "projectId": "uuid", + "nodeId": "uuid-or-null", + "cwd": "/abs/path", + "rows": 30, + "cols": 120 + } +} +``` + +`launch_agent` + +```json +{ + "id": "3", + "kind": "agent.launch", + "payload": { + "projectId": "uuid", + "agentId": "uuid", + "nodeId": "uuid-or-null", + "rows": 30, + "cols": 120, + "conversationId": "engine-conversation-id-or-null" + } +} +``` + +`input` + +```json +{ + "id": "4", + "kind": "terminal.input", + "payload": { + "sessionId": "uuid", + "bytesBase64": "DQ==" + } +} +``` + +`resize` + +```json +{ + "id": "5", + "kind": "terminal.resize", + "payload": { + "sessionId": "uuid", + "rows": 40, + "cols": 140 + } +} +``` + +`detach` + +```json +{ + "id": "6", + "kind": "terminal.detach", + "payload": { + "sessionId": "uuid" + } +} +``` + +`close` + +```json +{ + "id": "7", + "kind": "terminal.close", + "payload": { + "sessionId": "uuid" + } +} +``` + +`ping` + +```json +{ + "id": "8", + "kind": "ping", + "payload": { + "atMs": 1784123456789 + } +} +``` + +### PTY server -> client frames + +`attached` + +```json +{ + "kind": "terminal.attached", + "replyTo": "1", + "payload": { + "session": { "sessionId": "uuid", "projectId": "uuid", "nodeId": "uuid", "rows": 30, "cols": 120 }, + "nextSeq": 43, + "scrollback": [ + { "seq": 1, "bytesBase64": "..." } + ], + "gap": false + } +} +``` + +`output` + +```json +{ + "kind": "terminal.output", + "payload": { + "sessionId": "uuid", + "seq": 43, + "bytesBase64": "..." + } +} +``` + +`status` + +```json +{ + "kind": "terminal.status", + "payload": { + "sessionId": "uuid", + "status": "running" + } +} +``` + +Suggested status values: `starting`, `running`, `exited`, `closed`, +`detached`, `reattached`. + +`error` + +```json +{ + "kind": "error", + "replyTo": "7", + "payload": { + "code": "NOT_FOUND", + "message": "terminal session ..." + } +} +``` + +`pong` + +```json +{ + "kind": "pong", + "replyTo": "8", + "payload": { + "atMs": 1784123456789 + } +} +``` + +### Structured chat frames + +The ticket #13 PTY contract does not require structured chat in B5, but the +current backend already has a second stream. To avoid a later incompatibility, +reserve a parallel namespace: + +Client: + +```json +{ "id": "9", "kind": "chat.attach", "payload": { "sessionId": "uuid" } } +{ "id": "10", "kind": "chat.send", "payload": { "sessionId": "uuid", "prompt": "..." } } +{ "id": "11", "kind": "chat.detach", "payload": { "sessionId": "uuid" } } +``` + +Server: + +```json +{ + "kind": "chat.attached", + "replyTo": "9", + "payload": { + "sessionId": "uuid", + "scrollback": [ + { "kind": "textDelta", "text": "..." } + ] + } +} +``` + +```json +{ + "kind": "chat.output", + "payload": { + "sessionId": "uuid", + "chunk": { "kind": "final", "content": "..." } + } +} +``` + +### Low-frequency live events + +Domain events can be pushed on the same WS connection: + +```json +{ + "kind": "event.domain", + "payload": { + "type": "agentBusyChanged", + "agentId": "uuid", + "busy": true + } +} +``` + +This replaces Tauri `listen("domain://event")` for web. Desktop can keep the +existing event relay. + +## Plan impact / risks found + +No finding invalidates the ticket #13 lot plan. + +Points to decide early in B1/B2: + +- The duplicate Tauri registration of `detect_agent_drift` in + `generate_handler!` is harmless for B0 but should be cleaned in a separate fix. +- `open_project` currently also triggers open-time reconciliation and per-project + runtime side effects. The shared backend core facade should make those explicit + methods, not hide them in transport commands. +- `launch_agent` has both raw PTY and structured-session branches. B5 PTY V1 can + implement raw PTY first, but the WS namespace should reserve chat/live frames. +- `PtyBridge` and `ChatBridge` contain transport scrollback/replay behavior. + Extract the behavior as a core subscription contract; do not leak Tauri + `Channel` into the future core. +- Desktop window/focus commands are cleanly separable as desktop-only. Workspace + topology can remain shared, OS-window creation cannot. diff --git a/docs/ticket13-f0-frontend-transport-inventory.md b/docs/ticket13-f0-frontend-transport-inventory.md new file mode 100644 index 0000000..52413f8 --- /dev/null +++ b/docs/ticket13-f0-frontend-transport-inventory.md @@ -0,0 +1,158 @@ +# Ticket #13 — Lot B0/F0 : inventaire du transport frontend (TS/React) + +> Livrable d'inventaire. **Aucun code de feature modifié.** Miroir frontend de +> l'inventaire backend (`ticket13-b0-backend-transport-inventory.md`). +> Objectif : cartographier tous les accès `@tauri-apps/api`, les classer par nature +> (request/response · stream · event · desktop-only), et localiser la frontière +> « gateways TS transport-neutres » du lot F1 (adapter Tauri desktop | adapter +> HTTP+WS web). + +## 0. Constat majeur — la frontière F1 existe déjà (à ~90 %) + +Le frontend est **déjà** structuré exactement comme le plan cible le demande : + +- **Ports transport-neutres** : `src/ports/index.ts` déclare 21 gateways (interfaces + TS) décrivant *ce dont l'UI a besoin*, sans aucune référence à Tauri. Toute la + couche `features/` + `app/` dépend de ces ports via DI (`useGateways()`). +- **Adapters = seul lieu Tauri** : `src/adapters/*` implémente les ports via + `@tauri-apps/api`. C'est le **seul** répertoire autorisé à importer Tauri. +- **Garde d'architecture L1 active** : `src/app/no-direct-invoke.test.ts` échoue en CI + si un fichier hors `src/adapters` importe `@tauri-apps/api` **ou** appelle `invoke(`. + Cette garde est notre filet de sécurité pour F1 : elle garantit qu'aucun composant + n'appelle Tauri en dur — donc **aucun composant métier à réécrire** pour le web. +- **Composition déjà bifurquée** : `src/app/di.tsx` → `resolveGateways()` choisit + `createTauriGateways()` vs `createMockGateways()` selon `VITE_USE_MOCK`. F1 ajoute + simplement une **3ᵉ implémentation** (`createHttpWsGateways()`) au même seam. + +**Conséquence sur le plan de lots** : F1 n'est *pas* une extraction de frontière +(elle est faite), mais l'écriture d'un **second jeu d'adapters** derrière des ports +inchangés. Le risque est concentré dans **3 adapters à flux (Channel)** et **3 usages +desktop-only**, pas dans les 15 gateways request/response (mécaniques). + +## 1. Inventaire exhaustif des accès `@tauri-apps/api` + +Aucun accès hors `src/adapters`. Détail par fichier adapter : + +| Adapter | API Tauri utilisée | Nature | Commentaire transport | +|---|---|---|---| +| `system.ts` | `invoke` (`health`) | request/response | trivial HTTP | +| `system.ts` | `listen("domain://event")` | **event** | bus d'événements domaine global → **WS** (fan-out serveur→client) | +| `system.ts` | `open()` de `@tauri-apps/plugin-dialog` | **desktop-only** | picker dossier natif — **pas de sens sur web** (voir §4) | +| `agent.ts` | `invoke` (list/create/change/read/update/delete/…) | request/response | HTTP | +| `agent.ts` | `Channel` (`launch_agent`, `reattach_terminal`) | **stream** | flux PTY agent → **WS** (contrat PTY du carnet) | +| `terminal.ts` | `Channel` (`open_terminal`, `reattach_terminal`) | **stream** | flux PTY terminal → **WS** (cœur de F3) | +| `terminal.ts` | `invoke` (`write_/resize_/close_terminal`) | request/response (contrôle PTY) | messages WS client→serveur (input/resize/close) | +| `ticket.ts` | `Channel` (`agent_send`) | **stream** | flux de réponse chat assistant ticket → **WS** | +| `ticket.ts` | `invoke` (~20 commandes `ticket_*`) | request/response | HTTP | +| `window.ts` | `invoke` (`open_/close_/list_view_window`) | **desktop-only** | cycle de vie fenêtre OS (WebviewWindow) — voir §4 | +| `window.ts` | `listen("view-window://lifecycle")` | **desktop-only / event** | fermeture fenêtre OS — voir §4 | +| `focusedProject.ts` | `invoke` + `listen("focused-project://changed")` | **desktop-only / event** | canal fenêtre principale ⇄ fenêtre détachée — voir §4 | +| `project.ts`, `layout.ts`, `git.ts`, `profile.ts`, `modelServer.ts`, `template.ts`, `skill.ts`, `memory.ts`, `embedder.ts`, `permission.ts`, `workState.ts`, `conversation.ts`, `input.ts` | `invoke` seul | request/response | HTTP mécanique | +| `uiPreferences.ts` | *(aucune)* — `localStorage` | frontend-pur | déjà transport-neutre, marche tel quel sur web | + +## 2. Classement par nature de transport + +### 2a. `request/response` (le gros du volume — mapping HTTP direct) +Tous les `invoke()` non-Channel. 15 gateways sur 21 sont **exclusivement** +request/response : `project, layout, git, profile, modelServer, template, skill, +memory, embedder, permission, workState, conversation, input`, + le `health` de +`system` et les ~20 commandes `ticket_*`. +→ **Aucune décision d'archi** : un adapter HTTP générique (POST `{command, args}` ou +route par commande) suffit. C'est le socle du **premier livrable B4/F2** (web ouvre un +projet + read-only), qui n'a besoin d'aucun stream. + +### 2b. `stream` (Channel Tauri → WebSocket) — **le vrai chantier F1/F3** +Trois flux, tous modélisés côté port par un **callback `onData`/`onChunk`** (le port +ne connaît jamais `Channel`) : + +1. **PTY terminal** — `terminal.ts` `openTerminal`/`reattach` : `Channel`, + octets bruts → `term.write`. **Cœur de F3 (xterm sur WS).** +2. **PTY agent** — `agent.ts` `launchAgent`/`reattach` : même `Channel`, + réutilise `makeTerminalHandle`. Même transport que le PTY terminal (le carnet dit + « agents via WS PTY », lot B6). +3. **Chat assistant ticket** — `ticket.ts` `sendTicketChat` : `Channel`, + flux de réponse LLM structuré (`agent_send`). Stream applicatif, pas PTY. + +Point de contrat clé : le **handle** (`TerminalHandle`) porte déjà `write/resize/ +detach/close` **et** la sémantique detach≠close (détacher la vue sans tuer le PTY). +Cette sémantique **correspond exactement** au contrat PTY WebSocket du carnet +(`attach_terminal`/`detach`, PTY autorité serveur, scrollback borné). L'adapter WS +implémentera `makeTerminalHandle` en émettant `input/resize/detach/close` sur la socket +et en poussant `output(seq,bytes)` dans `onData`. **Le port n'a pas à changer.** + +### 2c. `event` (listen Tauri → WebSocket serveur→client) +Un seul événement *portable* : `system.ts` `onDomainEvent("domain://event")` — le bus +d'événements domaine (agent launched, rate-limited, backgroundTaskChanged, issueDeleted, +progress modèle…). Sur web ⇒ **canal WS serveur→client** (ou multiplexé sur la même +socket que le PTY selon l'arbitrage HTTP+WS séparés vs WS unique, laissé à Architect). +Les deux autres `listen` (`view-window://lifecycle`, `focused-project://changed`) sont +**desktop-only** (§4), pas des events métier. + +### 2d. `desktop-only` (pas de portage web direct — voir §4) +`system.pickFolder` (dialog natif), tout `window.ts` (WebviewWindow OS), +`focusedProject.ts` (coordination multi-fenêtres OS). + +## 3. Frontière F1 : où placer les deux implémentations + +**La frontière est déjà `src/ports/` ⇄ `src/adapters/`.** F1 = ajouter un dossier +`src/adapters/http/` (ou `webws/`) avec une implémentation par port, et un +`createHttpWsGateways()` branché dans `resolveGateways()`. + +Découpage recommandé des adapters web par priorité (aligné sur les lots du carnet) : + +- **F2 (premier livrable, request/response only)** : adapters HTTP pour `system.health`, + `project`, `layout` (read), `workState`, `ticket` (read), `agent.listAgents` + + `onDomainEvent` sur WS. Suffit pour « ouvrir un projet + état read-only ». +- **F3 (xterm sur WS)** : adapter WS pour `terminal` (+ réécriture de + `makeTerminalHandle` en variante socket). `TerminalView.tsx` **ne change pas** : il ne + connaît que le port et un `open/reattach` injecté. +- **B6/F(agents)** : `agent.launchAgent/reattach` sur le même transport WS PTY. +- **Chat ticket** : `ticket.sendTicketChat` sur WS applicatif. +- **desktop-only** : `window`, `focusedProject`, `pickFolder` → implémentations web + dégradées/alternatives (§4). + +**Aucun composant métier (`features/`, `app/`) n'appelle Tauri** — garanti par la garde +L1. Donc F1 ne touche **que** `src/adapters/`, `src/app/di.tsx` (3-way resolve) et +`vite`/build (cible web sans Tauri). C'est la bonne nouvelle du plan. + +## 4. Points desktop-only — décisions à cadrer (Architect) + +Ces trois surfaces n'ont pas d'équivalent WS trivial ; elles ne bloquent **pas** le +premier livrable (read-only) mais doivent être tranchées avant les lots concernés : + +1. **`system.pickFolder`** (créer/ouvrir un projet par un dossier local). Sur le + serveur, le « dossier » est **sur la machine serveur**, pas sur le client web. ⇒ Il + faut un **file-picker serveur** (browse arborescence serveur via HTTP) au lieu du + dialog natif OS. Impacte la création/ouverture de projet côté web. **À cadrer.** +2. **`window.ts` (détacher un panneau en fenêtre OS)** : concept purement desktop + (Tauri `WebviewWindow`). Équivalent web = nouvel **onglet/`window.open`** navigateur, + sémantique différente (pas de registre anti-doublon OS). V1 web : dégrader en + **no-op** ou en onglet navigateur, la garde L1 permet une impl web distincte sans + toucher les composants. **À cadrer (probablement hors-scope V1 web).** +3. **`focusedProject.ts`** : coordination fenêtre principale ⇄ fenêtres détachées. Sans + fenêtres détachées (point 2), le canal se réduit à un état local. V1 web : + implémentation triviale in-memory / `BroadcastChannel` si multi-onglets. **À cadrer.** + +## 5. Signaux pour le plan de lots / contrat de transport + +- ✅ **Rien ne remet en cause le plan.** La frontière gateways transport-neutres du + carnet est **déjà réalisée et testée** (garde L1). F1 est un ajout d'adapters, pas un + refactor de composants. Gain de risque majeur. +- ✅ **Le contrat PTY WebSocket du carnet colle au port existant** : `TerminalHandle` + (write/resize/detach/close, detach≠close, scrollback au reattach) mappe 1-pour-1 sur + `attach_terminal/input/resize/detach/close` + `attached(scrollback)/output(seq)`. + Aucune évolution de port nécessaire pour F3. +- ⚠️ **3 flux Channel** (PTY terminal, PTY agent, chat ticket) partagent la même + mécanique `onData`/callback — un **transport WS commun** peut les servir tous les + trois. À confirmer avec le choix « HTTP+WS séparés vs WS multiplexé » (délégué + Architect). +- ⚠️ **`pickFolder`** est le seul point request/response qui **change de sémantique** sur + web (dossier serveur ≠ dossier client). À arbitrer avant le lot création/ouverture de + projet web. +- ⚠️ **`window`/`focusedProject`** (multi-fenêtres OS) : probablement **hors V1 web** ; + à confirmer pour ne pas gonfler le périmètre. +- ℹ️ **`uiPreferences`** (localStorage) marche déjà tel quel sur web — aucun adapter à + écrire. +- ℹ️ **Convention DTO** : commandes snake_case, payloads camelCase, souvent enveloppés + dans `{ request: {...} }`. L'adapter HTTP doit préserver cette enveloppe pour parler au + même cœur backend (le miroir backend confirme ce point de contrat).