源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s

This commit is contained in:
agent-radar committed 2026-09-01 20:17:27 +08:00
commit 2ed3415fb7
8018 files changed
+1936476

No files matched your search

@@ -0,0 +1,97 @@
# Browser Node Actions Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use `executing-plans` to implement this plan task-by-task.
**Goal:** Add page find, printing, zoom controls, visible-page screenshots, download management, and profile-scoped browsing-data clearing to the Browser Node overflow menu.
**Architecture:** Extend the reusable `@tutti-os/browser-node` contract and Electron guest manager with generic browser actions and state events. Keep native file dialogs, download-folder integration, and file opening in the desktop host adapter. Reuse the existing UI System menu, buttons, inputs, and confirmation dialog, with all browser copy in the package i18n module.
**Tech Stack:** React 19, TypeScript, Electron 35, `@tutti-os/ui-system`, Node test runner.
---
### Task 1: Extend Browser Node contracts and runtime state
**Files:**
- Modify: `packages/browser/workbench-node/src/core/types.ts`
- Modify: `packages/browser/workbench-node/src/core/runtimeStore.ts`
- Test: `packages/browser/workbench-node/src/core/runtimeStore.test.ts`
**Steps:**
1. Add typed inputs for find, zoom, screenshot, browsing-data clearing, and download actions.
2. Add find-result and download-state Browser Node events.
3. Extend the runtime store with stable find, zoom, and download snapshots.
4. Add failing runtime-store tests for find and download event transitions.
5. Implement the minimal reducer changes and rerun the focused tests.
### Task 2: Implement Electron guest actions
**Files:**
- Modify: `packages/browser/workbench-node/src/electron-main/types.ts`
- Modify: `packages/browser/workbench-node/src/electron-main/guestManager.ts`
- Test: `packages/browser/workbench-node/src/electron-main/electronMain.test.ts`
**Steps:**
1. Extend the mockable guest interfaces with find, print, zoom, session clearing, and download APIs.
2. Add failing tests for find results, printing, zoom clamping, screenshot capture, data clearing, and download lifecycle/actions.
3. Implement action routing against the registered guest only.
4. Attach and detach `found-in-page` and session download listeners without leaking across guest lifecycles.
5. Rerun the focused package tests.
### Task 3: Wire package IPC and desktop-native adapters
**Files:**
- Modify: `packages/browser/workbench-node/src/electron-main/registerElectronMain.ts`
- Modify: `apps/desktop/src/shared/contracts/ipc.ts`
- Modify: `apps/desktop/src/preload/api/browser.ts`
- Modify: `apps/desktop/src/main/ipc/browser.ts`
- Test: `packages/browser/workbench-node/src/electron-main/electronMain.test.ts`
**Steps:**
1. Add optional package IPC channels and handlers for each new action.
2. Add desktop IPC request and response mappings.
3. Expose the narrow preload API methods.
4. Save screenshots through a native save dialog and PNG write in the desktop host.
5. Configure browser sessions to use the operating-system Downloads directory.
6. Route open-file and reveal-in-folder download actions through Electron `shell`.
7. Add handler-routing tests and rerun them.
### Task 4: Build the Browser Node menu and panels
**Files:**
- Modify: `packages/browser/workbench-node/src/react/BrowserNode.tsx`
- Modify: `packages/browser/workbench-node/src/i18n/browserNodeI18n.ts`
**Steps:**
1. Make the overflow menu independent of the development-only DevTools action.
2. Add the find bar with query, result count, previous/next, and close behavior.
3. Add the inline zoom controls with reset, decrease, and increase actions.
4. Add print, screenshot, and downloads commands.
5. Add a compact downloads panel with progress, pause/resume, cancel, open, and reveal actions.
6. Add a UI System confirmation dialog before clearing the active browser profile's browsing data.
7. Route all English and Simplified Chinese copy through package i18n.
### Task 5: Verify boundaries, behavior, and documentation
**Files:**
- Modify: `docs/architecture/browser-node-package.md`
- Modify: `docs/conventions/troubleshooting/toolchain-browser-terminal.md` only if verification reveals a reusable debugging trap.
**Steps:**
1. Run `pnpm --filter @tutti-os/browser-node test`.
2. Run `pnpm --filter @tutti-os/browser-node typecheck`.
3. Run `pnpm --filter @tutti-os/desktop typecheck`.
4. Run `pnpm check:i18n` and `pnpm check:ui-boundaries`.
5. Run `pnpm --filter @tutti-os/desktop build`.
6. Inspect the final diff for package/host ownership, raw UI copy, and accidental unrelated changes.
7. Update the Browser Node architecture document with the new action and native-host ownership boundaries.
@@ -0,0 +1,36 @@
# Browser Node Advanced Actions Implementation Plan
**Goal:** Add device emulation, full-page screenshots, Cookie import, and an in-product browser settings surface without moving browser policy into the desktop renderer.
**Architecture:** Keep reusable contracts, runtime state, Cookie parsing, and Electron guest operations in `@tutti-os/browser-workbench-node`. The desktop host owns native file/directory dialogs and IPC transport. The renderer only presents settings and invokes the package host API. Settings apply to the active browser session for the current app run; popup and external-navigation security policy remains fixed.
## Task 1: Extend package contracts and runtime state
- Add screenshot modes, device presets, Cookie import results, and browser session settings types.
- Add optional host API operations and runtime-store events.
- Cover state transitions with focused unit tests.
## Task 2: Implement Electron guest capabilities
- Capture full-page screenshots through the Chrome DevTools Protocol while preserving existing visible-area capture.
- Enable and disable fixed device-emulation presets.
- Parse JSON and Netscape Cookie exports, validate entries, and import only into the active guest session.
- Support choosing and applying a download directory.
- Add unit tests for parsers, page actions, and manager behavior.
## Task 3: Wire desktop IPC and native dialogs
- Extend shared IPC contracts, main-process registration, preload API, and preload types.
- Use native open/save dialogs for Cookie files, screenshot output, and download directories.
- Avoid exposing Cookie file contents to the renderer or diagnostics.
## Task 4: Build the browser settings experience
- Add device-emulation and screenshot-mode actions to the overflow menu.
- Add a UI System-based settings dialog for device preset, zoom, screenshot mode, download directory, Cookie import, and browsing-data clearing.
- Route all visible copy through BrowserNode i18n resources.
## Task 5: Document and verify
- Update package and architecture documentation for ownership and session-scoped behavior.
- Run focused package tests, desktop/package typechecks, i18n/UI boundary checks, desktop build, and changed-aware validation.
@@ -0,0 +1,37 @@
# Standalone Agent Sidebar Motion Design
## Goal
Make right-sidebar tools feel immediate and continuous instead of pausing for
native window resize and heavy panel initialization before appearing.
## Interaction
Panel selection updates immediately. The outer sidebar clips its full-width
content while its reserved width grows from the right over 260 milliseconds,
using the existing workbench motion curve. The conversation narrows during the
same transition, so the panel reads as an adjacent surface growing into place.
Native content-width expansion starts on the next animation frame and never
gates renderer state. macOS uses Electron's native bounds animation; other
platforms keep the existing bounds behavior.
## Loading and lifecycle
Files, Browser, Apps, and Message Center defer their first body mount until the
outer transition finishes. The lightweight title and panel controls appear
immediately, so the first frame is cheap. Once mounted, tool bodies remain
mounted while hidden to preserve browser state, file selection, and later-open
performance.
Rapid panel changes cancel any queued native resize frame. Stale IPC responses
continue to be rejected by the existing resize request sequence. A system
`prefers-reduced-motion` preference removes the CSS transition and mount delay.
## Verification
- source-level component checks cover immediate dispatch, clipped motion,
delayed first mount, and reduced-motion behavior
- pure main-process tests cover macOS-only native animation policy
- desktop typecheck, build, renderer boundaries, and changed-aware checks cover
integration
@@ -0,0 +1,147 @@
# Standalone Agent Tool Sidebar Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add a host-owned, mutually exclusive right tool sidebar to the standalone Agent window without adding tool concepts or state to AgentGUI. The sidebar always reserves adjacent layout space and never covers the message flow.
**Architecture:** `apps/desktop` owns the tool identifiers, open/close reducer, reminder badges, toolbar, panel shell, and content renderers. `StandaloneAgentWindow` composes the toolbar beside the existing Agent workbench header and the panel beside `DesktopAgentGUIWorkbenchBody`; `@tutti-os/agent-gui` receives no new tool state, commands, or durable data. Browser and Terminal share one dropdown trigger while retaining distinct panel identities.
**Tech Stack:** React 19, TypeScript, Node test runner, `@tutti-os/ui-system`, desktop i18n, existing workspace file manager and app center panes.
---
### Task 1: Define the sidebar interaction model
**Files:**
- Create: `apps/desktop/src/renderer/src/features/workspace-workbench/ui/standaloneAgentToolSidebarModel.ts`
- Test: `apps/desktop/src/renderer/src/features/workspace-workbench/ui/standaloneAgentToolSidebarModel.test.ts`
**Step 1: Write failing tests**
Cover these transitions:
- clicking a closed top-level entry opens it
- clicking the active entry closes it
- selecting another entry swaps the single open panel
- Browser and Terminal share one launcher; clicking the whole control opens a menu and selecting an item opens that tool
- switching panels preserves per-panel mounted state in the view layer
- reminder values normalize to compact non-negative counts
**Step 2: Run the focused test and confirm failure**
Run:
```sh
pnpm --filter @tutti-os/desktop exec node --import ./test/register-asset-stub.mjs --test --experimental-strip-types ./src/renderer/src/features/workspace-workbench/ui/standaloneAgentToolSidebarModel.test.ts
```
Expected: fail because the model module does not exist.
**Step 3: Implement the reducer and selectors**
Keep the model pure. It owns only the active and mounted panel state plus deterministic toggle/select transitions. It must not import AgentGUI, daemon clients, Electron APIs, or React.
**Step 4: Run the focused test and confirm pass**
Run the same command and expect all model tests to pass.
### Task 2: Build the host-owned toolbar and panel shell
**Files:**
- Create: `apps/desktop/src/renderer/src/features/workspace-workbench/ui/StandaloneAgentToolSidebar.tsx`
**Step 1: Implement the component**
Use UI-system `Button`, `DropdownMenu`, `Tooltip`, and semantic icons/tokens. Keep the toolbar, reminders, lazy panel mounting, existing Files and Apps panes, and neutral host-adapter placeholders encapsulated in the desktop component.
**Step 2: Verify through the pure model test and desktop typecheck**
Run:
```sh
pnpm --filter @tutti-os/desktop exec node --import ./test/register-asset-stub.mjs --test --experimental-strip-types ./src/renderer/src/features/workspace-workbench/ui/standaloneAgentToolSidebarModel.test.ts
pnpm --filter @tutti-os/desktop typecheck
```
Expected: the state model tests and all desktop types pass. The desktop package's
Node test lane does not execute TSX component tests, so interaction behavior is
kept in the tested pure model while the component is covered by typecheck and
the desktop build.
### Task 3: Compose the sidebar around the standalone Agent window
**Files:**
- Modify: `apps/desktop/src/renderer/src/features/workspace-workbench/ui/StandaloneAgentWindow.tsx`
- Modify: `apps/desktop/src/shared/i18n/locales/en.ts`
- Modify: `apps/desktop/src/shared/i18n/locales/zh-CN.ts`
**Step 1: Add localized host labels**
Add desktop-owned labels for File, Tool, Browser, Terminal, Apps, Messages, Close, and reminder accessibility copy. Use `Apps` / `应用`; do not introduce `App Center` / `应用中心` in the new shell.
**Step 2: Inject the toolbar without changing AgentGUI internals**
Render the host toolbar as a sibling overlay in the standalone header. Do not add tool props to `AgentGUI`, `AgentGUINode`, its controller, or its view model.
**Step 3: Wrap the body with the sidebar shell**
Keep `DesktopAgentGUIWorkbenchBody` as the flexible main child. Mount existing
File and Apps panes through render slots. Browser and Terminal reuse their real
desktop workbench contributions in ephemeral panel-local hosts, while Messages
retains a neutral slot until its host adapter is attached. The sidebar shell
owns only visibility and layout.
The full resolved sidebar width participates in the body flex layout even when
the native window cannot grow by that amount. Conversation file links and
reference preview requests open Files with a reveal intent; shared App Center
open state activates Apps. Screen constraints narrow the main Agent surface
instead of turning the tool panel into an overlay.
Panel activation is optimistic UI-local state: dispatch it before native resize
IPC, schedule that IPC for the next animation frame, and reveal the shell with
a clipped width transition. Delay expensive first-use panel bodies until the
shell transition completes, retain them after mounting, and honor the system
reduced-motion preference. On macOS, animate the Electron content bounds in
parallel with the renderer transition.
**Step 4: Derive reminder state at the host boundary**
Subscribe to the shared workspace Agent Activity snapshot and derive the Messages reminder count with the existing attention selector. Pass only the resulting number into the toolbar.
### Task 4: Record the durable boundary
**Files:**
- Modify: `docs/architecture/agent-gui-node.md`
Document that standalone-window tool drawers are desktop host chrome. AgentGUI may receive neutral rendered header actions, but tool identity, badges, panel visibility, mutual exclusion, widths, and tool content must stay outside AgentGUI runtime/controller state.
### Task 5: Verify the change
**Step 1: Run focused tests**
```sh
pnpm --filter @tutti-os/desktop exec node --import ./test/register-asset-stub.mjs --test --experimental-strip-types ./src/renderer/src/features/workspace-workbench/ui/standaloneAgentToolSidebarModel.test.ts
```
**Step 2: Run boundary and i18n checks**
```sh
pnpm check:i18n
pnpm check:renderer-boundaries
pnpm check:agent-activity-runtime-boundaries
```
**Step 3: Run desktop typecheck and changed-aware validation**
```sh
pnpm --filter @tutti-os/desktop typecheck
pnpm check:changed
```
**Step 4: Perform a documentation impact check**
Expected decision: `improve` the existing AgentGuiNode architecture document with the host-owned tool-sidebar invariant.
@@ -0,0 +1,130 @@
# Workspace App Background State Implementation Plan
> **For Codex:** Use the `executing-plans` workflow to implement this plan task-by-task in the current workspace. Do not delegate without explicit user approval.
**Goal:** Keep every workspace app page and its in-page Agent alive when the user returns to the Apps catalog, then restore the same live page when that app is reopened.
**Architecture:** The shared App Center body will own a renderer-lifetime set of opened app ids. It will keep one `BrowserNode` mounted per opened app, give each browser a stable app-specific node id, and switch both DOM visibility and the Browser Node `hidden` input instead of conditionally unmounting the active app. Once a ready App Center snapshot confirms an app no longer exists, its retained browser is released.
**Tech Stack:** React, TypeScript, Electron `<webview>`, `@tutti-os/browser-node`, Node test runner.
---
### Task 1: Specify retained app instance behavior
**Files:**
- Create: `apps/desktop/src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterInlineAppRetention.ts`
- Create: `apps/desktop/src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterInlineAppRetention.test.ts`
- Modify: `apps/desktop/src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterInlineAppBody.tsx`
- Modify: `apps/desktop/src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterLaunchRequest.ts`
- Modify: `apps/desktop/src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterContribution.test.ts`
**Step 1: Write failing tests**
Cover these pure state transitions:
- opening the first app retains it;
- returning to the catalog keeps retained apps;
- opening a second app retains both instances without duplication;
- a confirmed available-app list removes an uninstalled app;
- the inline browser node id is stable and unique per app.
**Step 2: Run the focused test and confirm it fails**
Run:
```sh
cd apps/desktop
node --import ./test/register-asset-stub.mjs --test --experimental-strip-types \
src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterInlineAppRetention.test.ts \
src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterContribution.test.ts
```
Expected: failure because the retention and node-id helpers do not exist yet.
**Step 3: Add the minimal pure helpers**
Add a helper that appends a non-empty active app id, preserves insertion order, removes duplicates, and prunes only when the caller supplies a confirmed available-app set. Add a stable inline node id using the existing `workspace-app:` namespace so workspace-app Browser events continue to route through the existing host feature.
**Step 4: Run the focused test and confirm it passes**
Run the same command. Expected: all focused tests pass.
### Task 2: Preserve all opened app Browser Nodes
**Files:**
- Modify: `apps/desktop/src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterInlineAppBody.tsx`
**Step 1: Keep catalog and app layers mounted**
Replace the mutually exclusive catalog/app return branches with one stacked container. Keep `WorkspaceAppCenterPane` mounted and keep one keyed `WorkspaceAppCenterInlineBrowser` per retained app id.
**Step 2: Coordinate Electron guest visibility**
For every inactive app layer, apply non-interactive/invisible container styles and pass `hidden={true}` into `BrowserNode`. For the active app, pass through the Workbench minimized state. This prevents hidden Electron guest surfaces from covering the catalog while preserving their renderer state.
**Step 3: Release unavailable apps safely**
Prune retained ids only when App Center is `ready` for the current workspace. Loading and reconnection snapshots must not destroy live pages.
**Step 4: Run feature tests**
Run:
```sh
cd apps/desktop
node --import ./test/register-asset-stub.mjs --test --experimental-strip-types \
src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterInlineAppRetention.test.ts \
src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterContribution.test.ts \
src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterInlineOpen.test.ts \
src/renderer/src/features/workspace-workbench/ui/standaloneAgentToolWorkbench.test.ts
```
Expected: all tests pass.
### Task 3: Record the durable lifecycle rule
**Files:**
- Modify: `docs/architecture/desktop-windows.md`
- Modify: `docs/conventions/troubleshooting/toolchain-browser-terminal.md`
**Step 1: Update architecture**
Document that both OS Workbench Apps and Agent-only Apps panels retain all opened inline app Browser Nodes, use `openAppId` only for visibility, and release instances only after confirmed app removal or host teardown.
**Step 2: Improve the existing webview troubleshooting note**
Add App Center catalog/app switching as a canonical case: ancestor hiding is insufficient; every inactive Browser Node must receive `hidden={true}` and remain mounted when state preservation is required.
### Task 4: Verify desktop integration
**Files:**
- No additional files expected.
**Step 1: Run changed-aware and boundary checks**
```sh
pnpm check:changed
pnpm check:renderer-boundaries
```
Expected: pass.
**Step 2: Run Desktop typecheck and build**
```sh
pnpm --filter @tutti-os/desktop typecheck
pnpm --filter @tutti-os/desktop build
```
Expected: pass.
**Step 3: Visual verification**
Open an app with a running Agent, return to Apps, reopen it, and confirm the same page state and Agent progress remain. Repeat with two different apps to verify both instances survive catalog round trips. If no live desktop session is available, report visual verification as unavailable rather than claiming it passed.
No commits are included in this execution because the user requested the behavior change, not repository history mutation.
@@ -0,0 +1,41 @@
# Workspace UI Mode Design
## Goal
Keep the original Tutti OS workspace available while allowing users to select
the focused standalone Agent experience as their normal workspace interface.
## Interaction
The General settings page exposes two radio-card choices:
- Agent mode renders AgentGUI as the primary workspace surface and opens host
tools through the new right sidebar.
- OS mode renders the existing desktop, windows, dock, launchpad, and workspace
chrome without removing or rewriting that code path.
The preference is saved immediately and applies whenever main next creates a
window for startup or workspace opening. It does not morph an already-created
OS window into an Agent window. Explicit `view=agent` windows always remain
standalone Agent windows.
## Ownership and persistence
The mode belongs to the desktop host, not AgentGUI. It is encoded through the
existing durable desktop feature-preference map under
`workspace.standaloneAgentMode`; an absent flag resolves to OS mode. A manual
selection is always durable: `true` preserves Agent mode and `false` preserves
OS mode. The mode helper preserves unrelated flags when it changes this value.
Electron main resolves the preference before window creation. Agent mode uses
`windowKind: "agent"` plus `view=agent`; OS mode uses the original workspace
window plus `view=workspace`. Renderer routing remains explicit and AgentGUI
receives no mode state, so the OS workbench remains intact.
## Verification
- pure tests cover preference mapping and native launch-window selection
- settings-service tests cover persistence without clobbering other flags
- settings source tests cover the General-page control wiring
- desktop typecheck, i18n, renderer boundaries, build, and changed-aware checks
cover integration
@@ -0,0 +1,109 @@
# Agent Sidebar Management Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add a configuration-menu entry that opens a local Agent sidebar manager with ordering and visibility controls.
**Architecture:** Keep ordering and visibility as UI-local AgentGUI chrome state in one device-global `localStorage` record. The provider rail and management dialog share that model; hiding the selected target returns the rail filter to All, and same-page preference events keep multiple AgentGUI instances synchronized.
**Tech Stack:** React, TypeScript, Vitest, `@tutti-os/ui-system`, browser `localStorage`.
---
### Task 1: Define and test the local preference model
**Files:**
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/model/agentGuiProviderRailOrder.ts`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/model/agentGuiProviderRailOrder.spec.ts`
**Steps:**
1. Add failing tests for a device-global storage key, versioned order/hidden serialization, malformed-input sanitization, visibility projection, and reorder behavior.
2. Run the focused model test and confirm it fails.
3. Implement the typed preference parser, serializer, visibility helper, and global key.
4. Run the focused model test and confirm it passes.
### Task 2: Add the management entry and dialog
**Files:**
- Create: `packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUIProviderManagerDialog.tsx`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUIAccountConfig.tsx`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUIProviderRail.tsx`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/AgentGUINodeView.tsx`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUINodeView.types.ts`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/AgentGUINode.labels.ts`
- Modify: `packages/agent/gui/app/renderer/i18n/locales/en.ts`
- Modify: `packages/agent/gui/app/renderer/i18n/locales/zh-CN.ts`
**Steps:**
1. Add failing layout tests for the menu entry, dialog, reorder control, hidden tiles, and selected-target fallback to All.
2. Run the focused layout test and confirm it fails.
3. Add localized labels and a UI System dialog with draggable rows, accessible move controls, and visibility switches.
4. Store preferences under one global key and broadcast changes to mounted AgentGUI instances.
5. Filter only provider tiles; keep the All tile fixed and preserve real Agent target data.
6. Run the focused layout tests and confirm they pass.
### Task 3: Verify package boundaries and documentation
**Files:**
- Modify: `docs/architecture/agent-gui-node.md`
**Steps:**
1. Update the durable AgentGUI architecture note from workspace-scoped ordering to device-local ordering and visibility.
2. Run `pnpm --filter @tutti-os/agent-gui test`.
3. Run `pnpm check:agent-activity-runtime-boundaries`.
4. Run `pnpm check:i18n`.
5. Run `pnpm check:changed` and inspect any focused failure logs.
### Task 4: Present Agent management as an editable grid
**Files:**
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUIProviderManagerDialog.tsx`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUINodeView.types.ts`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/view/agentGUIProviderManagerLabels.ts`
- Modify: `packages/agent/gui/app/renderer/i18n/locales/en.agentGuiProviderIdentity.ts`
- Modify: `packages/agent/gui/app/renderer/i18n/locales/zh-CN.agentGuiProviderIdentity.ts`
- Modify: `packages/agent/gui/app/renderer/agentactivity.css`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/AgentGUINodeView.layout.spec.tsx`
- Modify: `docs/architecture/agent-gui-node.md`
**Steps:**
1. Replace the row/switch assertions with failing tests for separate available and disabled grids, long-press edit mode, remove/add actions, and active-only ordering.
2. Run the focused layout test and confirm the new expectations fail.
3. Render a five-column, icon-led grid using the existing Agent artwork and UI System dialog primitives.
4. Enter global edit mode after a long press, show semantic destructive remove controls, and apply a reduced-motion-safe wiggle animation to available tiles.
5. Keep drag sorting in the available grid; move removed targets into the disabled grid and restore added targets to the end of the available order.
6. Add localized section, instruction, remove, add, and empty-state labels.
7. Update the durable architecture note with the edit-mode and active-only ordering rules.
8. Run the focused test, package tests, i18n check, UI boundary check, Agent activity boundary check, and desktop build.
### Task 5: Enforce one available Agent and support cross-grid drag
**Files:**
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUIProviderManagerDialog.tsx`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUIProviderRail.tsx`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUINodeView.types.ts`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/view/agentGUIProviderManagerLabels.ts`
- Modify: `packages/agent/gui/app/renderer/i18n/locales/en.agentGuiProviderIdentity.ts`
- Modify: `packages/agent/gui/app/renderer/i18n/locales/zh-CN.agentGuiProviderIdentity.ts`
- Modify: `packages/agent/gui/app/renderer/agentactivity.css`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/AgentGUINodeView.layout.spec.tsx`
- Modify: `docs/architecture/agent-gui-node.md`
**Steps:**
1. Add failing tests that the final available Agent cannot be removed by its action or by a drop into the disabled grid.
2. Add failing tests for dragging an available Agent into the disabled grid and a disabled Agent into a specific position in the available grid.
3. Replace the dialog's single-list drag state with source-aware drag state and accessible drop-zone presentation.
4. Make both grids accept drops; same-grid drops reorder, while cross-grid drops update order and visibility atomically.
5. Enforce the minimum-one invariant again in the preference update handler so it cannot be bypassed by UI events.
6. Add localized copy for the final-Agent restriction and semantic drop-zone states.
7. Update the durable AgentGUI interaction rule and rerun focused tests, package tests, degradation checks, i18n checks, boundary checks, and the desktop build.
@@ -0,0 +1,87 @@
# Open With Submenu Placement Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Keep the file manager's Open With submenu fully usable at every window width by opening right, flipping left, or replacing the parent menu in constrained viewports.
**Architecture:** Keep placement policy in the file-manager package as a pure function and render through the existing `ViewportMenuSurface`. The menu uses the viewport as its collision boundary, preserves the existing flyout interaction when either side fits, and uses an in-place overlay with a back action only when two columns cannot fit.
**Tech Stack:** React 19, TypeScript, `@tutti-os/ui-system`, Node test runner, Tailwind utilities.
---
### Task 1: Model adaptive submenu placement
**Files:**
- Modify: `packages/workspace/file-manager/src/ui/contextMenuPlacement.ts`
- Test: `packages/workspace/file-manager/src/ui/contextMenuPlacement.test.ts`
**Step 1: Write failing placement tests**
Cover these exact cases:
- enough right-side room returns `mode: "right"` beside the trigger;
- insufficient right room with enough left room returns `mode: "left"`;
- insufficient room on both sides returns `mode: "overlay"` at the parent menu position;
- vertical position and effective width stay inside the configured viewport padding.
**Step 2: Run the focused test and verify it fails**
Run:
```bash
pnpm --filter @tutti-os/workspace-file-manager test
```
Expected: the new placement symbol or assertions fail before implementation.
**Step 3: Implement the pure placement resolver**
Add a resolver that accepts trigger bounds, parent-menu bounds, submenu dimensions, viewport dimensions, gap, and padding. Prefer right, then left, then overlay. Clamp the returned position and shrink the effective submenu width only when the viewport is narrower than the normal 220-pixel surface.
**Step 4: Run the focused test and verify it passes**
Run the same package test command and expect all placement tests to pass.
### Task 2: Render the responsive submenu modes
**Files:**
- Modify: `packages/workspace/file-manager/src/ui/WorkspaceFileManagerContextMenu.tsx`
- Test: `packages/workspace/file-manager/src/ui/contextMenuPlacement.test.ts`
**Step 1: Integrate the resolver**
Measure the trigger and parent menu when the submenu opens or its estimated height changes. Recalculate on window resize. Keep `ViewportMenuSurface` boundary constraints enabled and expose the resolved mode as a data attribute for inspection.
**Step 2: Add the constrained-width overlay interaction**
When the resolver returns `overlay`, render the submenu over the parent menu and prepend a back row using the shared `ArrowLeftIcon` and existing localized Open With label. Keep the submenu scrollable with a viewport-safe maximum height and width.
**Step 3: Add keyboard behavior**
Open with ArrowRight, focus the first submenu action for keyboard entry, and close back to the trigger with ArrowLeft or Escape. Do not steal focus for pointer-hover opening.
**Step 4: Run package and desktop checks**
Run:
```bash
pnpm --filter @tutti-os/workspace-file-manager test
pnpm --filter @tutti-os/workspace-file-manager typecheck
pnpm check:ui-boundaries
pnpm --filter @tutti-os/desktop build
pnpm check:changed --tail-lines 80
```
Expected: all commands pass; the submenu is visible in right, left, overlay, and tall-content cases.
### Task 3: Documentation impact check
**Files:**
- Review: `packages/workspace/file-manager/README.md`
- Review: `docs/conventions/desktop-visual-language.md`
Confirm whether this collision behavior introduces a durable public contract. If not, keep the implementation plan as the design record and report that no additional durable documentation update is required.
@@ -0,0 +1,89 @@
# Standalone Open-With Internal Actions Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Hide the file-viewer and Tutti in-app-browser entries from the standalone Agent window's Open With submenu without changing the OS workspace mode.
**Architecture:** Add a default-on presentation flag to the reusable workspace file manager and pass it through the desktop file-manager pane. The standalone sidebar opts out explicitly, while the OS workspace node keeps the default-on behavior and all action handlers remain unchanged.
**Tech Stack:** React 19, TypeScript, Node test runner, Electron/Vite desktop renderer.
---
### Task 1: Lock the standalone-only behavior
**Files:**
- Modify: `apps/desktop/src/renderer/src/features/workspace-workbench/ui/standaloneAgentToolWorkbench.test.ts`
- Verify: `apps/desktop/src/renderer/src/features/workspace-workbench/ui/WorkspaceFilesNodeBody.tsx`
**Step 1: Write the failing source-contract test**
Require the standalone `WorkspaceFileManagerPane` call to pass
`showInternalOpenWithActions={false}`. Require the pane implementation to
default the flag to `true` so callers in the OS workspace remain unchanged.
**Step 2: Run the focused test and verify it fails**
Run:
```sh
pnpm --filter @tutti-os/desktop test -- src/renderer/src/features/workspace-workbench/ui/standaloneAgentToolWorkbench.test.ts
```
Expected: the new standalone opt-out assertion fails before implementation.
### Task 2: Thread the presentation flag through the shared surface
**Files:**
- Modify: `packages/workspace/file-manager/src/ui/WorkspaceFileManager.tsx`
- Modify: `packages/workspace/file-manager/src/ui/WorkspaceFileManagerContextMenuContainer.tsx`
- Modify: `apps/desktop/src/renderer/src/features/workspace-file-manager/ui/WorkspaceFileManagerPane.tsx`
- Modify: `apps/desktop/src/renderer/src/features/workspace-workbench/ui/StandaloneAgentToolSidebarPanel.tsx`
**Step 1: Add the default-on shared prop**
Add `showInternalOpenWithActions?: boolean` to `WorkspaceFileManager`, default
it to `true`, and pass it into the context-menu container. Gate only
`showOpenInFileViewerAction` and `showOpenInAppBrowserAction`; do not change
system application, default-browser, or Other actions.
**Step 2: Expose the same default-on prop from the desktop pane**
Thread the value unchanged into the shared file manager.
**Step 3: Opt out only in the standalone sidebar**
Pass `showInternalOpenWithActions={false}` on the standalone files panel. Do
not add the opt-out to `WorkspaceFilesNodeBody`, so OS workspace behavior uses
the default-on value.
### Task 3: Verify behavior and repository boundaries
**Files:**
- Test: `apps/desktop/src/renderer/src/features/workspace-workbench/ui/standaloneAgentToolWorkbench.test.ts`
- Test: `packages/workspace/file-manager/src/**/*.test.ts`
**Step 1: Run focused tests and type checks**
```sh
pnpm --filter @tutti-os/desktop test -- src/renderer/src/features/workspace-workbench/ui/standaloneAgentToolWorkbench.test.ts
pnpm --filter @tutti-os/workspace-file-manager test
pnpm --filter @tutti-os/workspace-file-manager typecheck
pnpm --filter @tutti-os/desktop typecheck
```
Expected: all commands pass.
**Step 2: Run desktop integration checks**
```sh
pnpm check:renderer-boundaries
pnpm --filter @tutti-os/desktop build
pnpm check:changed --tail-lines 80
```
Expected: all commands pass. No commit is created because this work continues
inside the user's existing dirty task chain.
@@ -0,0 +1,74 @@
# Standalone Browser Element Context Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Let users select a DOM element in the standalone Agent window's browser tool and append a structured snapshot attachment to the current Agent composer draft.
**Architecture:** A desktop-local `browser-element-context` feature owns selection, sanitization, snapshot serialization, and prompt-asset archiving. The feature is loaded with `React.lazy` only after the standalone browser tool mounts. AgentGUI receives a host-neutral sequenced append request that merges the archived file into the currently active draft scope without replacing prompt text or submitting it.
**Tech Stack:** React 19, Electron `<webview>.executeJavaScript`, TypeScript, Vitest, Tutti AgentGUI draft model, desktop prompt-asset archive IPC.
---
### Task 1: Add a host-neutral composer append request
**Files:**
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/AgentGUINode.types.ts`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/AgentGUINode.tsx`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUINodeController.ts`
- Create: `packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUIComposerAppendRequest.ts`
- Test: `packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUIComposerAppendRequest.spec.ts`
1. Define a sequenced request containing already-landed file attachments.
2. Write tests proving append preserves text and existing attachments, deduplicates file ids, and ignores an already-handled sequence.
3. Apply the request to the active session or home draft scope through the canonical draft writer.
4. Run the focused AgentGUI test.
### Task 2: Build the lazy standalone browser element module
**Files:**
- Create: `apps/desktop/src/renderer/src/features/workspace-workbench/browser-element-context/browserElementSnapshot.ts`
- Create: `apps/desktop/src/renderer/src/features/workspace-workbench/browser-element-context/browserElementSelectorScript.ts`
- Create: `apps/desktop/src/renderer/src/features/workspace-workbench/browser-element-context/BrowserElementContextAction.tsx`
- Test: `apps/desktop/src/renderer/src/features/workspace-workbench/browser-element-context/browserElementSnapshot.test.ts`
1. Generate a self-contained guest-page selector script with hover outline, click selection, Escape cancellation, and cleanup.
2. Capture selector, semantic attributes, bounded text/HTML, geometry, viewport, key computed styles, page title, and sanitized URLs.
3. Strip executable content, sensitive attributes, form values, and secret-looking URL parameters before returning the snapshot.
4. Archive the JSON through `archiveAgentPromptFile` and return a landed file attachment.
5. Render an i18n-backed selector button using UI System components and tokens.
### Task 3: Wire only the standalone Agent window
**Files:**
- Modify: `packages/browser/workbench-node/src/react/BrowserNode.tsx`
- Modify: `packages/browser/workbench-node/src/react/BrowserNodeChrome.tsx`
- Modify: `packages/browser/workbench-node/src/react/webviewTag.ts`
- Modify: `apps/desktop/src/renderer/src/features/workspace-workbench/ui/StandaloneAgentBrowserToolPanel.tsx`
- Modify: `apps/desktop/src/renderer/src/features/workspace-workbench/ui/StandaloneAgentToolSidebarPanel.tsx`
- Modify: `apps/desktop/src/renderer/src/features/workspace-workbench/ui/StandaloneAgentToolSidebar.tsx`
- Modify: `apps/desktop/src/renderer/src/features/workspace-workbench/ui/StandaloneAgentWindow.tsx`
- Modify: `apps/desktop/src/renderer/src/features/workspace-agent/ui/DesktopAgentGUIWorkbenchBody.tsx`
- Modify: `apps/desktop/src/renderer/src/features/workspace-agent/ui/desktopAgentGUIWorkbenchModel.ts`
1. Add a neutral navigation-action slot and typed `executeJavaScript` support to BrowserNode.
2. Lazy-load `BrowserElementContextAction` from the standalone browser panel only.
3. Archive selections with the standalone window's existing host-files API.
4. Forward the resulting sequenced append request into AgentGUI and focus the composer.
5. Do not wire the workspace/OS browser contribution.
### Task 4: Localize, document, and verify
**Files:**
- Modify: `apps/desktop/src/shared/i18n/locales/en.ts`
- Modify: `apps/desktop/src/shared/i18n/locales/zh-CN.ts`
- Modify: `docs/architecture/agent-gui-node.md`
1. Add English and Chinese labels for selecting, cancelling, archiving, success, and failure.
2. Document standalone ownership, lazy loading, snapshot privacy limits, and current-draft insertion.
3. Run `pnpm --filter @tutti-os/agent-gui test`.
4. Run focused desktop tests, `pnpm check:i18n`, `pnpm check:agent-activity-runtime-boundaries`, `pnpm --filter @tutti-os/desktop typecheck`, and `pnpm --filter @tutti-os/desktop build`.
@@ -0,0 +1,100 @@
# Browser Element Mention Timeline Implementation Plan
> **For Codex:** Use the Code workflow to implement and verify each task in this plan.
**Goal:** Show standalone Browser element references as compact DOM-tag chips while sending their bounded DOM context to the Agent as ordinary prompt text.
**Architecture:** Keep the `browser-element` reference as one canonical rich-text mention in the display prompt. Its scope carries the sanitized `DOM Path`, `Position`, and `HTML Element` text. The desktop registration renders the compact chip and provides the prompt materializer; AgentGUI expands registered custom mentions immediately before runtime dispatch while retaining the canonical mention in `displayPrompt`. The daemon and runtime protocols receive only ordinary text blocks.
**Tech Stack:** React, TypeScript, Tiptap, Vitest, Go agent runtime tests where the durable prompt payload boundary is involved.
---
### Task 1: Reproduce the sent-message regression
**Files:**
- Modify: `packages/agent/gui/shared/AgentRichTextReadonly.spec.tsx`
- Modify: `packages/agent/gui/shared/agentConversation/projection/workspaceAgentMessageProjection.spec.ts`
**Steps:**
1. Add a readonly-render regression test for a registered `browser-element` custom mention followed by ordinary prompt text.
2. Add a durable-message projection test whose canonical content contains multiple browser-element mentions and a concrete prompt.
3. Run the focused tests and confirm the current failure identifies the layer that drops the mention URI or chip.
### Task 2: Preserve canonical browser-element presentation
**Files:**
- Modify only the smallest owning AgentGUI rich-text/projection file identified by Task 1.
- Preserve the existing desktop registration in `apps/desktop/src/renderer/src/features/workspace-workbench/browser-element-context/registerDesktopBrowserElementMention.tsx`.
**Steps:**
1. Make the canonical rich-text prompt win over lossy provider-visible text when structured user prompt content is available.
2. Ensure readonly custom mentions use the registered host chip renderer and retain adjacent prompt text.
3. Keep attachments/images as independent message content; do not convert browser-element references back into attachment cards.
### Task 3: Preserve the rich prompt in the active detail header
**Files:**
- Modify: `packages/agent/gui/workbench/conversationIdentity.ts`
- Modify: `packages/agent/gui/workbench/header.ts`
- Modify: `packages/agent/gui/workbench/AgentGuiWorkbenchReactiveHeader.tsx`
- Modify: `apps/desktop/src/renderer/src/features/workspace-workbench/ui/StandaloneAgentWindowHeader.tsx`
**Steps:**
1. Derive a presentation-only rich title from the pending activation or first canonical user message.
2. Apply it only while the normalized, capped prompt still matches the session's automatic title; an explicit rename or clear disables it.
3. Render the readonly rich title on demand in the expanded detail header while keeping collapsed and cross-surface identities plain.
### Task 4: Verify the end-to-end contract
**Files:**
- Modify: `docs/architecture/agent-gui-node.md` only if the implementation reveals a missing durable invariant.
**Steps:**
1. Run the focused AgentGUI rich-text, projection, and browser-element context tests.
2. Run `pnpm check:agent-activity-runtime-boundaries` and the focused package typecheck/test lane.
3. Build the desktop renderer and verify the header path compiles in the production bundle.
4. Perform the documentation-impact decision (`discard`, `improve`, `merge`, or `create`) and record any durable ownership rule if needed.
### Task 5: Materialize execution text without changing runtime protocols
**Files:**
- Modify: `packages/agent/gui/shared/agentCustomMentionKinds.ts`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/agentRichText/agentMentionMarkdown.ts`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/controller/agentGuiController.draftMessageHelpers.ts`
- Modify: `apps/desktop/src/renderer/src/features/workspace-workbench/browser-element-context/browserElementMention.ts`
- Modify: `apps/desktop/src/renderer/src/features/workspace-workbench/browser-element-context/registerDesktopBrowserElementMention.tsx`
**Steps:**
1. Let a registered custom mention optionally materialize provider-visible prompt text.
2. Preserve the canonical mention as `displayPrompt`, but expand it in `runtimeContent` immediately before dispatch.
3. Carry the bounded browser snapshot text in the browser mention scope and remove the temporary prompt-file archive.
4. Preserve the published `files` append behavior for external AgentGUI consumers; browser selection uses the additive text append path only.
### Task 6: Narrow conversation-title mention projection
**Files:**
- Modify: `packages/agent/gui/shared/agentConversationTitleProjection.ts`
- Create: `packages/agent/gui/shared/agentConversationTitlePromptSelector.ts`
- Modify: `packages/agent/gui/contexts/workspace/presentation/renderer/agentGuiConversationList/useAgentGuiConversationList.ts`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/controller/useAgentGUIConversationRailQuery.ts`
- Modify: `packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUIConversationRailItem.tsx`
**Steps:**
1. Add failing tests for task, session, app, file, and Agent title markers, plus explicit browser-element exclusion.
2. Project only the first user display prompt per session through a stable selector; assistant streaming must retain the same selector result identity.
3. Replace full `messagesBySessionId` subscriptions and per-session message sorting with the narrow prompt projection.
4. Keep browser-element mentions as registered cards in composer, timeline, and detail/workbench-header title presentation; remove them only from the conversation rail so it shows the remaining conversation text, and preserve the stored canonical title and explicit renames.
5. Run the focused title/list/Rail tests, package typecheck, runtime/renderer boundary checks, and `pnpm check:changed`.
@@ -0,0 +1,62 @@
# Agent Tool Sidebar Header Ownership Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Let the shared Agent tool sidebar participate in either a native Electron window header or a host-owned Workbench header without creating a second header or duplicating control rules.
**Architecture:** `@tutti-os/agent-gui` remains the single owner of the tool sidebar and its interactive header boundaries. `AgentToolSidebarHeaderContract` selects exactly one Header owner (`window` or `host`) and independently declares whether that Header overlays the body or is stacked above it. The sidebar publishes structured actions, open state, and layout width through `AgentToolSidebarHeaderLayout`; the selected owner renders those actions in its one existing Header. Blank host-header gestures bubble to the host while interactive controls stop propagation.
**Tech Stack:** React 19, TypeScript, Vitest, Testing Library, pnpm fixed npm release cohort.
---
### Task 1: Add the shared Header ownership contract
**Files:**
- Modify: `packages/agent/gui/workbench/tool-sidebar/AgentToolSidebar.tsx`
- Test: `packages/agent/gui/workbench/tool-sidebar/AgentToolSidebar.test.tsx`
1. Add a failing test proving host ownership creates no native window wrapper or panel Header, lets blank-header pointer and double-click events bubble, and stops control gestures.
2. Run `pnpm --filter @tutti-os/agent-gui test -- AgentToolSidebar.test.tsx` and confirm the new contract is missing.
3. Add `AgentToolSidebarHeaderContract` and `AgentToolSidebarHeaderLayout`.
4. Make ownership and body layout explicit at every consumer: Tutti Desktop selects `window + overlay`; TSH selects `host + overlay`.
5. Run the focused package test and `pnpm --filter @tutti-os/agent-gui typecheck`.
### Task 2: Validate and publish the Tutti cohort
**Files:**
- Modify only release-generated manifests during the official release workflow; do not hand-edit package versions.
1. Run `pnpm check:changed` and `pnpm release:pack:check`.
2. Commit with DCO sign-off and open a focused PR from `fix/agent-tool-sidebar-host-drag`.
3. Merge the PR, then dispatch the stable package release workflow on `main`.
4. Confirm every fixed-group package exists at the new shared version before downstream changes.
### Task 3: Integrate the TSH host Header
**Files:**
- Modify: `apps/tsh-desktop/src/app/renderer/features/workspace-agent/ui/agentToolSidebar/TshAgentToolSidebar.tsx`
- Modify: `apps/tsh-desktop/src/app/renderer/features/workspace-workbench/ui/TshAgentGuiWorkbenchHeader.tsx`
- Modify/Test: focused Workbench and Agent tool-sidebar specs
- Delete: the old accessory-only registration bridge
1. Register the structured sidebar Header layout by Agent node ID.
2. Render that layout through the existing `TshAgentGuiWorkbenchHeader`.
3. Remove panel Header placement and the old accessory-only bridge.
4. Run focused Vitest suites and `pnpm --dir apps/tsh-desktop check`.
### Task 4: Upgrade and assess the dependency cohort
**Files:**
- Modify with package-manager commands: TSH manifests and `pnpm-lock.yaml`
- Modify with Go tooling when the release cohort changes selected Go modules: `go.mod`, `go.sum`
- Modify: `.github/tutti-dependency-assessment.json`
1. Upgrade all `@tutti-os/*` and Tutti Go module cohort members to the same stable release.
2. Record whether sibling `tsh-server` can keep its current cohort because this change is renderer-only.
3. Run `pnpm check:tutti-dependencies`, `pnpm check:tutti-dependencies:graph`, `pnpm test:tutti-dependencies`, and the relevant desktop checks.
4. Confirm development HMR is sufficient; packaged builds require an application restart but no desktopd/VM restart.
@@ -0,0 +1,50 @@
# Workspace App Tabs Design
## Goal
Present the app catalog and every opened workspace app inside one application
window with a browser-like tab strip.
## Interaction
- The app catalog is the permanent first tab and cannot be closed
- Opening an app creates a tab after the catalog tab and selects it
- Opening an app that already has a tab selects the existing tab
- The add button selects the catalog tab so the user can open another app
- Closing the active app tab selects the adjacent app tab, or the catalog when
no app tabs remain
- Closing an inactive app tab preserves the current selection
- App tabs display the localized app name and app icon when available
## Architecture
The app-center view state remains the source of truth. It gains an ordered list
of open app ids alongside the existing active app id, so the tab session is
included in workbench snapshots. Normal workspace launches will target the
singleton app-center node instead of creating one workbench window per app.
The existing inline app body already keeps multiple app webviews mounted and
only exposes the active one. It will read the persisted tab list directly and
unmount a webview when its tab closes. The current standalone-agent presenter
and the normal workbench presenter will share the same view-state semantics.
The tab strip will use existing UI System buttons, icons, semantic tokens, and
the dimensions already established by Browser Node. This is app-center chrome,
not a second business state store.
## Compatibility And Errors
Restored view state without an app-id list is normalized by deriving a list
from the legacy active app id. Invalid, blank, and duplicate ids are removed.
If app preparation fails, the presenter restores the tab state from before the
attempt. Existing standalone and legacy app-webview definitions remain readable
so older workbench snapshots do not become invalid.
## Verification
- Unit-test view-state normalization, deduplication, equality, selection, close
fallback, and launch rollback
- Unit-test that normal workspace launches focus the singleton app-center node
- Run app-center and desktop targeted tests
- Run i18n, UI, renderer boundary, TypeScript, and desktop build checks
- Hot-update the desktop development environment once after verification
+100
View File
@@ -0,0 +1,100 @@
# Workspace App Tabs Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add browser-like multi-tab navigation to the workspace application window.
**Architecture:** Persist ordered app tabs in `WorkspaceAppCenterViewState`, route app launches into the singleton app-center node, and render all open inline app webviews beneath one tab strip. Keep host orchestration in desktop services and reusable view-state normalization in the workspace app-center package.
**Tech Stack:** React 19, TypeScript, Valtio, `@tutti-os/ui-system`, `@tutti-os/workbench-surface`, Node test runner.
---
### Task 1: Persist application tabs
**Files:**
- Modify: `packages/workspace/app-center/src/contracts/host.ts`
- Modify: `packages/workspace/app-center/src/core/appCenterControllerHelpers.ts`
- Test: `packages/workspace/app-center/src/core/appCenterController.test.ts`
**Steps:**
1. Add a failing test for normalized, deduplicated `openAppIds` and legacy
`openAppId` restoration.
2. Run `pnpm --filter @tutti-os/workspace-app-center test` and confirm failure.
3. Add `openAppIds` to the view-state contract, normalization, and equality.
4. Rerun the package test and confirm it passes.
### Task 2: Define tab selection and close behavior
**Files:**
- Create: `apps/desktop/src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterTabs.ts`
- Test: `apps/desktop/src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterTabs.test.ts`
**Steps:**
1. Add failing tests for opening an existing/new app and closing active/inactive
app tabs.
2. Run the targeted desktop test and confirm failure.
3. Implement pure tab-state helpers.
4. Rerun the targeted test and confirm it passes.
### Task 3: Route launches into the application window
**Files:**
- Modify: `apps/desktop/src/renderer/src/features/workspace-workbench/services/workbenchWorkspaceAppSurfacePresenter.ts`
- Modify: `apps/desktop/src/renderer/src/features/workspace-workbench/ui/useWorkspaceWorkbenchShellRuntime.tsx`
- Modify: `apps/desktop/src/renderer/src/features/workspace-workbench/services/standaloneAgentWorkspaceAppSurfacePresenter.ts`
- Test: `apps/desktop/src/renderer/src/features/workspace-workbench/services/workbenchWorkspaceAppSurfacePresenter.test.ts`
- Test: `apps/desktop/src/renderer/src/features/workspace-workbench/services/standaloneAgentWorkspaceAppSurfacePresenter.test.ts`
**Steps:**
1. Replace the presenter expectations with tests for tab state, singleton
app-center launch/focus, close, and rollback.
2. Run the targeted tests and confirm failure.
3. Update both presenters to use the shared tab-state helpers.
4. Register the normal presenter with app-center view-state access.
5. Rerun the targeted tests and confirm they pass.
### Task 4: Render browser-like app tabs
**Files:**
- Modify: `apps/desktop/src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterContribution.tsx`
- Modify: `apps/desktop/src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterInlineAppBody.tsx`
- Modify: `apps/desktop/src/shared/i18n/locales/en.ts`
- Modify: `apps/desktop/src/shared/i18n/locales/zh-CN.ts`
- Test: `apps/desktop/src/renderer/src/features/workspace-app-center/services/internal/workspaceAppCenterInlineAppRetention.test.ts`
**Steps:**
1. Add failing source/behavior tests for the fixed catalog tab, application
tabs, close buttons, and add action.
2. Run the targeted desktop test and confirm failure.
3. Replace the back button header with the tab strip using UI System primitives
and semantic tokens.
4. Render inline app bodies from persisted open tabs and unmount closed tabs.
5. Add localized accessibility labels and rerun the tests.
### Task 5: Verify and update the running desktop
**Files:**
- Review: `docs/conventions/desktop-visual-language.md`
- Review: `docs/conventions/troubleshooting/README.md`
**Steps:**
1. Run `pnpm --filter @tutti-os/workspace-app-center test`.
2. Run targeted desktop tests for the changed presenters and app-center UI.
3. Run `pnpm check:i18n`, `pnpm check:ui-boundaries`, and
`pnpm check:renderer-boundaries`.
4. Run `pnpm lint:ts`, `pnpm typecheck`, and
`pnpm --filter @tutti-os/desktop build`.
5. Check the final diff for durable documentation impact.
6. Trigger exactly one desktop hot update if the running environment supports
it; otherwise report that a full dev-process restart is required.
@@ -0,0 +1,27 @@
# Agent Header Read-Only Session Actions
## Goal
Let read-only host surfaces reuse the complete `AgentGuiWorkbenchHeader` while exposing only the session actions they can support. TSH Activity Center is the first consumer: it needs the canonical session title, Agent icon, copy-as-Markdown, copy-as-reference, and its host-owned close action, but it must not offer rename.
## Design
- `AgentGuiWorkbenchHeader` accepts an optional `sessionMenuActions` list.
- Omitting the list preserves the existing rename and copy menu.
- The shared menu renders only declared actions and emits separators only between visible action groups.
- The existing `agent-conversation` entrypoint exports the pure transcript serializer so external read-only conversation surfaces can preserve AgentGUI's Markdown format without copying implementation.
- Hosts retain ownership of canonical session/message loading, clipboard access, toasts, and window actions.
- Workbench window-chrome selectors recognize the Header only when it occupies that window's header slot, so a complete Header embedded in another window's body cannot reclassify the outer window.
## Invariants
1. Existing AgentGUI headers keep their current menu when no capability list is supplied.
2. A read-only host cannot expose rename when it declares only copy actions.
3. Session identity comes from the host's canonical session projection; menu availability never changes the title.
4. A nested Header does not alter its containing host window's rows, drag layer, or body offsets.
## Verification
- Shared menu component tests for default and copy-only action sets.
- Agent GUI package typecheck and packed-surface validation.
- TSH Activity Center tests for shared title, menu contents, Markdown copy, reference copy, and close behavior.
@@ -0,0 +1,408 @@
# AgentGUI TanStack Virtual Upgrade Plan
Status: implemented on 2026-07-25
## Goal
Reduce AgentGUI transcript scroll and layout work by upgrading TanStack Virtual
and letting it own the chat-list mechanics it already implements.
Keep Turn-level virtualization in the first implementation. Row-level
virtualization is a later step only if traces still show that one oversized
active Turn is the main bottleneck.
## Current evidence
- `@tutti-os/agent-gui` declares `@tanstack/react-virtual: ^3.13.12`.
- The lockfile currently resolves `@tanstack/react-virtual` 3.13.26 and
`@tanstack/virtual-core` 3.16.0.
- The current virtualizer counts transcript Turn groups, uses stable Turn-group
keys, dynamically measures mounted groups, anchors to the end outside Turn
disclosure motion, and uses six Turn groups of overscan.
- `followOnAppend`, `isAtEnd`, `getDistanceFromEnd`, `scrollToEnd`,
`scrollToIndex`, `rangeExtractor`, `initialMeasurementsCache`, and
`takeSnapshot` already exist in the resolved core version.
- `AgentTranscriptView` does not enable `followOnAppend`.
- `useAgentGUIDetailScroll` separately owns bottom following, content-resize
bottom correction, and `scrollHeight`-delta prepend compensation.
- The older-message loading indicator is a normal-flow sibling before
`AgentConversationFlow`. While it is visible, the virtual list does not start
at the scroll element's origin.
- The transcript virtualizer and `useAgentGUIDetailScroll` can therefore react
to the same content-size change. This split ownership is the main
simplification target.
- The latest packages checked on 2026-07-25 are
`@tanstack/react-virtual` 3.14.8 and `@tanstack/virtual-core` 3.17.6.
React Virtual 3.14.8 depends on Virtual Core 3.17.6.
- The newer releases add eager prepend reconciliation, safer dynamic-size
adjustment, cached remeasurement, iOS momentum handling, and React
`directDomUpdates`.
The original Browser-behind-AgentGUI visual tearing report is not a proven
TanStack bug. Renderer pressure and Electron webview stacking remain separate
candidate mechanisms.
## Decisions
### 1. Upgrade before changing virtualization granularity
Upgrade:
```text
@tanstack/react-virtual 3.13.26 -> 3.14.8
@tanstack/virtual-core 3.16.0 -> 3.17.6 (transitive)
```
Update the manifest minimum to `^3.14.8` so the declared contract includes the
React APIs used by the implementation.
Do not add a direct `@tanstack/virtual-core` dependency. AgentGUI consumes the
React adapter, and its exact core dependency supplies the matching core.
### 2. Keep Turn-level virtual items initially
The first implementation keeps:
```text
virtual item = presentation Turn group + its Turn attachments
```
This preserves:
- completed Turn disclosure and auto-collapse;
- canonical Turn versus presentation-group separation;
- `goal-control` rows with `turnId: null`;
- current locator and attachment group indexes;
- current spacing, dividers, participant headers, and CSS grouping.
The change reduces scroll coordination and React/layout overhead. It does not
bound the DOM inside one mounted oversized running Turn.
### 3. Give each scroll behavior one owner
When Turn virtualization is active, TanStack owns item-list geometry changes.
When virtualization is inactive, `useAgentGUIDetailScroll` keeps the current
native DOM behavior.
| Behavior | Virtualized owner | Non-virtual owner |
| ------------------------------------------- | -------------------------------------------------- | ------------------------- |
| New Turn appended while already at the end | `followOnAppend` | `useAgentGUIDetailScroll` |
| Last Turn grows while streaming | `anchorTo: "end"` plus `measureElement` | content `ResizeObserver` |
| Older Turns prepended | stable keys, `scrollMargin`, and `anchorTo: "end"` | `scrollHeight` delta |
| Determine whether latest content is visible | `isAtEnd()` | DOM geometry |
| User requests “scroll to bottom” | `scrollToEnd()` | native smooth scroll |
| Locate an offscreen Turn or attachment | `scrollToIndex()` | DOM lookup |
| Trigger older-history loading near the top | AgentGUI | AgentGUI |
| Detect wheel, keyboard, or pointer intent | AgentGUI | AgentGUI |
| Conversation selection and identity fencing | AgentGUI | AgentGUI |
| Bottom-dock safe-area measurement | AgentGUI | AgentGUI |
AgentGUI must not apply its manual prepend delta or content-resize bottom write
after TanStack has accepted ownership of the virtualized branch.
### 4. Expose a narrow virtual-scroll controller
`AgentTranscriptView` owns the TanStack instance. The detail pane still owns
the product-level scroll button, loading trigger, conversation switch, and
native non-virtual branch.
Pass a ref through:
```text
AgentGUIDetailPane
-> AgentGUIDetailTimeline
-> AgentGUIConversationTimelinePane
-> AgentConversationFlow
-> AgentTranscriptView
```
The ref exposes only presentation scrolling:
```ts
interface AgentTranscriptVirtualScrollController {
agentSessionId: string;
enabled: boolean;
isAtEnd(): boolean;
scrollToEnd(options?: { behavior?: ScrollBehavior }): void;
}
```
`scrollToIndex` remains internal to `AgentTranscriptView`, because locator and
attachment indexes belong to the transcript projection.
Every controller use must check the exact `agentSessionId`. A controller from
the previous Session must not move the newly selected timeline.
TanStack measurement keys must also include
`conversation.sourceDetail.session.agentSessionId`. A Turn-group key is stable
inside one Session, but it is not a repository-wide Session identity. Prefixing
it prevents one Session from reading another Session's cached height.
### 5. Use the chat parameters deliberately
Target virtualizer configuration:
```ts
useVirtualizer({
count: turnGroups.length,
getScrollElement: () => virtualScrollElement,
getItemKey: (index) =>
`${agentSessionId}\u0000${turnGroups[index]?.key ?? index}`,
estimateSize: () => AGENT_TRANSCRIPT_ESTIMATED_TURN_HEIGHT_PX,
overscan: AGENT_TRANSCRIPT_VIRTUALIZATION_OVERSCAN,
scrollMargin: virtualListOffsetFromScrollOrigin,
anchorTo: hasMovingTurnDisclosure ? "start" : "end",
followOnAppend: hasMovingTurnDisclosure ? false : true,
scrollEndThreshold: 24,
directDomUpdates: true,
directDomUpdatesMode: "transform",
useFlushSync: true
});
```
Rules:
- `followOnAppend` handles only a count increase with a new last key while the
previous list was at the end.
- Memoize the Session-prefixed `getItemKey` callback so unrelated renders do
not invalidate measurement work.
- Measure `scrollMargin` from the timeline scroll origin to the virtual
container. Recompute it when the older-message loading indicator appears or
disappears. Direct DOM transform mode subtracts this option when positioning
items.
- Before direct DOM updates are enabled, AgentGUI's JSX transform must subtract
the same `scrollMargin`. Task 3 removes that JSX transform when TanStack takes
over item positioning.
- Do not remove manual prepend compensation until the measured `scrollMargin`
path is active. Otherwise the external loading indicator can move every
rendered Turn without TanStack knowing that the list origin moved.
- Streaming growth of the existing last Turn is handled by end anchoring and
measurement, not `followOnAppend`.
- During disclosure motion, keep start anchoring, disable append following,
keep the existing pinned-row behavior, and suppress TanStack size-change
correction.
- Keep `useFlushSync: true` for the first implementation. Test
`useFlushSync: false` separately only if a trace still identifies synchronous
React commits as a material cost.
- Do not enable `useAnimationFrameWithResizeObserver` without trace evidence;
it intentionally delays measurement by one frame.
- Do not enable `useCachedMeasurements` unless AgentGUI keeps the same
transcript mounted but CSS-hidden. Conversation replacement is not that
case.
- Do not add snapshot restoration. The current product contract opens a newly
selected conversation at the latest content.
- Do not add a custom `rangeExtractor` until a real offscreen item must remain
mounted.
### 6. Use direct DOM updates only with its required markup
With `directDomUpdates: true`:
- attach `rowVirtualizer.containerRef` to the inner virtual-size container;
- remove its JSX `height: rowVirtualizer.getTotalSize()` value;
- keep each virtual item `position: absolute`, `top: 0`, and `left: 0`;
- remove each item's JSX main-axis `transform`;
- keep `measureElement` and `data-index`;
- keep cross-axis width and Turn-specific bottom padding in AgentGUI.
The existing CSS already satisfies the absolute-position requirements.
Transform mode creates a stacking context and usually a compositor layer per
item. Because the reported visual symptom involves an Electron webview, landing
this option requires both trace evidence and a real-pixel/webview-order check.
If transform mode reproduces the visual fault, compare the upgraded virtualizer
without direct DOM updates before considering position mode. Do not ship an
unmeasured mode switch.
## What stays in AgentGUI
TanStack does not understand AgentGUI domain semantics. These remain
application-owned:
- transcript row and Turn-group projection;
- canonical Turn identity and presentation grouping;
- `goal-control` rows without Turn ownership;
- Turn disclosure and completion-time collapse;
- user-message locator and Turn-attachment index mapping;
- exact Session identity and stale-controller fencing;
- older-page request state and top-prefetch policy;
- participant headers, dividers, spacing, and attachment placement;
- tool-group disclosure and its inner DOM;
- Browser webview visibility, stacking, and pixel correctness.
## Implementation tasks
### Task 1: Upgrade and lock the dependency
Files:
- `packages/agent/gui/package.json`
- `pnpm-lock.yaml`
Steps:
1. Update `@tanstack/react-virtual` to `^3.14.8`.
2. Install with the repository package manager.
3. Confirm the lockfile resolves React Virtual 3.14.8 and Core 3.17.6.
4. Run the focused AgentGUI typecheck before behavior changes.
### Task 2: Add the virtual-scroll controller and behavior delegation
Files:
- `packages/agent/gui/shared/agentConversation/components/AgentTranscriptView.tsx`
- `packages/agent/gui/shared/agentConversation/components/AgentConversationFlow.tsx`
- `packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUIConversationTimelinePane.tsx`
- `packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUIDetailTimeline.tsx`
- `packages/agent/gui/agent-gui/agentGuiNode/view/AgentGUIDetailPane.tsx`
- `packages/agent/gui/agent-gui/agentGuiNode/view/useAgentGUIDetailScroll.ts`
Steps:
1. Add the exact-conversation virtual-scroll controller.
2. Prefix TanStack item keys with the exact Agent Session id.
3. Pass older-message loading state as a virtual-list layout revision and
measure the list's `scrollMargin`.
4. Subtract `scrollMargin` in the existing JSX transform.
5. Enable `followOnAppend` outside disclosure motion.
6. Route virtualized at-end reads and explicit bottom requests through TanStack.
7. Disable the outer content-resize bottom write for virtualized transcripts.
8. Disable manual prepend-delta restoration for virtualized transcripts only
after `scrollMargin` is active.
9. Preserve the native branch unchanged for short transcripts.
### Task 3: Enable direct DOM updates
Files:
- `packages/agent/gui/shared/agentConversation/components/AgentTranscriptView.tsx`
- `packages/agent/gui/app/renderer/agentactivity.css`
Steps:
1. Attach `containerRef`.
2. Remove React-owned virtual container height and item transform.
3. Keep disclosure spacing, measurement, and locator attributes.
4. Confirm the option remains fixed for the virtualizer's mount lifetime.
### Task 4: Add and update behavior coverage
Files:
- `packages/agent/gui/shared/agentConversation/components/AgentTranscriptView.virtual.spec.tsx`
- `packages/agent/gui/agent-gui/agentGuiNode/view/useAgentGUIDetailScroll.spec.tsx`
- relevant detail timeline render-budget specs
Required cases:
1. A new Turn follows only when the previous viewport was at the end.
2. Streaming growth remains bottom-locked at the end.
3. Wheel, keyboard, and pointer scroll-away intent prevents following.
4. A submitted prompt explicitly returns to the end.
5. Older-history prepend preserves the visible Turn without a manual DOM delta.
6. Showing and hiding the older-message loading indicator does not move the
visible Turn.
7. Disclosure motion keeps its pinned position and does not follow appends.
8. The scroll-to-bottom button uses TanStack only for the matching Session.
9. Switching Sessions cannot reuse the previous Session's measurement keys.
10. Locator and attachment scrolling still reach offscreen Turn groups.
11. Direct DOM mode owns container height and item transforms.
### Task 5: Capture before/after performance evidence
Use the same database snapshot and viewport for both captures.
Run:
- `virtualized-streaming`;
- `virtualized-scroll-locator`;
- repeated long/short Session switching;
- an oversized live-Turn case matching approximately 19 Turns, 250 tool calls,
and at least 40 tool calls in the active Turn.
If the oversized live-Turn case becomes a named performance scenario, add it to
`tools/scripts/agent-gui-layout-performance-scenarios.mjs`, its registry tests,
and `docs/conventions/testing.md`.
Compare:
- React commit and render work;
- `Layout` and `UpdateLayoutTree`;
- scroll event duration and long tasks;
- mounted Turn and row counts;
- viewport jumps during prepend, append, disclosure, and Session switch.
Also reproduce with a Browser node mounted behind AgentGUI and inspect real
pixels. DOM count or trace improvement alone does not prove the original visual
tearing is fixed.
## Acceptance gates
The first implementation may land when:
- all required behavior cases pass;
- virtualized prepend and loading-indicator transitions have no manual
`scrollHeight` compensation;
- a user reading history is never pulled to the end by streaming or append;
- Session switching never shows or scrolls the previous Session;
- existing disclosure, locator, and attachment behavior is unchanged;
- `virtualized-streaming` and `virtualized-scroll-locator` do not regress;
- the chosen direct DOM mode improves or preserves measured trace results;
- Browser-behind-AgentGUI pixel validation shows no new stacking fault.
If traces still show one oversized active Turn dominating render or DOM work,
continue with stable render-unit virtualization. Do not use the current
complexity score as a split threshold: it can be high even when the expensive
content is one indivisible tool-group row.
## Implementation evidence
- The manifest resolves React Virtual 3.14.8 and Virtual Core 3.17.6.
- AgentGUI package typecheck and all 2,341 package tests pass.
- `virtualized-scroll-locator` passes all 24 gates. The locator index moves
monotonically from 14 to 0 with zero reverse or return transitions; measured
`Layout` is 31.58 ms and `UpdateLayoutTree` is 348.77 ms.
- `virtualized-streaming` remains within the same observed range as the source
snapshot: 2,258.26 ms after the change versus 2,212 ms before it. This is
preservation evidence, not a broad speedup claim.
- `virtualized-session-cycle` passes two long/short round trips and observes
`native → virtual → native → virtual`.
- `virtualized-oversized-active-turn` reaches 19 Turns and 250 tool calls while
keeping only 9 virtual Turns mounted. Its 1,118 `AgentGUINode` markers show
that stable render-unit virtualization has real evidence for a separate
follow-up, but it remains outside this upgrade.
- `browser-behind-agent-gui-pixels` captures the high-contrast Browser fixture
first, then the fullscreen AgentGUI covering that still-mounted webview. The
composited screenshot has no visible fixture-color leakage.
## Deferred render-unit virtualization
The deferred design uses one pure projection that emits stable render units and
all indexes consumed by locators and attachments:
```ts
interface AgentTranscriptRenderUnitProjection {
units: readonly AgentTranscriptRenderUnit[];
unitIndexByRowIndex: ReadonlyMap<number, number>;
unitIndexByRenderKey: ReadonlyMap<string, number>;
anchorUnitIndexByPresentationGroup: ReadonlyMap<string, number>;
lastUnitIndexByCanonicalTurnId: ReadonlyMap<string, number>;
unitIndexByAttachmentId: ReadonlyMap<string, number>;
}
```
The active Turn is split from the start. A settled collapsed Turn remains one
unit. A stable group-anchor key survives active-to-settled presentation so the
visible anchor is not deleted during collapse.
This phase requires its own design review and performance evidence. It is not
part of the TanStack upgrade.
## References
- [TanStack Virtual chat guide](https://tanstack.com/virtual/latest/docs/chat)
- [TanStack Virtualizer API](https://tanstack.com/virtual/latest/docs/api/virtualizer)
- [TanStack Virtual performance and iOS notes](https://tanstack.com/blog/tanstack-virtual-perf-and-ios)
- [TanStack eager prepend reconciliation](https://github.com/TanStack/virtual/pull/1176)
- [TanStack React Virtual source](https://github.com/TanStack/virtual/blob/main/packages/react-virtual/src/index.tsx)
- [TanStack Virtual Core source](https://github.com/TanStack/virtual/blob/main/packages/virtual-core/src/index.ts)
@@ -0,0 +1,178 @@
# Mobile Agent DI Data Layer
Status: implemented; physical-device visual review pending
## Goal
Move the Mobile Agent MVP's account, device, workspace, session, message, and
composer behavior out of React screens. Keep Agent Activity semantics aligned
with Desktop while allowing Native-specific presentation and interaction.
The first conversation rail alignment is now implemented on top of the same
section membership and canonical summary semantics as Agent GUI.
The first conversation-flow alignment now consumes the same canonical
AgentGUI transcript projection. Mobile keeps its Native renderer, scroll
follow behavior, and local disclosure state, while AgentGUI owns message
merging, thinking, tool activity, processing, notices, and turn summaries.
## Confirmed boundaries
- Desktop and Mobile are sibling applications. Neither imports source from the
other application.
- Shared canonical session state, reducers, selectors, and command semantics
live in `@tutti-os/agent-activity-core`.
- Generated tuttid DTO-to-activity mapping lives in the narrow
`@tutti-os/agent-activity-tuttid-adapter` package and is consumed by both
applications.
- Application DI wrappers own account state, DeviceLink, transport creation,
logging, navigation preferences, polling/event wiring, and lifecycle.
- Mobile `services/**` are `.ts` files and do not import React or hooks.
- React binding components use `useSyncExternalStore`. They do not copy service
snapshots into page state.
- `useEffect` remains allowed only for presentation effects such as focus,
keyboard, scroll, animation, layout, and resetting component-local input.
## Scope model
```text
Bootstrap container
├─ Unauthenticated child
│ └─ LoginService
└─ Authenticated child (mutually exclusive with Unauthenticated)
├─ immutable AccountSession
├─ DeviceService
├─ one DeviceLink-backed TuttidClient
├─ AgentDirectoryService
├─ WorkspaceCatalogService
└─ one active Workspace child
├─ WorkspaceActivityService + one AgentSessionEngine
├─ WorkspaceConversationRailService
├─ WorkspaceNavigationService
└─ ComposerDraftService
```
Only one authenticated account, connected device, and active workspace are
retained. Account changes replace the complete authenticated subtree. Workspace
changes dispose the old child before initializing and publishing the candidate.
Sibling application scopes stay isolated; reusable implementation belongs in a
package rather than a cross-application import.
Mobile keeps dynamic account, DeviceLink, and workspace inputs explicit in its
composition root, registers the resulting services in the matching child
container, and lets that container be their sole disposal owner. This preserves
the Canvas-style child-scope lifetime and sibling isolation without requiring
React bindings or decorator syntax in service modules.
## Data and command flow
- `WorkspaceConversationRailService` owns section queries, pagination, polling,
cursors, totals, and project labels. It uses the server's exact
`railSectionKey` membership and never infers projects from `cwd`.
- Rail Session DTOs and message pages are mapped into canonical activity
entities and dispatched into one workspace `AgentSessionEngine`. The Rail
snapshot retains only membership, ordered ids, cursors, totals, and request
state; Session DTOs are transient input rather than a second entity cache.
- Mobile consumes the DOM-free
`@tutti-os/agent-gui/conversation-rail-projection` entry for the same title,
provider, status, attention, pin, and sort semantics as Agent GUI, then
renders a Native-specific drawer. Its local disclosure state may collapse a
section, while loading/error/retry state comes from the Rail service rather
than from a second list cache.
- Mobile consumes the DOM-free
`@tutti-os/agent-gui/conversation-projection` entry for the selected
Session's canonical conversation VM. Pending Interactions stay Engine facts;
the Native renderer does not infer actionability from a transcript row.
- Mobile consumes the DOM-free
`@tutti-os/agent-gui/composer-projection` entry for Composer support and
presented settings. Composer options load through the exact Agent Target's
Engine command; the shared activity-tuttid adapter maps the daemon response
once. Existing sessions update settings through the Engine, while a new
session retains target-scoped draft settings in `ComposerDraftService` and
carries them on its activation intent.
- Create, send, stop, pin, delete, and Interaction response enter the Engine as
intents. The Mobile command port performs generated-client calls and returns
mapped command results to the Engine. Rename uses the generated client, then
immediately upserts the returned canonical Session and reconciles Rail
membership.
- A workspace-scoped DeviceLink `agent_live` stream is the foreground update
lane. Its Go Subscriber validates revision, identity, epoch, and sequence
continuity before Mobile applies `message_delta` through the activity-core
optimistic overlay or schedules canonical reconciliation.
- Mobile keeps that lane in a dedicated service and Android executor rather
than mixing its retry/overlay lifecycle into workspace orchestration or
consuming a general DeviceLink request worker. The Mobile Android host owns
the composite DeviceLink plus Agent Subscriber AAR assembly.
- Session polling remains single-flight at two seconds and selected-session
message polling remains single-flight at one second only while the live lane
is disconnected. `stream_ready` disables both pollers; disconnect restores
them while Mobile retries the long stream.
- Initial messages use the Desktop-aligned newest 100 descending page and the
server `hasMore` boundary. Incremental reads use `afterVersion`; older history
uses `beforeVersion`.
- Active conversation identity is owned by
`WorkspaceNavigationService`. Per-session/new-session drafts are process-only
data owned by `ComposerDraftService`.
- Services expose structured error codes. Native views map those codes through
Mobile i18n.
## Native UI foundation
- The Mobile UI is a Native renderer for the same canonical data, not a DOM
implementation of existing Desktop components.
- `@tutti-os/ui-system` remains the semantic-token and component-contract
owner. Its experimental Native entry owns reusable React Native primitives; Mobile
owns screen composition and platform-specific interaction.
- `src/tokens/renderer-theme.json` is the single semantic manifest for the
shared Web/Native token subset. It generates Web CSS renderer variables and
Native light/dark palettes, while allowing renderer-specific literal values
where their visual language or color syntax differs. `MobileUIProviders`
resolves the operating-system scheme and composition consumes
`useNativeTheme()`. Full cross-renderer visual-token parity remains deferred
work.
- The bootstrap spike establishes NativeWind 4/Tailwind 3, the Gesture Handler
root, Reanimated Worklets, a Bottom Sheet modal provider, and an RN Primitives
portal host. The first experimental Native Button, IconButton, ListRow, and
Sheet primitives are now promoted into the UI System.
- React Native Reusables is the source-copy starting point for primitives. A
component is adapted and promoted into the UI System Native layer before an
app imports it. `@gorhom/bottom-sheet` remains a dependency for complex
sheets and is wrapped only once it is a reusable product pattern.
- Native primitives use the Mobile development gallery as their target-renderer
preview surface; the DOM storyboard cannot provide valid visual evidence for
React Native components.
## Lifecycle
- Backgrounding immediately pauses pairing work, polling, commands, and runtime
availability.
- The active workspace child and DeviceLink are retained for a 15-second grace
window.
- Foregrounding inside the window resumes and requests authoritative
reconciliation.
- Expiry disposes the workspace child, closes DeviceLink, and returns to device
selection.
## Validation
- Mobile TypeScript typecheck and Jest suite.
- Shared adapter and Desktop TypeScript typechecks.
- Container transition and background-grace tests.
- Navigation/draft ownership tests.
- Lightweight `useSyncExternalStore` binding test.
- Final changed-aware repository validation.
## Deferred follow-ups
- Search and project-level Rail actions.
- Agent GUI-equivalent optimistic Rail rows before a newly created Session is
confirmed by the canonical section read.
- Physical-device visual and accessibility review of the Native Rail.
- Device/workspace/session navigation preference persistence after a generic
Mobile preference port exists.
- Relay fallback and direct/Relay live-stream handoff.
- Richer ambiguous-delivery diagnostics beyond the current reconcile-first,
exact-identity Retry state.
- Remaining rich conversation parity after Markdown/code, canonical prompt
actions, media attachments, processing, and basic tool grouping: richer tool
detail presentation plus host capabilities for file/app/issue links.
@@ -0,0 +1,169 @@
# 跨平台文件 Picker 搜索与 @ 面板导航改造方案
> 状态:已确认范围,代码已实现、待最终验收。仓库:tutti(本次没有拆分出的第二个仓库)。
> Windows Search 已在当前 Windows 开发机验证;目标发行环境与 macOS 权限/索引服务仍需实机确认。
## 结论
统一解决三个用户可见问题:Windows 与 macOS 文件搜索不再走 renderer/daemon 的全量递归扫描,Windows 隐藏“最近访问”入口,并修复 @ 面板点击文件夹箭头后无法进入下一层的问题。
## 1. 背景与目标
工作区文件管理器和 rich-text/AgentGUI 的 @ 引用面板,最终都依赖 tuttid 的 /files/search、/files/directory 和 /files/recent。
目标:
- macOS、Windows 的关键词搜索和文件类型筛选统一走后端 Search,再由平台 Adapter 调用系统索引。
- 正常产品路径退出 os.ReadDir 递归搜索;不新增旧扫描器 fallback。索引不可用时快速失败并留下可观测错误,具体 UI 文案待确认。
- Windows 不生成 local:recent location;已有持久化选择自动回到仍存在的位置。
- Agent @ 面板目录箭头和键盘层级导航共用状态更新;从搜索结果进入目录时,编辑器查询词与目录浏览状态同步切换。
- 不改变公开 HTTP 请求/响应结构,不增加 preload 通道。
## 2. 当前架构和完整链路
### 文件管理器
用户输入关键词/筛选
-> workspace-file-manager Session.search
-> DesktopWorkspaceFileReferenceAdapter.searchReferences
-> TuttidClient.searchWorkspaceFiles
-> daemon SearchWorkspaceFiles
-> workspace Service.Search
-> LocalFilesAdapter.Search
-> os.ReadDir 队列递归 + 候选上限/deadline/忽略目录
-> ScoreSearchCandidates 或 BuildListingEntries
-> renderer 展示结果
SearchInput 已支持 query、filters、includeKinds、within 和 limit,但 LocalFilesAdapter.Search 直接遍历物理目录,结果受遍历顺序、候选上限和 deadline 影响。
### @ 面板
输入 @ / 选择位置 / 选择筛选
-> ReferenceSourcePickerController
-> ReferenceSourceAggregator.search
-> workspace file location source
-> DesktopWorkspaceFileReferenceAdapter
-> 上面的 /files/search 链路
Agent Composer 输入 `@关键词`
-> agentFileMentionExtension 生成 suggestion
-> useComposerPresentation 调用 AgentMentionSearchController.updateQuery
-> 文件 Provider 通过既有 workspace file reference Search 返回文件和文件夹
-> AgentFileMentionPalette / MentionRow 渲染文件夹箭头
-> useComposerMentionActions.navigateIntoFileMentionItem
-> AgentMentionSearchController.selectFileMentionNavigationItem
已确认直接原因不是失焦:MentionRow 已阻止箭头的默认 focus/bubble,实际控制器在 `currentQuery` 非空时直接返回 `false`,所以点击回调虽然执行,却不会进入目录。若只删除该判断,编辑器仍保留 `@关键词`,下一次 suggestion 更新又会把控制器重置回搜索态,因此必须先同步清空查询词和控制器 query,再进入目录。
另一个独立问题是本地 source 没有打开 filtersUseSearch:无关键词筛选会在 picker 侧递归 listChildren 后再投影,从而重新触碰大量目录。
## 3. 目标架构和完整链路
Picker / @ 面板
-> 现有 SearchInput(query + filters + kinds + within)
-> daemon FileService / workspace Service
-> LocalFilesAdapter.Search
-> PlatformFileSearchAdapter(按 GOOS 选择)
Windows:Windows Search SystemIndex Adapter
macOS:Spotlight mdfind Adapter
其他系统:保持现有非目标能力,策略待确认
-> 统一路径/类型/隐藏项过滤 + 现有结果映射
-> SearchResult -> renderer
平台命令/API 只存在 services/tuttid/data/workspace 的 build-tag 文件。通用层只依赖窄接口:输入物理 root、within、query、kind/filter、limit,输出物理路径;kind 和隐藏项由通用层用文件元数据二次确认。索引查询失败不转回旧递归扫描;返回明确错误并记录平台、耗时、索引不可用原因。
## 4. 每个仓库、服务、模块改什么
本次只有一个 Git 仓库 tutti,相关模块如下。
| 模块/文件 | 改动 |
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| services/tuttid/data/workspace/local_files.go | LocalFilesAdapter 增加内部搜索 Provider 依赖;目录读写、recent、preview 保持不变。测试可注入 fake Provider,生产默认不能回到递归扫描。 |
| services/tuttid/data/workspace/local_files_search.go | 保留请求归一、物理路径校验、类型/隐藏项过滤、结果映射和日志;删除生产调用的递归候选收集。 |
| services/tuttid/data/workspace/local_files_search_windows.go | Windows build-tag Adapter:查询 Windows Search SystemIndex,安全传递 PowerShell 参数,处理取消、deadline、命令失败和输出解析。SQL 字段/PowerShell 版本需实机确认。 |
| services/tuttid/data/workspace/local_files_search_darwin.go | macOS build-tag Adapter:用 Spotlight mdfind 查询名称/路径,做 root containment、类型、隐藏项和 filter 过滤;复用现有 mdfind 进程调用模式。 |
| services/tuttid/data/workspace/local_files_search_other.go | 非目标平台不得偷偷使用旧递归搜索;可返回“平台搜索不可用”,Linux 策略待确认。 |
| services/tuttid/data/workspace/local_files_search_test_provider_test.go、平台测试文件 | 覆盖 Provider 输入映射、路径越界、类型/隐藏、取消、Windows SQL/URL 输出解析和错误;临时目录测试改成显式 fake Provider。 |
| apps/desktop/.../workspaceFileReferenceSource.ts | 本地 source 声明 filtersUseSearch: true,使无关键词筛选直接调用既有 Search。 |
| apps/desktop/.../workspaceFileLocationReferenceSources.ts | 项目位置和本地位置 source 都声明 filtersUseSearch: true;Recent 仍走自己的列表过滤。 |
| apps/desktop/.../desktopWorkspaceFileLocations.ts | buildLocalLocations 接收平台;Windows 排除 local:recent,macOS 保持。 |
| apps/desktop/.../workspaceFileManagerService.ts、AgentGUI host | 传递已有 platformApi.os 给 location builder,不增加 preload 字段。 |
| apps/desktop/.../workspaceFileManagerService.test.ts 与 source tests | 覆盖 Windows 无 recent、macOS 有 recent、旧 local:recent 持久化选择回退,以及两类位置均走 Search。 |
| packages/agent/gui/.../composer/useComposerMentionActions.ts | 从搜索结果进入 workspace 文件夹前,将活动的 `@关键词` 替换成 `@`,并让点击、Enter、ArrowRight 共用同一入口。 |
| packages/agent/gui/.../AgentMentionSearchController.ts | 目录浏览中忽略同 workspace/context 的重复空查询更新,避免 suggestion effect 把刚进入的目录重置到根目录。 |
| packages/agent/gui/.../AgentMentionSearchController.spec.ts | 覆盖“搜索命中文件夹 -> 清空查询 -> 进入目录 -> 重复空查询不重置”的完整状态转换。 |
| docs/plans/2026-08-12-cross-platform-file-picker-search-at-navigation.md | 固化背景、链路、边界、迁移、风险、验收和实施顺序。 |
### 接口变化
不新增公开 HTTP endpoint、请求字段、响应字段、IPC/preload 方法。SearchInput 已有 query、filters、includeKinds、within 和 limit。新增的仅是服务内部 Adapter 接口/构造依赖。
## 5. 可复用部分与 Adapter 边界
可复用:
- workspace Service.Search 的参数归一、根路径解析和错误出口;
- SearchInput、SearchEntry 及现有结果映射;
- Recent 的 macOS mdfind 超时/输出读取模式;
- Picker 的 filtersUseSearch、source registry、limit/abort 机制;
- MentionRow 的箭头视觉、阻止失焦行为和键盘导航协议。
Adapter 只负责系统索引查询、平台命令/API 生命周期、原始路径解析和平台错误。Adapter 不负责 renderer 状态、不拼接 mention://、不决定 source/tab 展示、不改变公开协议。
## 6. 明确不做
- 不保留 Windows/macOS 正常搜索的旧 os.ReadDir 递归 fallback。
- 不新增自建全文索引、数据库 schema、后台索引任务或文件内容搜索。
- 不把 Recent 的访问时间语义改成普通关键词搜索;Windows 只隐藏入口,macOS 保持现有 Spotlight Recent。
- 不重做右上角按钮/整套 Picker UI,不改变打开、预览、上传、删除能力。
- 不处理尚未确认的 Linux 索引策略,也不把未实机确认的命令可用性写成事实。
## 7. 数据迁移和兼容策略
- 无数据库、HTTP schema 或 preload 迁移。
- workspace file manager 持久化 schema 不变;Windows 读到旧 local:recent 时使用现有 location resolver 选择有效位置,具体优先顺序以现有 resolver 测试确认。
- macOS location id 保持 local:recent;Windows 新会话不产生该 id。
- 索引只覆盖系统已索引位置,结果可能少于旧递归扫描;索引覆盖范围和 UI 提示方案待确认。
## 8. 风险和回滚
| 风险 | 控制/回滚 |
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| Windows Search 未启动、位置未索引、PowerShell/AppLocker 策略拦截 | Adapter 快速报错并记录日志;发布前 Windows E2E 验证。回滚为恢复上一版本代码,不启用旧慢扫描。 |
| Spotlight 权限/索引异常 | macOS E2E 验证并保留错误日志;普通 Search 是否展示错误/空结果待确认。 |
| 系统索引路径或类型误判 | root containment、os.Stat/metadata 二次确认,覆盖空格和 Unicode 路径。 |
| 系统索引候选上限后再做隐藏项/元数据校验,可能不足 limit | 平台查询尽量下推 query/filter,再做通用层校验;记录候选/跳过数并补多目录、多类型测试。 |
| Agent @ 面板编辑器/控制器状态不同步 | 查询替换成功后同步清空 controller query;只忽略同 context 下目录浏览中的重复空查询。 |
回滚粒度:平台搜索、Windows location、Agent @ 导航、source capability 可分别 revert;公开协议无变化,不需要数据回滚。
## 9. 测试和验收标准
- Go:平台 Adapter/fake Adapter 单测、路径 containment、query/filter/kind/hidden/within/limit、取消、命令解析和错误日志。
- file-reference:query 为空且 filters 非空时调用 source search。
- desktop:Windows location 不含 local:recent,macOS 含;旧 persisted id 能落到有效 location。
- Agent GUI:控制器覆盖搜索命中文件夹后清空 query 并进入目录;Composer 覆盖将 `@关键词` 替换成 `@` 后再发起导航。
- 先执行 pnpm check:changed -- --dry-run,再执行覆盖变更能力的 pnpm check:changed;Go/UI 变更补跑对应定向测试和 typecheck。
- Windows E2E:已索引/未索引目录、文件/文件夹/type filter、取消、未触发全量递归日志、Recent 隐藏和旧选择回退。
- macOS E2E:Spotlight 关键词/type filter/within、Unicode/空格路径、Recent 继续按原链路显示。
- @ E2E:本地位置、点击箭头、返回、ArrowRight/ArrowLeft、点击外部关闭。
索引不可用时的产品文案和是否引导用户启用索引仍待确认;代码先保证快速失败和日志证据。
## 10. 分阶段实施顺序
1. 从最新 origin/main worktree 记录基线测试和当前 @ 回归结果。
2. 提交本方案文档,锁定不新增公开接口、不保留旧扫描 fallback。
3. 后端抽内部 Adapter,接入 Windows SystemIndex/macOS Spotlight,补测试和日志。
4. 打开两个本地 source 的 filtersUseSearch,Windows 隐藏 Recent,补持久化回退测试。
5. 修复 Agent @ 编辑器查询与目录控制器状态同步,补点击与键盘共用入口及控制器回归测试。
6. 运行变更检查、Go/TS 类型与单测,执行当前机器可执行的 E2E,并把不可确认项标出。
7. 由独立 subagent 只读 review diff、链路、风险和测试证据;修复后重新验证。
8. git commit -s、push 分支、创建 ready-for-review PR/MR,回读地址、描述和 CI 状态。
## 11. 待确认项
- Windows 发行包使用 Windows PowerShell 5.1 还是 PowerShell 7;Search.CollatorDSO 在目标系统的可用性和字段需实机确认。
- macOS 沙箱/权限下 mdfind 是否能查询用户选择的所有 workspace root。
- Windows/Linux 是否需要 Recent 的替代入口;本方案只落实 Windows 隐藏。
- 索引不可用时显示错误、空结果还是引导启用索引,需产品确认。
@@ -0,0 +1,97 @@
# Tutti Mode Explicit Guidance Routing Remediation
- Status: confirmed and implemented
- Baseline: `origin/main`
- Date: 2026-08-12
## Background and Goal
While a Tutti Plan is executing, an ordinary Composer Send is automatically
converted to guidance when the Plan has non-terminal work, the current Turn is
running, and the provider advertises `activeTurnGuidance`. That routing treats
a capability as user intent, so ordinary messages, `/compact`, and `/goal` can
bypass normal queueing or control-command parsing in a timing-dependent way.
The goal is for ordinary Send to preserve ordinary submit semantics and use the
existing prompt queue while busy. Guidance must require an explicit guidance
action. The completed-Goal active projection and the source of a later
`goal/set` are separate follow-ups once sufficient evidence exists.
## Current and Target Flows
Current flow:
```text
ordinary Send
-> submitPromptOrDecidePlan
-> Plan active + Turn running + activeTurnGuidance === true
-> submitGuidancePrompt(sendNow=true, targetTurnId)
-> routing=send_now
-> GuideActiveTurn
```
Target flow:
```text
ordinary Send
-> submitPromptOrDecidePlan
-> submitPrompt
-> routing=auto
-> send while idle, otherwise wait in the existing prompt queue
explicit Guidance
-> submitGuidancePrompt
-> routing=send_now + targetTurnId
-> GuideActiveTurn
```
## Module Changes
| Module | Change |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `useAgentGUITuttiWorkflow.ts` | Remove automatic guidance from ordinary Send during active Plan execution. After Plan Review feedback handling, always call ordinary submit. |
| `AgentGUIDetailPane.tsx` | Remove the unused Workflow guidance passthrough input while preserving the Composer's explicit `onSubmitGuidance`. |
| `useAgentGUITuttiWorkflow.spec.tsx` | Replace the automatic-steer expectation with active-execution coverage for ordinary text, `/compact`, and `/goal clear`. |
| `agent-gui-node.md` | Record that provider capability enables explicit guidance but never changes ordinary Send semantics. |
No API, persistence schema, queue, or provider adapter is added. The change
reuses the existing prompt queue, typed Goal control, explicit guidance path,
and runtime capability.
## Explicit Non-Goals
- Do not remove the underlying guidance capability.
- Do not make slash commands send-now by default.
- Do not implicitly interrupt the current Turn.
- Do not rewrite the Engine or provider runtime.
- Do not translate Goal `complete` into `clear` automatically.
- Do not change Goal continuation without source evidence.
## Compatibility, Risk, and Rollback
There is no data migration or protocol change. The main behavior risk is that
users who relied on ordinary text to correct the current Turn immediately will
now see that text queued. Immediate correction remains available through
explicit guidance. Tutti Mode Plan Review, Issue materialization, task
scheduling, Stop, recovery, and archival flows are unchanged.
Rollback restores only the Workflow routing and its tests/documentation; it
does not affect persisted data.
## Test and Acceptance Criteria
- With an active Plan, running Turn, and guidance support, ordinary text uses
ordinary submit.
- Under the same state, `/compact` uses ordinary submit and waits in the queue
while busy instead of becoming implicit guidance.
- Under the same state, `/goal clear` reaches typed Goal control through the
ordinary submit path.
- Explicit guidance continues to carry `routing=send_now` and the exact
`targetTurnId`.
- AgentGUI typecheck, formatting, degradation checks, and focused tests pass.
End-to-end acceptance focuses on the Composer-to-Engine routing boundary:
ordinary inputs do not create guidance, while explicit guidance still does.
Real-provider log verification follows in a staged build because no existing
Electron replay fixture combines an active Tutti Plan with an active source
Turn.
@@ -0,0 +1,356 @@
# Agent GUI / Windows 问题技术改造方案
状态:Proposed
日期:2026-08-14
范围:`tutti` 单仓库,覆盖此前已分析的 Agent GUI、ACP 和 Windows 问题。当前文档只定义改造方案,不包含业务代码修改。
## 1. 背景与目标
此前日志、代码、截图和 `make dev-gui` 启动验证暴露出一组相互放大的问题:Renderer 仍持有旧 session/turn/interactive 请求,daemon/runtime 同时接收新旧事件;provider 的异步事件和能力声明不完整;Windows 本地搜索及状态目录并发不稳定;部分 UI 只消费缓存或临时状态。
本方案的目标是:
1. 以 `(workspaceId, agentSessionId, turnId, requestId)` 作为跨层身份边界,旧请求不得改变当前会话状态。
2. 以 daemon 的 canonical session/turn/interaction 状态作为唯一状态源,Renderer 只做带身份的投影和恢复。
3. 将 provider 异步事件、能力和错误转换为统一的 Tutti 语义,并保留可诊断的安全错误信息。
4. 让模型切换、取消、关闭、重启、多窗口和高并发 handoff 具备幂等、可恢复的终态。
5. 修复 Windows 搜索、SQLite 状态目录竞争和窗口布局/缓存问题,同时保持现有 API 和已存在会话兼容。
### 已确认与待确认边界
已确认的证据包括:
- GPT-5.2 报告中,当前 session `80db00c1...` 的 `turn.start.succeeded` 和 `api.send.completed` 成功;失败请求却发往旧 session `70c13030...`。同一时间 `tuttid` 反复记录 `message_delta stream identity does not match its runtime scope`。
- Claude 高并发 handoff 中存在 5 秒终态 barrier 超时、workspace/agent target 查询超时,以及 provider 已不再认为 turn active 后的取消竞态。
- Cursor 声明 Plan mode,但缺少可用的 AskQuestion 能力;Kimi 的 permission 事件可能先于 tool input 到达。
- Windows Search 请求只带 `include_kinds=[file]`,provider 又排除了 Directory;搜索超时后 Renderer 收到 `AbortError/502`。
- Microsoft Store 版本确实启动了 AppX 内的 `tuttid.exe`,Agent 创建失败的日志根因是 `database is locked`;AppX Start 菜单注册和 manifest 图标文件均有证据存在。
- 新窗口可以看到新会话,旧窗口仍使用 rail projection/cache;取消问题缺少对应日志包。
以下结论在实施前必须通过复现或补充日志确认,不能作为本方案的隐含前提:
- GPT-5.2 失败 turn 的 provider 原始错误是网络、模型拒绝、权限、服务端错误还是进程退出;导出包没有保留该 error notification 的完整 payload。
- `message_delta` identity mismatch 是否直接导致该次“发送失败”,还是只造成流式内容丢失/旧状态投影。
- 原始问题清单中的第 11 项未在本次材料中提供独立症状和证据;在补充前不新增实现范围。
- 取消问题(下文问题 I)是终态事件丢失、Renderer 关联错误,还是 provider 取消延迟;当前只能依据代码提出防护。
- “Microsoft Store 是否需要桌面快捷方式”是产品要求还是误报;现有证据只证明 Start 菜单和 AppX 图标资源存在。
证据包(不提交到仓库):`C:\Work\logger-analysis-gpt52\extracted-20260814\logs\tutti-desktop.log`、`tuttid.log`。对应生产版本为 `0.2.23-rc.8`,日志窗口为 2026-08-13 14:15:25–14:25:25(+08:00)。
## 2. 当前架构与完整链路
### 2.1 普通发送和 GPT-5.2 场景
```text
Renderer Agent GUI
-> desktopAgentActivityAdapter.sendInput
-> POST /v1/workspaces/{workspaceID}/agent-sessions/{agentSessionID}/input
-> tuttid API SendWorkspaceAgentSessionInput
-> service/agent.SendInput
-> host.ApplicationHost().SendInput
-> daemon runtime controller
-> CodexAppServerAdapter.execBlocking
-> app-server turn/start
-> provider notifications
-> reducer/turn machine/normalizer
-> activity events + SQLite projection
-> tuttid session query
-> Renderer session/presentation/cache
```
当前代码证据:
- `apps/desktop/src/renderer/src/features/workspace-agent/services/desktopAgentActivityAdapter.ts` 负责 HTTP 发送和结果转换。
- `services/tuttid/api/daemon_agent_submit_handlers.go` 的 `SendWorkspaceAgentSessionInput` 记录 `api.send.received/completed`。
- `services/tuttid/service/agent/service_send_input.go` 负责 prompt 规范化、submit 幂等标识、Host dispatch 和 turn 刷新。
- `packages/agent/daemon/runtime/codex_appserver_turn.go:571-575` 记录并调用 `turn/start`;`turn/start` 成功后,后续 provider `error` 才会通过 turn machine 进入 failed。
- `packages/agent/daemon/runtime/codex_appserver_turn_machine.go:81-85,212-220` 将 provider error 转成普通 `error`;当前没有保证原始 code、provider turn 和安全用户消息均可追踪到日志/前端。
这说明 GPT-5.2 报告中的“请求失败”不能直接归因于 HTTP submit 或模型切换 API。已确认的直接错误表现是:旧 interactive response/旧 session 身份进入当前 Renderer 流程,当前新 session 的 turn 已被接受;实际 provider 失败原因仍待补齐原始 error payload。
### 2.2 Interactive、审批、AskUser 和取消链路
```text
provider permission / ask-user notification
-> runtime pending interaction
-> canonical pending interaction + activity projection
-> Renderer pending UI
-> POST .../interactives/{requestID}/response
-> tuttid SubmitInteractive
-> runtime SubmitInteractive / provider response
-> provider continuation
-> turn terminal event
-> canonical state -> Renderer
```
入口为 `services/tuttid/api/routes_workspace_agent_sessions.go:253`,服务接口为 `SubmitInteractive`。问题在于当前 response 主要按 request/session 处理,Renderer 仍可能携带已经切换前的 session 或 turn;projection bridge 发现 mismatch 后只记录 warning,未立即重建 canonical state。
取消和窗口关闭另有一条链:
```text
Renderer stop / window close
-> session engine stopRequested
-> cancel API
-> provider cancel + local context cancel
-> turn settled event
-> canonical activity projection
-> composer busy/loading state cleared
```
`cancel_requested` 只是“取消已接受”,不是终态。窗口关闭则额外受到 `workspaceWindowCloseGuard.ts` 和 `workspaceWindow.ts` 中 guard 条件的影响;启动恢复时 Renderer 可能先恢复旧 pending interaction,再等待 daemon reconcile。
### 2.3 子 Agent handoff 链路
```text
root turn
-> handoff/delegated task creation
-> child agent session/provider turn
-> child terminal report
-> controller_report_worker barrier
-> workspace/agent-target state query
-> root cancellation or continuation
```
当前固定 5 秒终态 barrier、子会话枚举、provider cancel 和终态持久化不是一个原子操作。根会话取消时,provider 可能已经清除 active turn,导致 `agent cancellation target is no longer active`,而子会话的最终状态尚未可见。
### 2.4 Windows 文件引用和状态目录链路
```text
ReferencePicker
-> filtersUseSearch / includeKinds
-> workspace file API
-> Windows Search OleDB
-> PowerShell child process
-> file/dir result
-> Renderer picker
```
`services/tuttid/data/workspace/local_files_search_windows.go:117` 使用 `System.ItemType <> 'Directory'`;`workspaceFileLocationReferenceSources.ts` 将相关 source 标为 `filtersUseSearch=true`。搜索超时会沿 API 转成 502,Renderer 只得到通用失败。
Store/Direct 启动链路为:
```text
AppX/Direct desktop
-> tuttid.exe
-> shared .tutti state directory
-> SQLite transaction
-> upsert workspace agent session
```
SQLite 目前只配置有限 busy timeout(`services/tuttid/data/workspace/sqlite_store.go:156`),无法区分旧 daemon 未退出、两个实例并发,还是同一 daemon 内的长事务。
## 3. 目标架构与完整链路
目标不是引入新的 Agent 状态系统,而是在现有 canonical session/turn/activity 之上补齐四个不变量:
1. **身份不变量**:每个 command、provider event、projection event 都携带 workspace、agent session、turn;interactive 额外携带 request ID。身份不匹配的事件不能写入另一条 session。
2. **终态不变量**:取消、失败、完成只能由 canonical runtime 产生一次;重试、重启和重复 response 只做幂等 reconcile。
3. **配置不变量**:session canonical settings、runtime effective settings、provider `turn/start` settings 必须可追踪;模型切换只影响尚未开始的下一 turn,不能改变已运行 turn。
4. **恢复不变量**:projection mismatch、超时、窗口重开或 daemon 重启都触发 canonical reread/reconcile,而不是停留在临时 loading/pending 状态。
### 3.1 目标普通发送链路
```text
Renderer 创建 SubmitContext
{workspaceId, agentSessionId, clientSubmitId, turnId?, requestId?, sessionRevision}
-> API 校验 session/turn/request 身份及幂等性
-> Service 在 canonical session lock 下准备一次 dispatch
-> Runtime snapshot effective settings(包括 gpt-5.2)
-> Provider adapter dispatch,记录 provider session/turn/model
-> provider event 按身份路由
-> terminal event 先持久化,再发布 projection
-> Renderer 只接受同一 SubmitContext 的事件;否则丢弃并 reconcile
```
HTTP 仍可保持现有成功响应;stale interactive response 使用明确的 `409/410` 语义和机器可读错误码,不再包装成无差别 502。普通 provider failure 返回/投影 `failureCode`、安全 `userMessage`、`retryable` 和关联 IDs。
### 3.2 目标模型切换链路
```text
Renderer settings patch(gpt-5.2)
-> API/service session settings lock
-> Host canonical settings update
-> Runtime adapter 更新“下一 turn effective settings”
-> 不重建 live session,不取消当前 turn
-> 下一次 turn/start 使用该 snapshot 的 model/effort
-> model catalog refresh 作为 single-flight、可超时的旁路任务
```
模型目录刷新失败不得把已经可用的 live session 或正在运行的 turn 标记为 failed。provider 原始错误只有在安全抽取后才进入活动事件和日志;不能为了显示错误而记录 token、完整 prompt 或凭据。
### 3.3 目标交互、取消和恢复链路
- Interactive response 先在 service 层校验完整 tuple;canonical pending 不属于当前 tuple 时返回 stale,清理本地 pending projection,但不得取消当前 session 的新 turn。
- identity mismatch 触发一次按 session/turn 的 canonical reread;reconcile 结果以 revision/cursor 单调更新,避免旧 snapshot 覆盖新状态。
- cancel 先冻结新的 child task/新 response,再以 provider-specific cancel adapter 取消 provider turn,最后提交本地 terminal;目标已结束时返回最终状态而非错误。
- close guard、菜单、快捷键和原生窗口事件统一调用同一个 bounded close flow;启动先恢复 canonical state,再决定是否显示 pending interaction。
- child task 以持久化 task tree + durable terminal outbox 表达;barrier 只是等待优化,不是终态正确性的唯一保障。
### 3.4 目标 Windows 链路
- 文件浏览、目录搜索和全文搜索分离;UI 的 `includeKinds` 与后端查询保持一致,目录不能被强制过滤为 file。
- Windows 搜索使用可取消、分阶段返回的 provider;索引不可用或超时后进入目录缓存/枚举 fallback,并显示可操作的状态。
- Recent provider 是否纳入 Windows 由产品确认;若不纳入,UI 明确标注平台能力,而不是静默移除。
- state directory 增加单 daemon lease/handshake;新实例等待旧实例退出或复用已就绪 daemon。SQLite busy retry 采用有上限的退避并记录等待信息,持续冲突返回明确错误。
- AppX 图标与 Start 菜单保持现有 manifest;桌面 `.lnk` 只有在产品验收明确要求后另行设计。
## 4. 各模块改造内容
### 4.1 `apps/desktop`
- 在普通发送、interactive response、settings update、cancel 和 close flow 中统一构造/传递 `SubmitContext`;所有异步回调按 session/turn/request 比较后再写 UI。
- 旧 session response 只执行 stale 清理和 canonical refresh,不得覆盖 active session;将“当前发送失败”“旧交互已过期”“provider 失败”分成不同 UI 状态。
- `agentGui` 的 pending/busy 状态改为精确绑定 `(agentSessionId, turnId)`;`cancel_requested` 显示“正在停止”并等待 bounded reconcile,超时显示可重试而非无限 loading。
- 窗口关闭所有入口统一进入 close guard;重启恢复顺序改为 canonical session 先行。
- rail 在 session create/upsert 后广播 invalidate/upsert 给其他窗口;移除“active id 已加载就跳过刷新”的错误短路,保留分页和排序语义。
- 图片工具栏改用统一 titlebar safe-area CSS 变量;消除 fixed portal 与 native titlebar controls 的独立坐标。
- ReferencePicker 分离 browse/search/kind,补齐错误、取消、fallback 和 Recent 能力显示。
### 4.2 `services/tuttid/api` 与 `services/tuttid/service/agent`
- 统一校验 workspace/session/turn/request tuple 和 request revision;SubmitInteractive 对 stale、重复、目标已结束返回稳定机器码。
- 保留 `clientSubmitId` 幂等语义,补齐 command/dispatch/terminal 三段 trace,日志统一记录 provider session/turn、model、failure code 和耗时,不记录 prompt/token/credential。
- 把 provider failure 映射为结构化 `failureCode/userMessage/retryable`;仅在真正的 transport/API 失败时使用 502。
- 将 cancel、handoff、child task 的状态写入 canonical task/terminal outbox;重复 cancel 和 target 已结束必须幂等。
- 维护 settings lock 和 session revision,确保 settings patch、send、cancel 不以旧 snapshot 覆盖新状态。
### 4.3 `packages/agent/host` 与 `packages/agent/daemon/runtime`
- Host 继续作为 canonical/live settings 的协调边界;新增/复用 tuple 校验和按 revision 的 reconcile,不把 Renderer 临时状态下沉为事实。
- Codex adapter:模型/effort 作为 next-turn override;补齐 model catalog 已加载时 `configOptions`、session settings 和实际 `turn/start` 的一致性测试。不要因 catalog refresh timeout 重建 live client。
- model catalog refresh 使用 single-flight、超时和 stale-while-usable 结果;与 active turn dispatch 解耦。具体复用现有 app-server client 还是独立探测 client,需根据当前生命周期实现确认,不在方案中强行指定。
- turn machine 在 provider `error`、`turn/completed`、transport EOF、cancel 中统一生产一次 terminal,并输出安全结构化错误。
- `agent_runtime_activity_event_bridge.go` 发现 identity mismatch 时停止错误投影、发起 canonical reread,并带上 expected/actual scope;`controller_stream_observer.go` 不得继续把错误 scope 的事件发布给当前 session。
- ACP capability 增加 AskUser/Question 维度;Plan mode 的可用性由 `PlanMode && AskUser` 或明确 fallback 决定,不再只看 PlanMode。
- generic permission 与 ask-user 均按 `(providerSessionId, turnId, toolCallId/requestId)` 延迟合并;后到的 Read/ReadFile input 更新已有 pending interaction。
- handoff 使用 provider adapter 的 cancel/settle 能力,child 终态通过 outbox 重放;root cancel 前冻结 child creation。
### 4.4 `packages/agent/store-sqlite` / `services/tuttid/data/workspace`
- 若现有 activity payload 足以承载 `SubmitContext`,优先使用已有 JSON,不新增表;只有 task tree/outbox 没有可复用存储时才做加法 schema。
- 为 SQLite 写事务增加有限指数退避、数据库路径和等待时长诊断;通过 daemon lease 避免 Store/Direct 进程并行写同一状态目录。
- 搜索 provider 将目录/文件 kind 变成明确参数;Windows PowerShell 进程必须可取消并受总 deadline 约束,fallback 不阻塞 session runtime。
### 4.5 OpenAPI、TypeScript client、打包资源和文档
- API 若增加 stale/failure 字段,先更新 `services/tuttid/api/openapi/tuttid.v1.yaml`,再重新生成 Go/TypeScript client;旧客户端仍可读取原有 session/turn 字段。
- AppX manifest 和 logo 文件本轮只做安装验收,不因没有证据而改动;桌面快捷方式另立产品需求。
- 增补运行手册、故障码和 Windows E2E 复现步骤,避免后续只能依赖通用“请求失败”。
## 5. 可复用部分与 Adapter 边界
### 可复用的核心能力
- 现有 session/turn/activity canonical projection、session lock、`clientSubmitId`、turn normalizer、activity replay/cursor。
- 现有 `ApplicationHost`、provider registry、runtime context、OpenAPI 生成链路。
- 现有 close guard、rail query controller、SQLite store 和 Windows provider 的取消/测试基础设施。
### Adapter 边界
- **Provider adapter** 只负责 provider protocol:Codex `turn/start`/model catalog,Claude child turn,Cursor capability,Kimi permission event 顺序;不能自行修改 Renderer session 状态。
- **Runtime core** 负责身份、幂等、终态、reconcile 和安全错误模型;不能把某个 provider 的字段直接暴露为全局状态。
- **Desktop adapter** 只负责传输、投影和用户交互;不能根据“请求返回 200”推断 turn 已完成。
- **Windows adapter** 只负责搜索/状态目录/打包平台差异;不改变 Agent turn 状态机。
这样可以复用一个 Tutti 状态机,同时把 provider 的异步协议差异限制在适配器内,避免为 Codex、Claude、Cursor、Kimi 各自复制一套取消或 pending 逻辑。
## 6. 明确不做的内容
- 不在未拿到 provider 原始错误前,强制把 GPT-5.2 失败归因成“模型不可用”,也不自动降级到 GPT-5.5。
- 不重写整个 Agent GUI、ACP 客户端或 SQLite 层;只补身份、终态和必要的旁路能力。
- 不把所有 `message_delta` mismatch 都当作 provider failure;先修复错误 scope 投影并保留独立指标。
- 不把固定等待时间简单调大来掩盖 handoff barrier;终态必须可持久化、可重放。
- 不在没有产品确认时创建桌面快捷方式或替换 AppX logo。
- 不默认替换 Windows Search 为第三方索引服务;先实现正确 kind、取消、fallback 和明确能力声明。
- 不删除已有 session、pending interaction 或 SQLite 数据;迁移只允许加法和可回滚重放。
## 7. 数据迁移与兼容策略
- 现有 session/turn 数据继续可读;新增的 context、failure、task/outbox 字段全部可选,旧记录按“无 revision/无 error detail”兼容读取。
- 启动 reconcile 扫描旧的 running/pending 状态:若 provider 已结束则补 terminal;若无法确定则标记 `reconcile_required`,不伪造 completed。
- 旧客户端不发送完整 tuple 时,服务端只在 canonical pending 与当前 session/turn 唯一匹配时兼容处理;否则返回 stale,而不是猜测目标。
- task tree/outbox 若需要 schema,采用新表/新列和版本化 payload;发布顺序为“先读兼容,再写新字段,确认稳定后才可清理旧字段”。
- SQLite lease 不改变数据库内容;busy retry 和错误码为运行时兼容变更。
- rail/event 广播采用可丢失通知 + canonical reread,不能把跨窗口通知当作唯一数据存储。
- API 保持原有 200 成功响应字段;新增错误字段为 optional,旧 Renderer 仍显示通用错误,新 Renderer 显示结构化错误。
## 8. 风险与回滚
主要风险:
- tuple 校验过严导致合法的旧客户端或重启恢复请求被拒绝;需保留兼容分支并记录拒绝原因。
- provider event 延迟/乱序使 reconcile 产生重复 terminal;必须由 canonical revision 和 terminal 幂等键去重。
- catalog single-flight 或 Windows fallback 增加首次等待;需以 stale catalog/渐进结果保证可用性。
- SQLite lease 误判残留进程,造成无法启动;lease 必须带 PID、启动时间和可验证的 ready/exit handshake,不能只看文件存在。
- close flow 的 bounded wait 可能让用户提前看到“正在停止”;这是可接受的中间态,但必须有最终 reconcile 和重试入口。
回滚方案:
1. 按能力设置独立 feature flag:`identity_guard`、`terminal_reconcile`、`provider_error_detail`、`handoff_outbox`、`windows_search_fallback`、`cross_window_invalidation`、`titlebar_safe_area`。
2. 先关闭 UI 展示和旁路优化,再保留只读日志;API 新字段保持 optional,旧版本可以继续运行。
3. schema 只做向后兼容加法;若 outbox 写入异常,停止新写入并从 canonical activity 重建,不删除已有数据。
4. 发布后以 session/turn/request 维度监控 stale reject、duplicate terminal、identity mismatch、SQLite lock wait、搜索超时和 provider failure code;异常升高时按模块回退。
## 9. 测试与验收标准
### 9.1 自动化测试
- Runtime:模型目录已加载时切换 `gpt-5.5 -> gpt-5.2`,断言 canonical settings、runtime config、`turn/start` params 全部为 gpt-5.2;旧 active turn 不改变。
- Runtime:provider error、EOF、cancel、重复 terminal 各只产生一个 terminal;错误包含安全 `failureCode` 和 retryability。
- Projection:构造 expected/actual scope 不同的 `message_delta`,确认错误事件不污染目标 session,并能完成一次 canonical reread。
- Interactive:旧 request、重复 response、目标已结束、当前 request 分别验证 409/410、幂等成功和正常 provider response。
- Handoff:10–20 个 child turn 下取消 root、取消 child、取消后重启;所有 child 最终为 canceled/settled,outbox 可重放。
- Capability/ACP:provider matrix 覆盖 Plan-only、Plan+AskUser、generic permission-first、tool input-first 和 tool input-late。
- Windows search:file/Directory/未索引/取消/超时/fallback/Recent(Recent 是否支持待产品确认)。
- SQLite:两个 daemon、Store/Direct 交替启动、旧进程退出中启动;验证无不可恢复 `database is locked`。
- Desktop:窗口关闭三种入口、重启恢复、双窗口新建会话、DPI 100/125/150% 和窄窗口。
### 9.2 端到端验收
使用真实 Windows 环境通过 `make dev-gui`:
1. 打开两个 Agent session,把当前 session 从 GPT-5.5 切到 GPT-5.2;在旧 session 留下 pending approval,再向当前 session 发送消息。
2. 断言发送请求的 tuple 指向当前 session,`turn/start` 的 model 为 gpt-5.2;旧 response 被标记 stale 且不会使当前 composer 失败。
3. 在 provider 失败、网络不可达和模型目录超时场景,UI 显示结构化可操作错误;目录刷新超时不影响普通发送。
4. 并发启动/取消 10–20 个 child,关闭并重开应用;所有 session/turn/pending 状态最终与 canonical 状态一致。
5. 测试文件夹和未索引目录搜索,确认参数 kind 正确、超时有 fallback;双窗口新会话在事件周期内出现。
6. 验收截图/日志必须能用 `workspaceId + agentSessionId + turnId + requestId` 串起一次操作,且不出现错误 scope 的 `message_delta` 投影。
建议的性能门槛(基线需在实施阶段测量,数值未达标时标记待确认):普通 send/settings API 的本地新增开销 p95 < 50ms;取消请求进入“正在停止” < 500ms;跨窗口 session upsert < 1s;搜索在 deadline 内返回结果或明确 fallback,不允许无限 spinner。
## 10. 分阶段实施顺序
### 阶段 0:补证据和冻结契约
- 用 `make dev-gui` 重现 GPT-5.2、旧 interactive、取消和双窗口场景,捕获完整 provider error notification。
- 确认原始第 11 项和取消问题的真实症状;确认 Recent、桌面快捷方式等产品决策。
- 先落地结构化 trace 字段和测试夹具,不改变用户行为。
### 阶段 1:身份、终态和恢复(P0)
- 实现 tuple 校验、stale interactive 语义、terminal 幂等和 projection mismatch reconcile。
- 修复 cancel/close/restart 的 canonical-first 流程;补 child cancel 冻结和最小 durable terminal outbox。
- 交付问题:GPT-5.2 旧请求串线、关闭后旧 AskUser、取消后无限 loading、Claude handoff 取消竞态。
### 阶段 2:provider 能力、模型与 ACP 事件(P0)
- 实现 AskUser capability matrix 和 Plan mode gating。
- 修复 Codex model settings/catalog single-flight、safe provider error;修复 generic permission 的 late tool input merge。
- 交付问题:Cursor Plan、Kimi Read/ReadFile 详情,以及 GPT-5.2 失败可诊断性。
### 阶段 3:Windows 数据链路(P1)
- 修复 file/directory search contract、取消/fallback/Recent 能力提示。
- 增加 state directory daemon lease、SQLite bounded retry 和明确错误码;完成 Store/Direct 交替启动验收。
- 交付问题:工作区引用搜索/文件夹、Microsoft Store Agent 启动。
### 阶段 4:桌面投影和视觉回归(P1)
- 完成跨窗口 rail invalidation、分页/排序回归。
- 完成 titlebar safe-area 和 DPI/窄窗口验收。
- 交付问题:旧窗口会话列表不刷新、图片工具栏与标题栏重叠。
### 阶段 5:灰度、观测和收口
- 按 feature flag 灰度,比较各阶段前后的 stale reject、identity mismatch、terminal latency、provider failure、SQLite lock 和 search timeout。
- 关闭不再需要的临时兼容分支前,确认旧客户端、重启恢复和已有数据均可读。
@@ -0,0 +1,218 @@
# Connector Market Dynamic Category Protocol Integration
> Status: implemented; under review in PR #2441. Scope: the Tutti Connector
> Market consumer path. The `tsh-server` protocol and runtime are already on main
> and are not changed by this plan.
>
> Baselines: `tutti` `origin/main` at `33c5182ad`; provider artifacts pinned to
> `tsh-server` commit `d257868a661af964835b3ac2263252128f731b25`.
## Decision
Tutti treats the Market service as the source of truth for category identity,
kind, order, count, and localized names. The daemon projects those accepted
facts through its local OpenAPI boundary, and the renderer selects the name for
the active locale. The client never derives a title from `categoryId`.
The remote boundary uses provider-generated protobuf and HTTP artifacts instead
of extending hand-written Market DTOs. Tutti pins those artifacts in the
market-neutral `packages/clients/market-go` module. Connector passes
`itemType=connector`; a future Skills Market can reuse the same remote client
with `itemType=skill` while keeping its own domain and UI state.
## Goals
- Consume `categoryId`, `kind`, `sortOrder`, `itemCount`, `displayNameZh`, and
`displayNameEn` without losing information between the service and renderer.
- Allow the service to add, rename, reorder, or disable categories without a
Tutti release.
- Preserve the exact category ID as the pagination `sectionId`; do not rewrite
`development` to `developer-tools` or perform any other aliasing.
- Keep the remote Market protocol separate from the local daemon OpenAPI
protocol.
- Support the service deployment and client rollback window.
- Establish a reusable remote client boundary without implementing Skills
Market UI in this change.
## Non-goals
- Changing the Connector publication manifest, installation, authorization,
runtime, or artifact protocols.
- Persisting remote categories in local SQLite.
- Adding category administration to Tutti.
- Merging Connector and Skills renderer state.
- Inferring category placement from a Connector key or manifest.
## Protocol ownership
The Market service owns these fields:
| Field | Contract in Tutti |
| --------------- | ---------------------------------------------------------- |
| `categoryId` | Preserve as an opaque identifier and pagination section ID |
| `kind` | Accept `featured` or `category` |
| `sortOrder` | Sort ascending without a client-defined category order |
| `itemCount` | Require a non-negative count |
| `displayNameZh` | Prefer for locales whose normalized value starts with `zh` |
| `displayNameEn` | Prefer for all other locales |
`packages/clients/market-go` owns only transport concerns: generated request and
response types, protobuf JSON decoding, the host-provided HTTP transport and
timeout, gateway base paths, request authorization, and bounded response bodies.
It has no Connector, Skills, UI, or daemon product semantics.
`packages/connector/daemon.CatalogSource` is the Connector adapter. It fixes
`itemType` to `connector`, validates the remote response, and explicitly maps
generated types into `packages/connector/host.CatalogCategory`.
`packages/connector/market/openapi/connector-market.v1.yaml` remains the local
renderer-facing protocol. Generated Go and TypeScript types come from this
OpenAPI source rather than exposing the remote protobuf types.
## Data flow
```text
tsh-server Market schema
-> pinned provider-generated artifacts
-> packages/clients/market-go
-> packages/connector/daemon CatalogSource
-> packages/connector/host.CatalogCategory
-> services/tuttid API projection
-> Connector Market local OpenAPI
-> generated @tutti-os/client-tuttid-ts types
-> Connector Market domain and view state
-> ConnectorCatalog locale-aware title selection
```
The following bypasses are intentionally forbidden:
```text
renderer -> remote Market API
UI -> generated title or slug from categoryId
manifest -> inferred category
remote generated DTO -> local OpenAPI or renderer
Connector adapter -> rewritten categoryId
```
## Generated client pinning
`packages/clients/market-go/source.lock.json` records the provider repository,
exact commit, source file paths, target file paths, and SHA-256 values. The
vendored files make CI checks network-independent.
Verify the pinned files:
```sh
pnpm check:api-generated
```
Update from an authorized checkout of the matching provider commit:
```sh
pnpm generate:market-go-client -- --source-root /path/to/tsh-server
```
The sync command deliberately requires a local provider checkout and verifies
that its HEAD matches the pinned provider commit before copying files. It does
not fetch a private repository anonymously or import the `tsh-server` root Go
module.
## Compatibility window
The local OpenAPI name fields are optional during the consume phase. This keeps
Tutti compatible with a service rolling deployment and allows a client rollback.
When either localized name is present, the renderer uses the preferred locale
and falls back to the other available name. When both names are absent, only
the categories from the earlier released response are accepted:
- `featured`
- `productivity`
- `development`
- `other`
Those four IDs use existing package-local i18n during the compatibility window.
An unknown category with no server-owned name is rejected at the remote adapter
boundary. The UI also has a defensive generic translated label and never renders
the raw slug.
`installed` is a Tutti-local virtual section and continues to use local i18n.
After every supported domestic and overseas service deployment returns names,
the rollback window is closed, and the minimum supported Tutti version includes
this consumer, a separate contract cleanup can:
- make both local OpenAPI fields required;
- make the Go and TypeScript domain fields non-optional;
- remove the legacy category switch and its i18n keys;
- retain cross-language fallback as defensive presentation behavior.
## Implemented changes
- Added `packages/clients/market-go` with the generated client, source lock,
bounded transport adapter, tests, and update instructions.
- Replaced the Connector daemon's hand-written Market response DTOs and raw GET
path with the generated client.
- Added localized category names to the Connector host model and local OpenAPI,
then regenerated the Go and TypeScript API types.
- Preserved localized names through Connector Market state and view projection.
- Added a pure locale-aware category title resolver and used it in the catalog.
- Added compatibility, dynamic-ID, exact-pagination-ID, additive-field,
market-neutral item type, and response-size coverage.
- Documented the generated-client ownership and dynamic category flow in the
durable architecture and project-structure documentation.
No new renderer primitives or styles were needed. The existing Connector Market
UI continues to use `@tutti-os/ui-system`; this change only replaces title data.
## Validation
The implementation is validated with:
```sh
node tools/scripts/sync-market-go-client.mjs --check
go test -race ./... # packages/clients/market-go
go test -race ./... # packages/connector/daemon
go test ./... # packages/connector/host
go test ./api # services/tuttid
pnpm --filter @tutti-os/client-tuttid-ts test
pnpm --filter @tutti-os/client-tuttid-ts typecheck
pnpm --filter @tutti-os/connector-market test
pnpm --filter @tutti-os/connector-market typecheck
pnpm check:api-generated
pnpm check:i18n
pnpm release:pack:check -- --packages-json '["@tutti-os/connector-market"]'
pnpm check:changed
```
## Rollback
- The service fields are additive, so old clients remain compatible.
- The local OpenAPI fields stay optional during the consume phase.
- Category IDs and listings are not migrated locally, so no data rollback is
required.
- If the generated transport integration must be rolled back, revert the Tutti
release; do not restore a second hand-written implementation as a permanent
path.
## Follow-up PR
The later contract-cleanup PR is intentionally separate. It requires production
evidence that both localized names are consistently present and that the service
rollback window is closed. It will not add category data, Connector listings, or
Skills Market UI.
## Acceptance criteria
- A newly administered category appears in Tutti in service order and with the
correct localized name without a code change.
- `developer-tools`, `communication`, `creativity`, and
`business-operations` never render as slugs.
- Locale changes only select presentation data and do not refetch the catalog.
- Pagination uses the exact category ID returned by the service.
- Domestic and overseas markets can return different counts and listings while
using the same category-name protocol.
- A future Skills consumer can reuse the Market client with `itemType=skill`
without importing Connector renderer types.
- Tutti has no new hand-written remote Market category response DTO or remote
category title enumeration.
@@ -0,0 +1,788 @@
# Connector Directory Convergence
> Status: implemented on 2026-08-19.
>
> Scope: the Tutti repository only. TSH and other external consumers are
> intentionally deferred.
>
> Baseline: tutti main at 74ba1915a on 2026-08-19.
## Implementation result
The migration was completed as one source-breaking change with no compatibility
aliases. The final owners, package exports, Go module identities, product
adapters, Agent GUI projection boundary, repository checks, release metadata,
and layered `AGENTS.md` files now match this document.
Focused Connector validation passes for all five Go modules, both npm packages,
Agent GUI, Desktop, Renderer component tests, package builds, package dry-runs,
generated contracts, ownership boundaries, lint, and type checking. The
changed-aware run passed 79 of 80 lanes; its only failing lane was the existing
full `services/tuttid` test lane. The same lane was the only failure in
`check:full`: two Agent Status tests observe a user-installed Claude executable
or credential, and one Workspace app-runner test intermittently times out while
retrying a port. Those tests do not execute Connector code and remain outside
this directory migration's change scope.
## Decision
Converge Connector code by architectural responsibility rather than
implementation language:
```text
contracts -> daemon -> runtime -> renderer
```
The actual dependency graph is not a linear call chain. The labels identify
ownership:
- Contracts owns Connector-specific cross-boundary protocols.
- Daemon owns lifecycle semantics, orchestration, and reusable daemon adapters.
- Runtime owns same-machine and managed-guest execution mechanics.
- Renderer owns frontend application services and every Connector-specific
React implementation.
Go and TypeScript remain the natural implementations of these responsibilities,
but language is not a directory boundary.
This is a source-breaking directory migration. Old module paths, npm package
names, import aliases, and compatibility re-exports will be removed. Runtime
behavior and external protocols remain unchanged.
## Goals
- Make Connector ownership visible from the directory tree.
- Keep the existing Host, Daemon, Runtime, Renderer, and product-adapter
architecture intact.
- Converge all Connector-specific React components, icons, default copy, and
presentation primitives into one Renderer UI owner.
- Keep Agent-specific draft, prompt, catalog-refresh, and Host-navigation
semantics in Agent GUI without leaving Connector-specific JSX there.
- Keep necessary frontend lifecycle, Market, View, and UI-state services in a
React-free Renderer Application layer.
- Preserve the current acyclic Go module graph.
- Make architecture boundaries executable through package exports, repository
checks, and layered AGENTS.md instructions.
- Complete the move in one atomic PR without temporary compatibility layers.
## Non-goals
- Changing Connector installation, authorization, reconciliation, execution,
or recovery behavior.
- Changing HTTP paths, OpenAPI field meanings, event names or payloads,
persisted data, manifest fields, status enums, error codes, authentication,
or call ordering.
- Redesigning Connector UI, interaction, information density, or copy.
- Replacing the current Renderer Root, Lifecycle, StartupJob, View, or UiState
service model.
- Moving Agent-owned draft or prompt semantics into Connector.
- Moving product-specific Desktop or tuttid composition into shared packages.
- Moving the market-neutral market-go client into Connector.
- Adding a new cross-process end-to-end test framework.
- Updating TSH or other external repositories.
- Publishing the new npm packages or Go modules as part of this change.
## Invariants
The migration is complete only when every invariant below is demonstrated:
1. HTTP, OpenAPI, event, persistence, and manifest semantics are unchanged.
2. Go lifecycle and Runtime behavior are unchanged.
3. Renderer state flow and service lifecycle are unchanged.
4. Connector UI behavior, layout, copy, loading, empty, error, authorization,
selection, and success states remain equivalent.
5. Agent draft blocks and prompt projection remain equivalent, including
connector-only submission.
6. Active source, configuration, CI, release metadata, and current
documentation contain no old Connector paths or package names.
7. Historical specs may retain old names only as historical narrative or in an
explicit migration note.
8. All new architecture guards and affected tests pass.
## Final directory structure
```text
packages/connector/
├── AGENTS.md
├── README.md
│
├── contracts/ # @tutti-os/connector-contracts
│ ├── AGENTS.md
│ ├── package.json
│ ├── README.md
│ ├── src/
│ │ └── authorization/
│ │ └── v1/
│ └── openapi/
│ └── connector-market.v1.yaml
│
├── daemon/
│ ├── AGENTS.md
│ ├── core/ # independent Go module
│ │ ├── go.mod
│ │ └── README.md
│ ├── application/ # independent Go module
│ │ ├── go.mod
│ │ ├── README.md
│ │ └── adapters/
│ │ └── catalog/
│ └── adapters/
│ ├── sqlite/ # independent Go module
│ │ ├── go.mod
│ │ └── README.md
│ └── controlplane/ # independent Go module
│ ├── go.mod
│ └── README.md
│
├── runtime/ # independent Go module
│ ├── AGENTS.md
│ ├── go.mod
│ ├── README.md
│ ├── agentgateway/
│ ├── artifact/
│ ├── command/
│ ├── implementationhost/
│ ├── mcp/
│ └── mcpserver/
│
└── renderer/ # @tutti-os/connector-renderer
├── AGENTS.md
├── package.json
├── README.md
└── src/
├── application/ # React-free
│ ├── contracts/
│ ├── authorization/
│ └── services/
│ ├── lifecycle/
│ ├── market/
│ ├── view/
│ └── ui-state/
└── ui/ # only Connector React owner
├── primitives/
├── catalog/
├── composer/
├── palette/
├── authorization/
├── dialogs/
├── toolbar/
└── i18n/
```
Host integration remains with each product owner:
```text
services/tuttid/service/connector/
├── market/
└── mcp/
apps/desktop/src/renderer/src/features/connector/
├── adapters/
├── registration/
└── index.ts
packages/agent/gui/agent-gui/agentGuiNode/integrations/connector/
├── model/
├── controller/
└── index.ts
```
The Agent GUI integration directory contains no JSX. Mixed Agent surfaces may
mount components imported from Connector Renderer UI, but they do not implement
Connector visuals.
## Source-to-target mapping
| Current owner | Target owner | Notes |
| ------------------------------------------------------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------- |
| packages/connector/host | packages/connector/daemon/core | Preserve the independent Go module and Host lifecycle/Port semantics |
| packages/connector/daemon | packages/connector/daemon/application | Preserve scheduling, recovery, lifecycle maintenance, and durable orchestration |
| packages/connector/daemon/catalog_source.go | packages/connector/daemon/application/adapters/catalog | Remains inside the Application Go module |
| packages/connector/store-sqlite | packages/connector/daemon/adapters/sqlite | Preserve the independent SQLite adapter module |
| packages/clients/connector-controlplane | packages/connector/daemon/adapters/controlplane | Preserve the independent HTTP/WebSocket adapter module |
| packages/connector/runtime | packages/connector/runtime | Keep the Runtime as a top-level execution boundary |
| packages/connector/authorization-protocol | packages/connector/contracts | Publish as the authorization/v1 subpath |
| packages/connector/market/openapi | packages/connector/contracts/openapi | Contracts package publishes the OpenAPI resource |
| packages/connector/market/src/contracts | packages/connector/renderer/src/application/contracts | These are Renderer domain and Host Port contracts, not wire schemas |
| packages/connector/market/src/services | packages/connector/renderer/src/application/services | Preserve Root, Runtime, Lifecycle, StartupJob, View, and UiState semantics |
| packages/connector/market/src/authorization/declarativeAuthorizationAdapter.ts | packages/connector/renderer/src/application/authorization | React-free authorization mapping |
| packages/connector/market/src/authorization/AuthorizationViewRenderer.tsx | packages/connector/renderer/src/ui/authorization | React implementation |
| packages/connector/market/src/ui | packages/connector/renderer/src/ui | All existing Market and Composer UI |
| packages/connector/market/src/i18n | packages/connector/renderer/src/ui/i18n | Connector UI owns default copy and its scoped runtime |
| apps/desktop/.../features/connector-market | apps/desktop/.../features/connector | Desktop Backend, Event, Admission, registration, and navigation adapters |
| services/tuttid/service/connectormarket | services/tuttid/service/connector/market | Product service adapter |
| services/tuttid/service/connectormcp | services/tuttid/service/connector/mcp | Product MCP service adapter |
| Agent GUI Connector JSX | packages/connector/renderer/src/ui | Re-express through neutral Connector props and semantic callbacks |
| Agent GUI Connector draft/prompt logic | agentGuiNode/integrations/connector | Remains Agent-owned and React-free |
The following owners do not move:
- packages/clients/market-go remains the Market-neutral generated client.
- packages/clients/tuttid-ts remains the generated daemon client owner.
- packages/events/protocol remains the unified business-event owner.
- services/tuttid/wiring.go remains the concrete product composition root.
- Cross-domain Connector files in eventstream, managedruntime, and Agent
runtime remain with those modules.
## Package and module identities
### Go
Keep five independent Connector modules:
```text
github.com/tutti-os/tutti/packages/connector/daemon/core
github.com/tutti-os/tutti/packages/connector/daemon/application
github.com/tutti-os/tutti/packages/connector/daemon/adapters/sqlite
github.com/tutti-os/tutti/packages/connector/daemon/adapters/controlplane
github.com/tutti-os/tutti/packages/connector/runtime
```
The separation is architectural rather than compatible scaffolding:
- Daemon Core is consumed by Application, Runtime, SQLite, Control Plane,
Agent Daemon, and tuttid.
- Runtime depends on Agent Daemon process mechanics, while Agent Daemon consumes
Connector Core for Composer catalog projection.
- Combining Core and Runtime would create a module release cycle.
- Concrete adapters must not become transitive dependencies of Core or Runtime.
packages/clients/market-go keeps its existing module identity and remains an
external dependency of Daemon Application.
### TypeScript
Create two public npm packages:
```text
@tutti-os/connector-contracts
@tutti-os/connector-renderer
```
Contracts exposes narrow resource/schema entries:
```text
@tutti-os/connector-contracts/authorization/v1
@tutti-os/connector-contracts/openapi/connector-market.v1.yaml
```
Renderer is one npm package with narrow responsibility entries:
```text
@tutti-os/connector-renderer/application
@tutti-os/connector-renderer/ui
@tutti-os/connector-renderer/i18n
```
Do not add a root barrel that combines Application and UI. This keeps
React-free consumers on the Application entry and makes accidental UI imports
detectable. The package remains physically discoverable by the repository's
existing packages/_/_ package globs.
The new npm and Go identities start from the current development version,
0.1.0. Old registry artifacts are not deleted and are not maintained through a
dual-publish path. This PR prepares manifests, changesets, pack verification,
and migration notes; it performs no external publication.
## Dependency direction
```text
connector/contracts
↑
├── connector/renderer/application
│ ↑
│ └── connector/renderer/ui
│ ↑
│ ├── agent-gui
│ └── desktop
│
├── connector/daemon/application
└── product adapters where the wire contract is required
connector/daemon/core
↑
├── connector/daemon/application
├── connector/daemon/adapters/sqlite
├── connector/daemon/adapters/controlplane
├── connector/runtime
├── agent/daemon
└── services/tuttid
```
Required negative boundaries:
- Contracts imports no Renderer code.
- Renderer Application imports no Renderer UI, React, DOM, Electron, Desktop,
or Agent GUI code.
- Connector packages import no Agent GUI or Desktop code.
- Daemon Core imports no concrete adapter.
- Runtime imports no Daemon Application, SQLite, or Control Plane adapter.
- Shared Connector modules import no services/tuttid code.
## Renderer responsibilities
### Application
Renderer Application remains React-free and owns:
- Host-neutral Backend, Event, Admission, and domain contracts.
- Connector Market Root and module lifecycle.
- Startup jobs and dependency-safe disposal.
- Authoritative snapshot reconciliation and mutation revision fencing.
- Market state and operation intents.
- View building and render-ready snapshots.
- Shared dialog and UI-state service.
- Declarative authorization mapping that contains no React rendering.
It obtains transport, event subscription, authentication admission, and
navigation through injected Ports. It does not construct HTTP clients, read
Electron globals, or access Desktop state.
### UI
Renderer UI is the only owner of Connector-specific React code:
```text
primitives/
ConnectorIcon
ConnectorStatus
ConnectorSelectionChip
composer/
ConnectorComposerMenu
ConnectorComposerControl
ConnectorSelectionList
palette/
ConnectorPaletteItem
```
It also owns the existing catalog, dialogs, toolbar, authorization renderer,
default i18n resources, and scoped i18n runtime.
The UI accepts Connector-owned, neutral items, labels/runtime, snapshots, and
semantic callbacks. It imports no Agent GUI draft type, AgentGUIProviderSkill
option, workspace settings store, Desktop global, or generated daemon client.
Use the repository UI System's public components, icons, and semantic tokens.
Preserve the current layout, sizing, state coverage, and interaction behavior.
Remove the Agent GUI-local Connector icon and visual implementation once all
consumers use Renderer UI.
### Agent GUI bridge
Agent GUI retains:
- AgentComposerDraft Connector block ownership.
- Connector selection updates scoped to the active Agent draft.
- Prompt block validation and connector-only submission.
- Provider/daemon capability projection into neutral Connector items.
- Connector catalog invalidation handling.
- Host navigation and settings intents.
- Slash-command and mixed Composer ordering semantics.
Move these concerns into agentGuiNode/integrations/connector as React-free
model/controller code. Mixed Agent GUI views consume Connector UI components.
For example:
- Replace the local ComposerConnectorsMenu wrapper with a neutral projection
plus ConnectorComposerMenu.
- Replace Connector-specific JSX in ComposerDraftAttachments with
ConnectorSelectionList.
- Replace Connector-specific palette row rendering with ConnectorPaletteItem.
- Keep the outer Agent Slash Palette and attachment surface in Agent GUI.
## Product adapters
### Tutti daemon
Collect the product-specific service adapters under:
```text
services/tuttid/service/connector/
├── market/
└── mcp/
```
Keep eventstream, managedruntime, agent runtime, and root wiring code with their
existing owners. Concrete construction remains in services/tuttid/wiring.go.
### Desktop
Collect renderer integration under:
```text
apps/desktop/src/renderer/src/features/connector/
├── adapters/
│ ├── desktopConnectorBackend.ts
│ ├── desktopConnectorEvents.ts
│ └── desktopConnectorAdmission.ts
├── registration/
└── index.ts
```
Desktop implements Renderer Application Ports and owns generated-client,
Electron, account, workspace-settings, and product-navigation access.
## AGENTS.md hierarchy
Generate the Connector instruction hierarchy with the writing-for-agents skill:
```text
packages/connector/AGENTS.md
packages/connector/contracts/AGENTS.md
packages/connector/daemon/AGENTS.md
packages/connector/runtime/AGENTS.md
packages/connector/renderer/AGENTS.md
```
Use progressive disclosure:
- packages/connector/AGENTS.md contains the routing map, cross-area invariants,
and strong pointers to the durable architecture documents.
- Contracts instructions define the contract-first sequence and cross-language
completion criteria.
- Daemon instructions define Core, Application, and Adapter ownership.
- Runtime instructions define execution-plane, process, path, security, and
Windows requirements.
- Renderer instructions define Application/UI separation, neutral integration
contracts, UI System/i18n ownership, and UI completion criteria.
Keep full state machines and data-flow explanations in architecture documents,
not AGENTS.md. Update packages/AGENTS.md with a trigger-first pointer requiring
Connector work to read packages/connector/AGENTS.md. Point validation rules to
the repository Testing and Static Analysis documents, adding only
Connector-specific checks locally.
## Architecture and documentation updates
Current durable documentation must describe the new shape:
- Rename docs/architecture/connector-market.md to
docs/architecture/connector.md.
- Keep docs/architecture/connector-remote-mcp.md as a focused reference.
- Update docs/architecture/README.md.
- Update docs/architecture/project-structure.md.
- Update the Connector sections in docs/architecture/agent-gui-node.md.
- Update docs/conventions/static-analysis.md.
- Update docs/conventions/npm-package-release.md.
- Update packages/AGENTS.md.
- Replace old package READMEs with the new responsibility-owner READMEs.
- Keep docs/conventions/troubleshooting/connector-market.md focused on the
user-visible Market surface and repair stale source links.
Dated specs and plans remain historical records. Add a moved/superseded note or
repair a dead link when needed; do not rewrite their historical architecture as
if the new layout existed at the time.
## Migration workflow
The change ships in one atomic PR. Logical commits may separate preparation,
moves, consumer rewrites, guards, and documentation, but the merged result has
no compatibility layer.
### 1. Freeze the baseline
- Record current module/package tests and generated checks.
- Inventory every active old path, package name, module import, OpenAPI
fragment, release entry, and static-check allowlist.
- Record the current public exports and UI behaviors covered by tests.
Completion criterion: every active reference has an owner and target mapping.
### 2. Move Contracts
- Move authorization schemas and validation into connector/contracts.
- Move the local Connector OpenAPI fragment into contracts/openapi.
- Create @tutti-os/connector-contracts with narrow subpath exports.
- Update protocol consumers and generated-check selection.
Completion criterion: Contracts builds and tests without Renderer, React, or
host dependencies; composed OpenAPI output remains semantically unchanged.
### 3. Move Go ownership
- Move Host to daemon/core.
- Move Daemon to daemon/application.
- Move Store SQLite and Control Plane to daemon/adapters.
- Move Catalog Source into daemon/application/adapters/catalog.
- Keep Runtime top-level and market-go in packages/clients.
- Rewrite module declarations, imports, replacements, and go.work entries.
- Run gofmt and go mod tidy for every affected module.
Completion criterion: all five Connector modules and services/tuttid compile
and test independently with the original dependency direction.
### 4. Build Renderer Application and UI
- Create @tutti-os/connector-renderer.
- Move React-free contracts, lifecycle, services, state, View, and
authorization mapping into application.
- Move all Connector React code and i18n into ui.
- Split mixed authorization and root export surfaces.
- Publish only /application, /ui, and /i18n.
Completion criterion: Application passes the React-free boundary guard and UI
preserves all existing state/interaction tests.
### 5. Converge Agent GUI and Desktop integration
- Extract neutral Connector UI props and callbacks.
- Move every Connector-specific JSX implementation into Renderer UI.
- Move Agent-owned non-UI logic into integrations/connector.
- Move Desktop integration into features/connector.
- Keep generated clients and workspace/product navigation in Desktop adapters.
Completion criterion: Agent GUI contains no Connector-specific JSX
implementation or local Connector visual asset; Agent draft, prompt, menu, and
event tests remain equivalent.
### 6. Converge tuttid adapters
- Move connectormarket and connectormcp into service/connector.
- Preserve eventstream, managedruntime, Agent runtime, and root wiring owners.
- Rewrite imports without changing API, event, or composition behavior.
Completion criterion: tuttid tests pass and wiring remains the only concrete
composition root.
### 7. Update repository tooling and release metadata
- Update Go lint module roots and their tests.
- Add the Control Plane module to Go lint coverage.
- Update HTTP Client Funnel scan roots for Connector Daemon adapters.
- Replace npm package dependencies and regenerate pnpm-lock.yaml.
- Update tsup entries, package files, repository directories, and Tailwind
source metadata.
- Update the OpenAPI fragment path and API-generated relevance selector.
- Update OpenAPI fragment tests.
- Replace old package names in active changesets and release conventions.
- Add and register check:connector-boundaries.
Completion criterion: changed-aware discovery selects every moved module,
package, contract, and boundary check.
### 8. Generate instructions and update documentation
- Generate the layered AGENTS.md files with writing-for-agents.
- Update current architecture, structure, testing/static-analysis, release, and
troubleshooting references.
- Add the Connector root navigation README.
Completion criterion: an Agent entering any target subtree is routed to the
correct owner rules without duplicated architecture prose.
### 9. Remove old identities
- Delete old directories after their contents are mapped.
- Remove old package exports, package names, module paths, aliases, and
compatibility barrels.
- Exclude historical specs from the active-reference search and inspect every
remaining match manually.
- Do not move dist, node_modules, or other local build artifacts.
Completion criterion: active source, configuration, CI, release metadata, and
current docs contain zero old Connector identities.
### 10. Verify
Run the focused and repository-wide validation matrix below.
Completion criterion: every command passes, generated files are clean, and
git diff contains only the planned migration.
## Required configuration updates
### Go
- go.work and go.work.sum.
- Five moved module declarations and sums.
- services/tuttid go.mod replacements and sums.
- packages/agent/daemon references to Daemon Core.
- Every Go source/test import of the old Host, Daemon, Store, Runtime, or
Control Plane module path.
- tools/scripts/run-check-changed-targets.mjs GO_LINT_MODULE_ROOTS and its
tests.
market-go remains at packages/clients/market-go. Its generated source lock and
sync target do not move.
### TypeScript and package tooling
- contracts and renderer package manifests.
- Renderer tsup and TypeScript entry configuration.
- Agent GUI and Desktop package dependencies.
- pnpm-lock.yaml.
- Desktop Tailwind source reference.
- Existing architecture guard replacement.
- Active changesets that name connector-market or
connector-authorization-protocol.
Both new npm packages remain at packages/_/_ depth, so the current workspace,
typecheck, test, changed-aware, and npm release discovery globs continue to
find them.
### OpenAPI and generated checks
- services/tuttid/api/openapi/tuttid.v1.yaml fragment path.
- tools/scripts/openapi-fragments.test.mjs package/resource expectations.
- tools/scripts/repository-checks.mjs API-contract relevance for
packages/connector/contracts/openapi.
The source contract moves, but the generated API semantics must remain
unchanged.
## Boundary enforcement
Add a repository-level command:
```sh
pnpm check:connector-boundaries
```
Register it in the repository-check boundaries group so check:changed and
check:full execute it when relevant. It must enforce at least:
```text
contracts -X-> renderer
renderer/application -X-> renderer/ui
renderer/application -X-> React, DOM, Electron, Desktop, AgentGUI
packages/connector -X-> agent-gui, desktop
daemon/core -X-> concrete adapters
runtime -X-> daemon/application, sqlite, controlplane
```
Extend the HTTP Client Funnel check to cover Connector Daemon network adapters.
Preserve existing UI, Renderer, Electron Runtime, Agent Activity Runtime, and
i18n boundary checks.
## Validation
Run commands from the repository root unless a subshell says otherwise.
### Focused Go
```sh
(cd packages/connector/daemon/core && go test ./...)
(cd packages/connector/daemon/application && go test ./...)
(cd packages/connector/daemon/adapters/sqlite && go test ./...)
(cd packages/connector/daemon/adapters/controlplane && go test ./...)
(cd packages/connector/runtime && go test ./...)
(cd services/tuttid && go test ./...)
pnpm test:go
pnpm build:go
pnpm lint:go
```
### Focused TypeScript
```sh
pnpm test:ts -- --packages-json '["@tutti-os/connector-contracts","@tutti-os/connector-renderer","@tutti-os/agent-gui","@tutti-os/desktop"]'
pnpm --filter @tutti-os/connector-contracts typecheck
pnpm --filter @tutti-os/connector-renderer typecheck
pnpm --filter @tutti-os/agent-gui typecheck
pnpm --filter @tutti-os/desktop typecheck
pnpm --filter @tutti-os/connector-contracts build
pnpm --filter @tutti-os/connector-renderer build
pnpm --filter @tutti-os/agent-gui build
pnpm --filter @tutti-os/desktop build
pnpm release:pack:check -- --packages-json '["@tutti-os/connector-contracts","@tutti-os/connector-renderer"]'
```
### Boundaries and generated contracts
```sh
pnpm check:connector-boundaries
pnpm check:ui-boundaries
pnpm check:renderer-boundaries
pnpm check:electron-runtime-boundaries
pnpm check:agent-activity-runtime-boundaries
pnpm check:i18n
pnpm check:api-generated
pnpm check:event-protocol-generated
pnpm test:tools
```
### Agent GUI behavior
Keep or relocate coverage for:
- Connector Composer loading, cached refresh, connection/authorization status,
selection, ordering, and quick-menu limits.
- Connector selection chips and removal.
- Slash Palette Connector grouping and state.
- Connector catalog projection and Host visibility gating.
- Structured Connector draft blocks, key normalization, connector-only
submission, and prompt-item projection.
- Catalog invalidation from tuttid event through Desktop to Agent GUI refresh.
There is no new cross-process E2E in this migration. Existing segmented tests
must continue to close the event and state-flow chain.
### Final repository gates
```sh
pnpm check:changed -- --dry-run
pnpm check:changed -- --push-ready
pnpm check:full
```
check:changed must select the new packages, all five Go modules, generated API
checks, boundary checks, and affected host consumers.
## Risks and mitigations
| Risk | Mitigation |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Go module release cycle | Preserve five module boundaries; keep Runtime separate from Daemon Application |
| Application accidentally gains React | Narrow subpath exports plus check:connector-boundaries |
| Renderer UI imports Agent GUI types | Neutral Connector items, snapshots, and callbacks |
| A moved module silently misses lint/test selection | Update go.work, GO_LINT_MODULE_ROOTS, relevance selectors, and their tests |
| Connector OpenAPI changes stop selecting generated checks | Add contracts/openapi to API-contract relevance |
| Network adapters bypass HTTP policy | Extend HTTP Client Funnel coverage |
| Agent prompt behavior changes during JSX extraction | Preserve Agent-owned projection and focused draft/prompt tests |
| UI appearance changes | Use existing UI System and retain current interaction tests and manual smoke comparison |
| Active changesets point to removed npm packages | Rewrite them to contracts or renderer ownership |
| External hosts cannot find new identities | Do not update external repositories now; keep old registry versions and document the replacement |
## Rollback
No database or protocol migration is introduced. Before publication, rollback
is a full PR revert:
- restore the old source tree and package/module identities;
- restore old OpenAPI fragment and tooling references;
- remove the new boundary checks and AGENTS routing added only for the new
shape.
Do not leave a partially reverted compatibility layer. If a problem is found
after merge but before publication, revert the atomic PR and repair the plan.
## Acceptance criteria
- The final directory tree matches this plan.
- All five Go modules retain the current dependency direction and pass their
tests independently.
- @tutti-os/connector-contracts contains no Renderer dependency.
- @tutti-os/connector-renderer/application contains no React or host-specific
dependency.
- Every Connector-specific React implementation lives under renderer/src/ui.
- Agent GUI owns only Agent-specific Connector projection and controller
semantics.
- Desktop and tuttid adapters are grouped under their owner-specific Connector
directories.
- market-go, tuttid-ts, and unified event definitions remain with their current
owners.
- API, event, persistence, manifest, lifecycle, and UI behavior are unchanged.
- No active old path, npm package name, Go module path, alias, or compatibility
export remains.
- Layered AGENTS.md instructions and current architecture documentation describe
and enforce the new ownership.
- Focused validation, release pack checks, check:changed --push-ready, and
check:full pass.
- The PR prepares new 0.1.0 package/module identities without publishing them
or modifying external repositories.
@@ -0,0 +1,134 @@
# Standalone Agent Tool Surface Adapter
## Background and goal
Standalone Agent windows render AgentGUI without the Workspace Workbench. The
provider-login, link, Browser Use, and file-preview paths nevertheless reused
Workspace launch contracts. Some standalone paths therefore targeted the
AgentGUI surface host (which is not a tool host) or escaped to an OS app.
The goal is to keep the semantic launch contracts shared while adapting their
presentation at the Desktop surface boundary:
- provider login opens a new Terminal tab in the right sidebar;
- ordinary HTTP links and Browser Use open the right-side Browser;
- file preview opens the right-side Files tool; only explicit reveal/Open in
Finder uses the OS;
- `reveal: false` may mount Browser without expanding the sidebar.
There is one standalone Agent window per workspace workflow. This change does
not introduce window ownership or multi-Agent-window selection.
## Current architecture and complete paths
Provider login currently follows
`AgentEnv -> provider terminal command runner -> surface.host.launchNode`. In a
Workspace window the host is a real Workbench host. In a standalone window the
AgentGUI host only represents the Agent node, so it cannot launch Terminal.
Ordinary links follow
`AgentGUI link action -> standalone link adapter -> shell.openExternal`, and
file preview follows
`workspace file preview host -> standalone presenter -> host files openFile`.
Both leave Tutti instead of selecting a sidebar tool.
Browser Use follows
`tuttid/browser request -> Electron main browser automation coordinator -> user
Browser host`, even when a ready standalone Agent Browser host exists.
## Target architecture and complete paths
Semantic requests remain surface-neutral and are resolved by a Desktop
presenter registered for the current renderer:
- login: `AgentEnv -> terminal runner -> workspace terminal-login coordinator
-> standalone presenter -> create terminal session -> add Terminal tab`;
- HTTP link: `AgentGUI link action -> workspace Browser launch coordinator ->
standalone presenter -> open Browser tab -> create/activate page`;
- file preview: `file preview surface host -> standalone presenter -> Files
launch coordinator/state -> open Files tab and select path`;
- Browser Use: `renderer submit result -> Turn surface claim -> browser
automation coordinator -> originating Agent or Workspace Browser host`. The
coordinator briefly waits when automation races ahead of the canonical Turn
claim, then creates/waits for the claimed host; unclaimed legacy Turns retain
the user/Workspace fallback.
Explicit external-link actions remain system-browser actions. Explicit
reveal/Open in Finder remains a host-files reveal action.
## Repository and module changes
Only `tutti` changes.
- Desktop main browser automation coordinator: record the validated renderer
origin for each canonical Turn, then select/create that surface's Browser;
preserve the user-host fallback for unclaimed legacy Turns.
- provider terminal runner: prefer the workspace-scoped terminal-login
coordinator and retain Workbench launch fallback.
- standalone Terminal presenter/runtime: create the session with command and
cwd, create one sidebar tab per request, and retain startup-action gating.
- standalone Browser sidebar adapter: register the shared Browser launch
coordinator, wait for the selected tab controller, and implement reveal vs
silent mount.
- standalone link routing: send ordinary HTTP actions to the Browser
coordinator; preserve explicit external actions.
- standalone file-preview presenter: select Files in the sidebar rather than
calling the OS open-file API.
- standalone Agent environment binding: bind provider launch behavior to the
right-side tool host group, not the AgentGUI surface host.
Tasks and Apps already use standalone presenters and sidebar state, so their
contracts do not change.
## Reuse and Adapter boundary
Shared semantic coordinators, terminal runtime, Browser controller, Files pane,
and provider startup-input gate are reused. Desktop owns the mode-specific
presenters and top-level focus/tab behavior. AgentGUI, provider protocol, and
daemon code do not learn about sidebar tabs, Electron windows, or OS reveal.
## Explicit non-goals
- no multi-Agent-window selection or `windowId` protocol;
- no new Browser, Terminal, Files, Tasks, or Apps implementation;
- no AgentGUI lifecycle/state changes for Desktop tool chrome;
- no change to explicit system-browser or Open in Finder actions;
- no provider authentication or credential format changes.
## Migration and compatibility
There is no persisted-data migration. Existing Workspace presentation remains
the fallback when no renderer presenter or Turn claim exists. Existing sessions
and saved layouts keep their current schema.
## Risks and rollback
The main risks are a presenter-registration race, a missing/forged Turn claim,
targeting the wrong Browser tab, and leaking a terminal session if tab creation
fails. Registration happens before the tool host is announced; Electron main
validates claims against the sender window; Browser launch waits for the exact
Turn claim and tab controller with bounded timeouts; terminal creation failure
cleans up the session.
Rollback is a single commit/MR revert. Because no schema or daemon contract is
changed, rollback needs no data repair.
## Tests and acceptance
- unit: terminal coordinator preference/fallback, new-tab creation, startup
action, link routing, file preview routing, Browser host selection and reveal
policy;
- static: Desktop typecheck and changed-file repository checks;
- build: Desktop production build;
- integration: exercise provider command through the coordinator/presenter and
Browser automation through main host selection and renderer response;
- manual acceptance: Claude login opens a new right Terminal tab; ordinary HTTP
and Browser Use stay in right Browser; file preview stays in Files; Open in
Finder uses Finder; `reveal:false` does not expand the sidebar.
## Phased implementation
1. Add the standalone Terminal presenter and bind the real tool host.
2. Route Browser, HTTP links, and Files through their shared coordinators.
3. Add contract tests, documentation, typecheck/build, and integration checks.
4. Obtain independent review, fix findings, then push one MR.
@@ -0,0 +1,238 @@
# Agent Runtime / Activity 可靠性改造方案
状态:Proposed
日期:2026-08-21
范围:`tutti` 单仓库;基于当前 `origin/main` 与 PR #2555 合并后的代码。
证据来源:飞书 bug 日志、当前代码调用链、已有回归测试;没有证据的结论明确标记为“待确认”。
## 1. 背景与目标
当前问题集中在三条边界:Provider/ACP 输出归一化、tuttid 持久化后的活动事件传播、桌面端 AgentGUI 投影。目标是让下一次执行得到正确结果或明确失败,而不是把错误隐藏、降级成成功,或只在 UI 层改文案。
### 1.1 已确认问题
| 问题 | 直接原因 | 系统性原因 | 结论可信度 |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------- |
| 高密度 `message_delta` 触发 React #185 / 更新风暴 | 事件到达时同时驱动 canonical reconcile、乐观消息和 UI 通知 | 传输适配、状态投影、通知调度边界曾经耦合,没有统一的 workspace 级批处理入口 | 高 |
| MCP 普通输出被展示为 Error | `output` 与 `error` 没有在 Provider payload、daemon projection、GUI renderer 全链路保持分离 | 各层兼容了不同的 `isError`/`is_error`/`output` 形状,缺少统一状态与字段不变量 | 高 |
| ACP `isError` 或 `is_error` 未可靠映射为失败 | 终态主要由事件类型或普通输出推断,显式错误标志未优先参与判定 | Provider 原始 payload 没有先归一化为统一的 `failed` 语义 | 高 |
| Windows 缺少 cwd 时 ACP 使用了不适合 Windows 的根路径 | 空 cwd 的跨平台 fallback 没有按 OS 选择 | 协议 cwd、进程 cwd、Provider 搜索 cwd 的责任没有被明确拆开 | 高 |
| Windows Cursor Glob/Grep 绝对路径搜索失败 | 日志证明进入 Provider 边界的绝对路径搜索失败;内置 `rg.exe` 对同类有效路径本身可用 | Provider 内部路径转换/搜索参数形状未被 Tutti 完整观测 | 边界结论高;Provider 内部具体函数待确认 |
| 会话尚未进入 canonical snapshot 时重命名失败或丢失 | UI action 直接写 session,没有等待 canonical session 出现 | 临时激活态与 canonical lifecycle 的 readiness 边界不明确 | 高 |
### 1.2 目标
1. canonical store 是会话、turn、message 和 terminal status 的唯一事实来源。
2. Provider 的 status、input、output、error 在 daemon 边界一次归一化,并保留可诊断信息。
3. 活动事件只在持久化提交后对外发布;desktop/mobile 只负责传输适配,workspace coordinator 负责顺序、批处理、断点和重连。
4. Windows 的 process cwd、protocol cwd 和 Provider 搜索路径分别有明确契约;失败时返回真实失败原因。
5. 每个修复都有跨边界回归测试,能证明“正确结果”或“明确错误”,不以“页面不报错”为验收标准。
## 2. 当前架构和完整链路
### 2.1 运行时到 UI 的读取/事件链
```text
Provider process / ACP
-> packages/agent/daemon/runtime
(standard ACP adapter, tool normalizer, Reporter)
-> services/tuttid/service/agent/ActivityProjection
-> packages/agent/store-sqlite
(canonical durable session/turn/message state)
-> post-commit publisher / agent.activity.updated
-> desktop WebSocket adapter 或 mobile DeviceLink adapter
-> packages/agent/activity-core
(workspace event coordinator, optimistic overlay, reconcile)
-> AgentSessionEngine
-> packages/agent/gui AgentGUIRuntime/selectors/projection
-> React AgentGUI tool renderers
```
关键事实:
- `DurableActivityReporter` 把 runtime event 转成 `ReportActivityInput`;tuttid wiring 将其接到 `ActivityProjection`。
- `ActivityProjection` 先写 canonical repository,再通过 `ObserveCommitted` 做事件发布、daemon-local view 和 Provider cleanup。事件回调不是持久化成功的前置条件。
- `agent.activity.updated` 是 server-to-client 的活动主题;desktop 的 `WorkspaceAgentActivityReconcileBridge` 将事件交给 `createAgentActivityWorkspaceEventCoordinator`,而不是直接在组件中改状态。
- `AgentGUIRuntime` 使用 `AgentSessionEngine` snapshot 与订阅;GUI 组件不应直接读取 WebSocket 或 Provider 原始 payload。
### 2.2 GUI action 的写链
```text
AgentGUI semantic action
-> AgentSessionEngine typed command
-> apps/desktop WorkspaceAgentActivityService / Desktop adapter
-> tuttid client / service / Host runtime boundary
-> ActivityProjection -> canonical store
-> post-commit event 或 session reconcile
-> activity-core -> AgentSessionEngine
-> GUI snapshot / selector
```
重命名的 readiness 修复位于 GUI controller:`waitForCanonicalSession` 等待 session 进入 engine 的 canonical snapshot 后,才调用 `renameSession`。这保持了写操作的 owner 仍为 engine,而不是在 UI 中新增第二份 session 状态。
### 2.3 Windows Provider 链
```text
activation / service resolves cwd
-> standard ACP ProcessSpec.CWD + ProtocolCWD
-> ACP session/new 或 resume
-> Provider Glob/Grep 或 native rg / PowerShell
-> normalized call status/input/output/error
-> Reporter -> canonical store -> event -> activity-core -> GUI
```
当前代码已经在 `standardACPProtocolCWD` 对空 cwd 做 Windows/POSIX 分支,并在 process start 日志同时记录 `cwd` 与 `protocol_cwd`。Cursor runtime preparation 还会注入 Windows 搜索策略:workspace 内使用相对路径,workspace 外或不确定时使用 native terminal 搜索。
### 2.4 直接原因与系统性原因
直接原因是一次请求或一次渲染中使用了错误的字段、路径、终态或 readiness;系统性原因是同一个事实在 Provider、daemon、transport、activity-core、engine、renderer 多层重复解释。改造重点是收窄解释边界,而不是增加更多兼容分支。
## 3. 目标架构和完整链路
### 3.1 目标链路
```text
Provider / ACP raw event
-> runtime adapter: 归一化 status + input/output/error + cwd
-> DurableActivityReporter
-> ActivityProjection: 事务内写 canonical store
-> committed activity envelope: 只发布已提交事实
-> transport adapter: WebSocket / DeviceLink,仅处理连接和反序列化
-> activity-core workspace coordinator:
顺序校验、批处理通知、乐观 overlay、gap/reconnect reconcile
-> AgentSessionEngine: 唯一前端生命周期状态
-> AgentGUIRuntime / selector / projection
-> renderer: 按 status 与 output/error 分区展示
```
### 3.2 目标不变量
- `failed` 只能来自 Provider 显式错误、`call.failed`/等价失败事件、明确的非零退出或归一化后的错误;普通 output 不能单独制造 Error 区域。
- `output` 是成功或失败调用的结果内容,`error` 是失败原因;失败时两者都可以存在,但不能互换或互相覆盖。
- canonical 写入先于对外活动事件;事件丢失、乱序、跨 session 或版本不合法时,触发 reconcile,不直接污染本地状态。
- workspace 级通知由 coordinator 的注入 scheduler 批处理;desktop 不再为同一事实新增第二个 renderer timer。
- 已提供 cwd 必须原样传递;Windows 空 cwd 使用当前进程的有效工作目录;workspace 内搜索优先相对路径,workspace 外搜索使用 Provider 已验证的 native 搜索入口。
- 不能确认 Provider 内部路径转换时,必须保留原始错误和诊断字段,并标记为 Provider 边界待确认,不能返回“搜索成功”。
### 3.3 Cursor 问题的边界
已能确认 Tutti 侧的两个防线:Windows 空 cwd 不再使用错误的 POSIX fallback;Cursor preparation 会给 Provider 明确的相对路径/native terminal 搜索策略。但现有日志没有原始 ACP tool request/response,且真实 Provider 复现曾受 quota 限制,因此“Cursor 内部将 `C:/...` 转换为空路径的具体实现位置”仍待确认。完整修复必须补齐该证据或与 Provider 维护方确认,不能仅凭 Tutti 侧 prompt policy 宣称已解决。
## 4. 仓库、服务和模块改造范围
当前证据只覆盖 `tutti` 这一仓库;未确认需要修改其他仓库。
| 范围 | 改造内容 | 不应承担的职责 |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `packages/agent/daemon/runtime` | 维护 ACP session cwd/protocol cwd;统一 `isError`/`is_error`、event type、退出状态、input/output/error 归一化;记录可脱敏的 process/tool 诊断 | 不做 GUI 文案、React 状态或桌面连接管理 |
| `packages/agent/runtimeprep` | 维护 Cursor Windows session-scoped 搜索上下文和相对路径/native terminal fallback;补充可验证的 preparation contract | 不在这里判断 Provider 返回是否成功,不复制 daemon status 逻辑 |
| `packages/agent/daemon/activity` | 将 normalized call 生成 `WorkspaceAgentMessageUpdate`,保持 status、input、output、error 字段分离 | 不读取 renderer 状态,不为每个 Provider 写 UI 分支 |
| `services/tuttid/service/agent` | 保持 `ActivityProjection` 作为 durable commit owner;统一 post-commit fanout 和 scope 校验;异常事件只请求 reconcile | 不把事件回调当作持久化成功条件,不创建第二个 canonical store |
| `services/tuttid/agent_runtime_activity_event_bridge.go` | 校验 runtime event scope、过滤 malformed/cross-scope event、限流 reconcile request,并保留可诊断日志 | 不在 event bridge 中修正业务状态或吞掉 Provider 错误 |
| `packages/agent/store-sqlite` | 当前不改 schema;继续作为 canonical durable repository | 不保存一份仅供 UI 使用的平行状态 |
| `packages/agent/activity-core` | 复用 workspace coordinator、optimistic overlay、gap/reconnect reconcile 和注入 scheduler;必要时补充 contract tests | 不依赖 WebSocket、Electron、DeviceLink 或 Provider 类型 |
| `apps/desktop` | WebSocket 生命周期、诊断、连接状态和 `WorkspaceAgentActivityReconcileBridge` 适配;所有状态变更经 engine/coordinator;保留 canonical readiness 等待 | 不新增桌面侧业务 coordinator、第二套定时器或 Provider 解析器 |
| `apps/mobile` | 只把 DeviceLink delivery 归一化后交给同一个 activity-core contract;补充移动端连接/重连适配测试 | 不复制 desktop 的事件顺序、overlay、reconcile 算法 |
| `packages/agent/gui` | 使用统一 status/output/error render data;GUI action 通过 `AgentGUIRuntime`/engine;structured fallback 有深度和长度上限 | 不直接解析 ACP 原始事件,不把异常改成普通 output |
| `packages/agent/host` | 当前无需改生命周期语义;若后续发现 session/turn lifecycle contract 缺能力,先增加 Host conformance scenario,再由 Host 提供 API | 不在 tuttid 或 desktop adapter 中重新实现 lifecycle owner |
| `services/tuttid/api/openapi` 与 clients | 当前不计划改公共 HTTP contract;若确需新增 envelope 字段,先改 OpenAPI,再生成/更新 client,并保持字段可选 | 不以未确认的日志字段直接扩展公共 API |
| `docs/architecture` / `docs/conventions/troubleshooting` | 仅在行为或边界发生新增时更新对应持久文档;本方案本身是实施入口 | 不复制代码注释或堆叠重复排障说明 |
## 5. 可复用部分与 Adapter 边界
| 可复用核心 | Adapter 输入/输出 | 边界规则 |
| ------------------------------------------------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `store-sqlite` canonical repository | 接收已校验的 projection command,返回 committed result | 只拥有 durable truth,不感知 Provider/UI |
| `ActivityProjection` + `DurableActivityReporter` | runtime normalized event -> canonical update | 归一化在 runtime;事务提交与 fanout 在 service;不把 provider raw payload 向上泄漏 |
| `activity-core` workspace coordinator + overlay | transport delivery -> normalized activity event;输出 snapshot/reconcile intent | 同一实现服务 desktop/mobile;只注入 scheduler、snapshot reader 和 dispatch capability |
| `AgentSessionEngine` + `AgentGUIRuntime` | service/adapter command port -> typed engine state | engine 负责前端 session lifecycle;组件只消费 snapshot/selector |
| ACP tool/status normalizer | Provider raw event -> stable status/input/output/error | Provider 特殊字段只在此处或其窄 adapter 解释,不能进入 GUI/shared generic model |
| OS/process path adapter | logical cwd -> process cwd/protocol cwd/native search command | Windows/POSIX 差异留在 runtimeprep/process adapter;产品核心保持 platform-neutral |
| desktop WebSocket / mobile DeviceLink adapter | transport payload + connection state -> activity-core input | 只负责连接、反序列化、诊断、analytics/presentation invalidation,不拥有业务顺序算法 |
禁止为上述边界再创建含义不清的 `shared/common/utils` 包或第二个 event center;只有出现真实的第二个消费者和稳定窄接口时才抽取新 package。
## 6. 明确不做的内容
- 不重写 AgentGUI、ACP runtime、SQLite 或整个 event stream。
- 不通过捕获异常、默认成功、清空错误字段或只改 UI 文案来“修复”问题。
- 不在当前证据不足时声称已经完成 Cursor Provider 内部绝对路径转换修复;真实 Provider request/response 和配额可用性属于待确认项。
- 不新增数据库 schema、历史数据回填或删除已有 activity;没有证据证明历史记录可安全重算。
- 不新增公共 API 字段;除非确认跨进程消费者需要,并按 OpenAPI-first 流程走兼容变更。
- 不扩大到 dormant background-task guard、未关联本调用链的 bug 表项目或其他仓库问题。
- 不把 desktop/mobile 各自实现成一套活动状态机,也不把 Provider 分支写进 `activity-core` 或 GUI。
## 7. 数据迁移和兼容策略
### 7.1 当前方案
当前改造不改变 SQLite schema、canonical record 语义或 `agent.activity.updated` 的必需字段,因此不需要数据迁移。已有 session 在下一次 start/resume 时使用新的 cwd 规则;历史已经写入的错误 status 不自动回写,因为缺少足够证据判断其真实 Provider 结果。
### 7.2 读取兼容
- 归一化同时读取 `isError` 与 `is_error`、旧 event type 别名和缺失的 output/error 字段。
- 旧记录缺少 `error` 时仍可展示已有 output;未知结构只做有上限的文本提取,不改变 terminal status。
- 新增字段只能采用 optional/additive 方式;旧 desktop/mobile 客户端忽略未知字段仍应能完成基本 reconcile。
- 若公共事件 envelope 需要版本变化,先更新 OpenAPI/事件 catalog 和 consumer contract,再同时保留旧字段一段兼容窗口;当前没有证据表明需要升版本。
- Cursor preparation 是 session-scoped;是否能在不重启 live session 的情况下重新注入上下文待确认。若不能,则只对新建或重新准备的 session 生效,并在诊断中明确这一点。
## 8. 风险与回滚方案
| 风险 | 影响 | 控制/验证 | 回滚 |
| -------------------------------------- | ----------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Provider payload 仍有未覆盖形状 | status 或 error 展示不一致 | 保留 raw-safe summary,增加 normalizer fixture 和未知字段日志 | 回退归一化提交;canonical schema 不变 |
| 严格失败判定暴露更多真实失败 | 用户看到失败数上升 | 对照 Provider 原始事件和退出状态,不以 UI 无红色为指标 | 回退代码提交,不修改历史数据 |
| coordinator 批处理造成短暂显示延迟 | UI 反馈变慢 | 测量通知次数、reconcile 延迟和最终一致性;使用现有注入 scheduler | 回退 coordinator 变更,transport contract 不变 |
| Windows cwd 仍在某个 Provider 内被重写 | 搜索继续失败 | 采集脱敏的 `cwd/protocol_cwd/tool input/output`,补真实 Provider ACP 测试 | 暂停 Cursor policy 变更,保留 native terminal fallback;等待 Provider 证据 |
| event scope/version 不兼容 | 客户端触发 reconcile 或丢更新 | malformed/cross-scope fixture、旧 payload contract test | 回退 event bridge/consumer commit,不清理 canonical 数据 |
首选回滚粒度是回退对应代码提交并重新部署,不做 destructive 数据操作;因为本方案不含 schema migration,回滚不需要 down migration。
## 9. 测试与验收标准
### 9.1 单元与契约测试
- `packages/agent/activity-core`:高密度 delta 的批处理、乱序/gap、跨 scope、重连 hydration、optimistic overlay terminal truth;保留现有 `511/511` 基线。
- `packages/agent/daemon/runtime`:Windows/POSIX 空 cwd、显式 `isError`/`is_error`、`call.failed`、非零退出、output/error 同时存在、未知 payload。
- `packages/agent/daemon/activity` 与 `services/tuttid/service/agent`:normalized call 到 canonical projection 的 status/input/output/error 映射,以及“提交后才发布”。
- `packages/agent/runtimeprep`:Cursor Windows prompt/context 使用相对路径和 native fallback;session-scoped cleanup。
- `packages/agent/gui`:普通 MCP output 不出现 Error 区域;失败调用显示真实 error;失败调用仍可显示 output;tool sequence 保持顺序;structured fallback 受深度/长度限制。
- `apps/desktop`:canonical session 尚未出现时 rename 等待;WebSocket connection state 与 coordinator 生命周期正确 dispose。
### 9.2 集成与 Windows 验收
1. 从 runtime event 开始,验证 `runtime -> ActivityProjection -> SQLite -> committed event -> desktop/mobile adapter -> activity-core -> engine -> GUI` 的端到端结果。
2. 注入连续 `message_delta`,确认通知被 workspace coordinator 合并,最终 canonical message 完整,且不产生 React #185 类重复更新风暴。
3. 注入 malformed、cross-session、gap event,确认不直接写入错误 session,而是产生受限 reconcile 请求。
4. Windows 分别验证:已提供 cwd、空 cwd、workspace 内相对搜索、workspace 外绝对路径 native 搜索;结果必须正确,失败必须带可行动错误。
5. 在真实 Cursor Provider 配额可用后,采集原始 ACP tool request/response,再验证绝对路径失败是否已由 Provider 修复;在此之前该项验收状态为“待确认”,不能标记完成。
### 9.3 验收口径
- 正确返回:canonical 数据、活动事件、engine snapshot 和 UI 展示的 status/output/error 一致。
- 正确失败:失败原因可从日志或 UI 定位,不能被转换为成功或静默丢弃。
- 兼容:旧 payload 和已有 session 可读取;不要求历史错误记录自动重算。
- 架构:没有新增第二个 canonical owner、第二套 workspace event coordinator 或 Provider 逻辑进入 GUI/activity-core。
- 当前基线已验证:activity-core 测试通过;ACP/runtimeprep/desktop 相关聚焦测试通过。部分更宽的 Windows symlink 测试受本机权限限制,需在具备权限的 Windows CI/环境补齐,不将权限失败误判为产品修复证据。
## 10. 分阶段实施顺序
### 阶段 0:证据和契约冻结
补齐 Cursor 原始 ACP request/response、真实 Provider 配额复现、live session 是否支持重新 preparation 三项证据;同时冻结 status/output/error、cwd、commit-before-publish 和 reconcile contract。未补齐的项保持“待确认”。
### 阶段 1:落地已确认的窄边界修复
以 PR #2555 当前代码为基线,完成并保持:runtime status/error 归一化、Windows ProtocolCWD fallback、Cursor Windows preparation policy、activity-core 批处理/overlay、GUI output/error 分区、canonical session readiness。每项都补上对应单元/契约测试。PR #2555 已包含其中一部分,不应重复实现。
### 阶段 2:跨层集成和 Windows gate
验证 `ActivityProjection -> committed event -> adapter -> coordinator -> engine` 全链路;加入高密度、gap、跨 scope、重连场景;在 Windows CI 或具备权限的环境验证 process cwd、native search 和真实 Provider tool call。
### 阶段 3:小范围发布与观察
观察失败分类、reconcile 频率、事件延迟、重复通知和 Windows 搜索成功率。若出现回归,按提交粒度回退;不通过修改历史 canonical 数据掩盖问题。只有真实 Provider 证据确认后,才决定是否需要 Provider 侧变更或新增兼容字段。
### 阶段 4:文档收敛
将已验证的边界写入对应 architecture/troubleshooting 文档,删除重复的临时说明;对仍未确认的 Provider 内部结论保留明确的待确认状态,直到有原始协议证据。