From 0a581030e8c5d6d4f0228d6e50ec51c6d798a068 Mon Sep 17 00:00:00 2001 From: Y1fe1Zh0u Date: Wed, 26 Aug 2026 11:38:28 +0800 Subject: [PATCH 001/339] Make application guidance enforce real ownership boundaries Backend guidance now contains failure-containment rules for Tool and Provider paths. Frontend guidance is rebuilt from the current React stack around state ownership, data-access boundaries, feature composition, typed contracts, and evidence-matched verification. Constraint: The Agent Note lifecycle tree is not yet present, so this commit records the decision until the owning process note can be established. Constraint: Existing Frontend ESLint and Prettier baselines are red outside this documentation-only diff. Confidence: high Scope-risk: moderate Directive: Do not claim lint or formatting compliance until the existing 1029 ESLint errors, 65 warnings, and 155 Prettier files are handled in separately scoped work. Tested: Frontend npm test (122 passed); npx tsc --noEmit; npm run build; git diff --cached --check Not-tested: Browser acceptance; Backend behavior; Agent Note gate because .agents/notes does not yet exist --- backend/AGENTS.md | 18 ++++ frontend/AGENTS.md | 223 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 198 insertions(+), 43 deletions(-) diff --git a/backend/AGENTS.md b/backend/AGENTS.md index 42ff8875f..465c750c3 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -150,6 +150,24 @@ instead of misclassifying them as ordinary provider or business outcomes. Test every supported source form through the real consumer-facing boundary. +## Failure containment and blocking decisions + +A Tool, Provider, integration, observer, or optional-capability failure does not +block the parent Run or unrelated work by default. Contain the failure at the +owning capability boundary, record its exact outcome, and return a bounded, +actionable error through the public result contract so the model or owning +workflow can decide the next action. + +Blocking a Turn, Run, downstream handler, or unrelated capability is an +explicit product and Runtime contract. Before introducing new blocking +semantics, identify why safe continuation is impossible, document the affected +contract and recovery behavior, and confirm the decision with the user. + +Security or authorization denial, durable-state corruption, protocol +invalidity, and uncertain irreversible side effects may fail closed. Do not use +these exceptions to turn ordinary Tool or Provider failures into global +failures. + ## State publication Publish events, notifications, cache updates, projections, and user-visible diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 3a7116ffe..c27466c40 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -1,51 +1,188 @@ -# Frontend AGENTS.md — Clawith Frontend Guidelines +# AGENTS.md — Clawith Frontend ---- +These frontend-specific rules apply to `frontend/**` and supplement the +repository-wide [conventions](../AGENTS.md#2-conventions). -## 1. Subsystem Overview +The Frontend is a React 19 and TypeScript web application built with Vite. It +provides the user-facing interfaces for configuring, operating, and observing +agents. It consumes Backend and Runtime contracts but does not own execution +lifecycle or security decisions. -**Stack**: React 18, TypeScript, Vite, Tailwind CSS, shadcn/ui. -**Root Spec**: Extended from root [`AGENTS.md`](file:///Users/alex/Documents/Code/dataelem/Clawith/AGENTS.md). +Project scripts and dependencies are defined in `package.json`; +`package-lock.json` records the resolved dependency graph. Application source +lives in `src/`, Frontend tests live in `tests/`, static assets live in +`public/`, and `dist/` is generated build output. ---- +## Commands -## 2. Common Commands - -From `frontend/` directory: +Run Frontend commands from `frontend/`: | Action | Command | |---|---| -| Run Dev Server | `npm run dev` | -| Type Check | `npx tsc --noEmit` | -| Run Linter | `npm run lint` | -| Build Production Bundle | `npm run build` | - ---- - -## 3. Frontend Hard Rules (P0) - -- **TypeScript Only**: Functional components only. Class components are strictly prohibited. -- **Single File Line Limit**: File length MUST NOT exceed 600 lines. Split into sub-components or custom hooks when approaching limit. -- **Interface vs Type**: Use `interface` for component Props and public API structures; use `type` for internal unions/tuples. -- **Naming Conventions**: - - Components: `PascalCase` - - Utilities & Hooks: `camelCase` (hooks MUST start with `use`) - - Event Handlers: Internal handler functions `handle` (e.g., `handleSubmit`), prop callbacks `on` (e.g., `onSubmit`). -- **Export Style**: Named exports ONLY (`export function ComponentName`). Default exports (`export default`) are forbidden. -- **HTTP Client Wrapper (C4)**: NEVER `import axios` directly in UI components or pages. Always use the unified request module (`src/api/request.ts`). -- **No Unexplained `any`**: Avoid `any`. If unavoidable due to external library constraints, append `// eslint-disable-next-line @typescript-scope` with a explicit reason on the preceding line. -- **Comment Language**: Write all code comments in clear English. - ---- - -## 4. UI & Aesthetics Guidelines - -- **Design System**: Use Tailwind CSS and shadcn/ui components for consistent design tokens. -- **Responsive Layout**: Ensure layouts adapt gracefully to desktop and mobile viewports. -- **Micro-Interactions**: Use smooth CSS transitions and hover states for interactive elements. - ---- - -## 5. Lifecycle Ownership - -Frontend-specific lifecycle ownership and cleanup rules will be defined here. +| Install locked dependencies | `npm ci` | +| Run the development server | `npm run dev` | +| Run a focused test file | `node --test tests/.test.mjs` | +| Run the complete Frontend test suite | `npm test` | +| Run static type checks | `npx tsc --noEmit` | +| Run lint checks | `npm run lint` | +| Check formatting | `npm run format:check` | +| Format supported files | `npm run format` | +| Build the production bundle | `npm run build` | + +Use focused tests during development. Run the complete Frontend suite and +production build when the affected contracts cross multiple Frontend areas or +change assembled user-visible behavior. + +## Application layout + +```text +package.json Project scripts and dependency declarations. +package-lock.json Locked npm dependency graph. +index.html Vite HTML entry document. +vite.config.ts Development-server and production-build configuration. +tsconfig.json TypeScript project and strictness configuration. +eslint.config.js Frontend lint configuration. +public/ Static files copied into the built application. +tests/ Frontend contract, behavior, and regression tests. +src/main.tsx React application bootstrap. +src/App.tsx Top-level providers and route composition. +src/pages/ Route-level product screens and feature composition. +src/components/ Reusable presentation and interaction components. +src/hooks/ Shared React hooks. +src/services/ Backend API and external-service client boundaries. +src/stores/ Shared client-side state stores. +src/types/ Shared TypeScript types. +src/i18n/ Localization setup and resources. +src/styles/ Shared style and theme definitions. +src/utils/ Shared pure helpers. +src/assets/ Source-controlled assets imported by the application. +``` + +Detailed feature structure belongs to the nearest path-specific instruction or +owning architecture document, not this file. + +## State ownership + +Each Frontend fact has one state owner. Do not dual-write the same committed +business fact into React Query, Zustand, component state, and browser storage. + +- Remote Backend data belongs to the React Query cache. Mutations update or + invalidate the owning query. +- Cross-route or remount-surviving client interaction state belongs to an + owning Zustand store. +- State used only by one mounted component or feature subtree remains local + React state. +- A user-editable draft may have local state because it is not yet the committed + server fact. Define how the draft initializes, saves, resets, and responds to + a server refresh. +- Durable browser preferences and credentials are accessed through their owning + store or utility. Do not scatter independent `localStorage` or + `sessionStorage` reads and writes across components. +- Backend, Runtime, WebSocket, SSE, and shared-event subscriptions belong to an + owning service or feature hook. Business components consume its values and + callbacks rather than opening a second subscription. The owner handles + reconnection, ordering, deduplication, cancellation, and cleanup. +- Realtime events update or invalidate the same owner used by ordinary reads; + they do not create a second realtime-only representation. + +Derived display values remain pure computations over their authoritative state; +do not persist or subscribe to another independently updated copy. + +## Component and data-access boundaries + +Components render product state and coordinate user interaction. Backend access, +authentication headers, endpoint construction, transport errors, and response +parsing belong to `src/services/` or an owning feature hook; do not add raw +`fetch()` calls or direct credential reads to business components. + +React Query hooks own remote reads, mutations, cache keys, invalidation, and +loading/error state. Service functions return typed application values or a +documented application error, not raw `Response` objects that force each +consumer to reinterpret the transport contract. + +Pass components the values and callbacks they need. Do not pass an entire +service, store, Runtime object, or transport client merely to avoid defining the +component contract. + +## Feature and presentation boundaries + +Route pages and feature-level containers own product-flow orchestration. +Reusable presentation components receive typed values, display state, and +callbacks through explicit props; they do not fetch data, interpret Backend or +Runtime lifecycle, mutate shared stores directly, or coordinate unrelated +features. + +Keep feature-specific components, hooks, services, types, and utilities close +to their owning feature. Move code into a shared directory only after a current +second consumer proves the shared contract. Do not create generic components or +hooks for hypothetical reuse. + +When a page becomes large, split it by owned responsibility and data flow, not +by arbitrary line ranges or visual fragments that still require the parent to +pass its entire state. + +## Testing + +Prefer behavior tests that execute the owning service, reducer, state +transition, or utility and assert its public result. Use source-text contract +tests only for narrow static constraints that cannot yet be exercised through +the current harness; do not use regex matches as evidence that a component +renders correctly or that a user journey works. + +Each test asserts the layer it owns: + +- Service and state tests cover data transformation, ordering, deduplication, + cache updates, error normalization, and lifecycle transitions. +- Component or browser validation covers rendered content, interaction, focus, + scrolling, responsive layout, and navigation. +- `npm run build` proves TypeScript compilation and production bundling, not + user-visible behavior. + +When a change affects browser-only behavior that the automated harness cannot +exercise, validate it in a real browser and report the automation gap. Do not +describe a source-contract match or successful build as browser acceptance. + +Run the focused owning test during development. Add `npm run lint`, +`npm run format:check`, and `npx tsc --noEmit` for changed Frontend code; add the +production build when the change affects application composition, routing, +assets, styles, build configuration, or assembled user-visible behavior. + +## TypeScript contracts + +Keep `strict` TypeScript and `noImplicitAny` enabled. New and changed component +props, hook results, service inputs and outputs, store state, events, and Backend +response models use explicit types. + +Treat external JSON, browser messages, storage values, and third-party payloads +as `unknown` until the owning boundary validates or narrows them. Do not use +`any`, broad index signatures, unchecked casts, non-null assertions, or optional +fields merely to silence a mismatch. When an exception is unavoidable, keep it +at the narrowest boundary and explain why the precise type is unavailable. + +Define a shared type at the layer that owns the contract. Components and feature +consumers import that type instead of recreating local variants of the same +Backend, Runtime, or state shape. + +Handle closed state and event unions exhaustively. Extensible external inputs +must define explicit unknown-value behavior rather than falling through an +accidental default. + +## Runtime and mutation presentation + +Render Backend and Runtime states according to their documented contracts. Do +not infer completion, success, permission, delivery, or recoverability from an +HTTP success, request acceptance, missing error, assistant text, local timer, or +optimistic UI state. + +Keep accepted, queued, running, waiting, completed, failed, cancelled, +synchronized, and delivered outcomes distinct when the Backend contract +distinguishes them. A mutation invalidates or updates the owning React Query +state only from its documented committed result. + +Use optimistic UI only for reversible presentation or interaction state with a +defined rollback. Do not optimistically publish irreversible external effects, +Runtime completion, permission changes, or durable business results. + +Preserve canonical Backend error identity, safe message, code, trace, Run, and +retryability fields when available. Do not classify errors by matching English +message fragments. From 34f1f663f19e48892d1f1eabad14625b03fd2552 Mon Sep 17 00:00:00 2001 From: Y1fe1Zh0u Date: Wed, 26 Aug 2026 13:46:44 +0800 Subject: [PATCH 002/339] Make engineering decisions durable across agent sessions Repository, Backend, and Frontend instructions now use one-paragraph source formatting and scoped ownership rules. The new Agent Note lifecycle preserves non-trivial decision rationale across proposed, implemented, rejected, and archived states while keeping code, Notes, and commit history aligned. Constraint: Testing Policy, its Testing Agent Note, and the pre-push Skill remain uncommitted until their wording is reviewed separately. Confidence: high Scope-risk: moderate Directive: Do not add root links to testing or pre-push guidance until those artifacts are reviewed and committed together. Tested: Prettier check for all changed instruction and Agent Note Markdown; relative-link validation; git diff --cached --check Not-tested: Product behavior; pre-push enforcement; Agent Note CI gates --- .agents/notes/AGENTS.md | 7 + .agents/notes/README.md | 64 ++++++++ .agents/notes/archived/AGENTS.md | 5 + .agents/notes/implemented/AGENTS.md | 5 + .../2026-08-26-agent-note-lifecycle.md | 31 ++++ .agents/notes/proposed/AGENTS.md | 5 + .agents/notes/rejected/AGENTS.md | 5 + AGENTS.md | 68 +++----- backend/AGENTS.md | 132 ++++------------ frontend/AGENTS.md | 146 +++++------------- 10 files changed, 206 insertions(+), 262 deletions(-) create mode 100644 .agents/notes/AGENTS.md create mode 100644 .agents/notes/README.md create mode 100644 .agents/notes/archived/AGENTS.md create mode 100644 .agents/notes/implemented/AGENTS.md create mode 100644 .agents/notes/implemented/process/2026-08-26-agent-note-lifecycle.md create mode 100644 .agents/notes/proposed/AGENTS.md create mode 100644 .agents/notes/rejected/AGENTS.md diff --git a/.agents/notes/AGENTS.md b/.agents/notes/AGENTS.md new file mode 100644 index 000000000..e5002fe5e --- /dev/null +++ b/.agents/notes/AGENTS.md @@ -0,0 +1,7 @@ +# AGENTS.md — Agent Notes + +These rules apply to `.agents/notes/**` and supplement the repository-wide [instructions](../../AGENTS.md). + +Before creating a Note, search active Notes for an existing owner or a decision that the new work supersedes. Update the owner when the decision is unchanged; create and cross-link a new Note when the decision changes. + +Follow the lifecycle, classification, format, and alignment rules in [`README.md`](README.md). Do not copy current architecture or product documentation into a Note; link the owning source and record only the durable decision rationale, consequences, and verification contract. diff --git a/.agents/notes/README.md b/.agents/notes/README.md new file mode 100644 index 000000000..d3d6ee16d --- /dev/null +++ b/.agents/notes/README.md @@ -0,0 +1,64 @@ +# Agent Notes + +An Agent Note records a durable engineering decision: the problem it addresses, the chosen decision, the alternatives actually considered, the consequences, and the evidence that verifies the result. + +Agent Notes do not replace product requirements, current architecture documentation, implementation plans, test reports, incident records, or commit history. They own why an engineering decision exists and what was deliberately given up. + +## Path and classification + +Every Agent Note uses this path: + +```text +{lifecycle}/{class}/yyyy-mm-dd-topic.md +``` + +The lifecycle is one of: + +- `proposed` — the decision is under discussion or implementation and has not become current repository behavior. +- `implemented` — the decision has shipped and the Note describes current repository behavior in the present tense. +- `rejected` — the proposal was declined and remains useful because it prevents a plausible repeated mistake. +- `archived` — a frozen historical snapshot of an implemented decision that no longer needs current-fact maintenance. Archived Notes are not current authority. + +The class is one of: + +- `architecture` — source structure, ownership, boundaries, runtime vocabulary, or durable execution semantics. +- `bug-fix` — a defect whose cause, contract, or prevention is likely to be revisited. +- `feature` — a product or platform capability decision. +- `process` — development, documentation, review, release, or operational workflow. +- `simplification` — removal, consolidation, or reduction of owned complexity. +- `testing` — test strategy, evidence boundaries, harnesses, or required gates. + +## When to write one + +A change is non-trivial when it alters observable behavior, architecture, ownership, a shared contract, Runtime semantics, lifecycle, persistence, configuration, compatibility, security, permissions, testing strategy, CI, release behavior, or another engineering decision a maintainer may reasonably revisit. + +Update the Agent Note that already owns the decision. Create a new Note only when no current Note owns it or when the decision itself changes. Purely mechanical or strictly local changes with no behavioral, contractual, architectural, or process effect are exempt. + +Agent Note work begins when the decision is discovered, not at Push time. The pre-push workflow is the final enforcement point: it inspects the complete outgoing change and blocks the Push when a required owning Note is missing or contradicts the code or commit history. + +## Required format + +Every active Agent Note begins with: + +```markdown +# Agent Note: + +Status: proposed | implemented | rejected — <reason> +``` + +Every Note opens with `## Problem` and includes `## Alternatives considered`. Lifecycle-specific content follows: + +- `proposed`: `## Proposal`, then plans, acceptance criteria, risks, and open questions only when they materially help decide or implement the proposal. +- `implemented`: `## Decision`, `## Consequences`, and the relevant verification evidence or named gaps. It describes current behavior, not a migration diary. +- `rejected`: retain the proposal and alternatives; put the rejection verdict on the `Status:` line. +- `archived`: retain `Status: implemented`, add `Archived: YYYY-MM-DD`, and freeze the file permanently. + +Alternatives are recorded, never invented. State what each real alternative would have changed and why it lost. + +## Updating and superseding decisions + +Keep an implemented Note's paths, names, defaults, and mechanisms aligned with the code when the decision itself has not changed. Do not append change history; rewrite stale current facts in place. + +Do not edit an existing Note into the opposite decision. Create a new proposed or implemented Note, cross-link both decisions, and retain the old rationale. Archive an implemented Note only when it is no longer useful as current guidance. + +Code, the owning Agent Note, and commit history must agree. Code implements the decision, the Note owns durable rationale and the current contract, and the commit records the intent, scope, and verification of the concrete change. diff --git a/.agents/notes/archived/AGENTS.md b/.agents/notes/archived/AGENTS.md new file mode 100644 index 000000000..039fc30dd --- /dev/null +++ b/.agents/notes/archived/AGENTS.md @@ -0,0 +1,5 @@ +# AGENTS.md — Archived Agent Notes + +Archived Agent Notes are frozen historical snapshots, not current authority. Never edit, reformat, move, delete, or repair a sealed archived Note. Record new facts and decisions in an active Note or current documentation. + +Archiving may only move an implemented Note into the matching archived class, add `Archived: YYYY-MM-DD` below `Status: implemented`, and repair inbound links. diff --git a/.agents/notes/implemented/AGENTS.md b/.agents/notes/implemented/AGENTS.md new file mode 100644 index 000000000..a6f34dc86 --- /dev/null +++ b/.agents/notes/implemented/AGENTS.md @@ -0,0 +1,5 @@ +# AGENTS.md — Implemented Agent Notes + +Implemented Agent Notes describe decisions that have shipped. Keep their paths, names, defaults, mechanisms, and verification facts aligned with current code in the same change that moves those facts. + +Update factual realization in place, but do not rewrite the decision or its rationale into a different choice. A reversal requires a new Agent Note and cross-links between the decisions. diff --git a/.agents/notes/implemented/process/2026-08-26-agent-note-lifecycle.md b/.agents/notes/implemented/process/2026-08-26-agent-note-lifecycle.md new file mode 100644 index 000000000..1f22162a1 --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-26-agent-note-lifecycle.md @@ -0,0 +1,31 @@ +# Agent Note: Agent Note lifecycle and decision alignment + +Status: implemented + +## Problem + +Clawith requires non-trivial changes to preserve their engineering rationale, but the repository had no durable location, lifecycle, classification, or format for those decisions. Commit messages alone describe one concrete change and ordinary documentation describes current facts, so neither reliably preserves the alternatives and consequences that future maintainers may revisit. + +## Decision + +Engineering decisions live under `.agents/notes/{lifecycle}/{class}/` using the rules in [the Agent Notes README](../../README.md). The lifecycle is proposed, implemented, rejected, or archived; the closed class set is architecture, bug-fix, feature, process, simplification, and testing. + +Non-trivial work creates or updates its owning Note while the decision is made. The pre-push workflow is the final enforcement point and must reject an outgoing change whose required Note is missing or inconsistent with the code and commit history. + +Implemented Notes stay aligned with current factual realization without accumulating change narration. Decision reversals use a new cross-linked Note; archived Notes are frozen and are not current authority. + +## Alternatives considered + +**Keep rationale only in commit messages.** Rejected because one decision may span several commits and later factual updates, while commit history is a poor current owner for alternatives and consequences. + +**Store decisions under ordinary `docs/`.** Rejected because project and architecture documentation own current human-facing facts, while Agent Notes have a separate decision lifecycle and maintenance contract. + +**Generate Notes automatically at Push time.** Rejected because a script cannot reliably determine engineering intent or invent real alternatives, and generated prose would turn an enforcement checkpoint into the source of the decision. + +## Consequences + +Non-trivial changes now carry a reviewable decision record alongside code and commit history. The repository still needs pre-push, review, and CI checks for semantic presence, classification, format, and archived-note immutability. + +## Verification + +The initial tree includes the canonical README, scoped instructions for active and archived Notes, and this implemented process decision. Mechanical gates and the pre-push integration remain explicit follow-up work. diff --git a/.agents/notes/proposed/AGENTS.md b/.agents/notes/proposed/AGENTS.md new file mode 100644 index 000000000..aba238500 --- /dev/null +++ b/.agents/notes/proposed/AGENTS.md @@ -0,0 +1,5 @@ +# AGENTS.md — Proposed Agent Notes + +Proposed Agent Notes describe decisions that are still under discussion or implementation. They are not current repository authority and must not be cited as proof that a behavior has shipped. + +Keep the Problem, Proposal, real alternatives, acceptance contract, risks, and open questions aligned with the decision being evaluated. When the decision ships, move the Note to the matching `implemented/<class>/` path, set `Status: implemented`, and rewrite proposal-era wording into the current Decision and Consequences. When the proposal is declined, move it to the matching `rejected/<class>/` path and record the reason on the Status line. diff --git a/.agents/notes/rejected/AGENTS.md b/.agents/notes/rejected/AGENTS.md new file mode 100644 index 000000000..dbb76fc3a --- /dev/null +++ b/.agents/notes/rejected/AGENTS.md @@ -0,0 +1,5 @@ +# AGENTS.md — Rejected Agent Notes + +Rejected Agent Notes retain declined proposals only while their rationale prevents a plausible repeated mistake. Keep the original Problem, Proposal, and Alternatives considered; record the rejection reason on the `Status:` line. + +Do not rewrite a rejected Note into a new proposal or current decision. Create a new cross-linked Note when changed evidence justifies reconsideration. Delete a rejected Note when it no longer carries durable preventive value; rejected Notes do not move into the implemented archive. diff --git a/AGENTS.md b/AGENTS.md index d7c7fd46f..2c964f3d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,62 +30,38 @@ Each behavior-driving fact has one authoritative owner. Other layers may submit - **Lifecycle ownership is explicit.** Every registration, task, subscription, connection, or resource that outlives the current operation has one owner, defined termination conditions, and cleanup paths for success, failure, and cancellation. - **Runtime responsibilities are documented.** Every capability or subsystem with an independent runtime responsibility must document the authoritative facts and relationships it owns, how those facts change, and how their correctness is verified. Do not infer runtime health from the presence of code, configuration, services, or UI state. - **State and protocol variants are explicit.** Treat internal lifecycle states and shared contracts as closed unless they are deliberately designed for extension. Update every producer and consumer when a closed set changes, and define explicit unknown-value behavior for extensible inputs. -- **Model-visible inputs are traceable.** Every input that can affect a model decision must have an identifiable source and be attributable to the corresponding Run. Do not inject transient context that cannot later be inspected or reconstructed. See [`docs/model-visible-inputs.md`](docs/model-visible-inputs.md). +- **Model-visible inputs are traceable.** Every input that can affect a model decision must have an identifiable source and be attributable to the corresponding Run. Do not inject transient context that cannot later be inspected or reconstructed. - **Keep the Runtime core generic.** The Agent Runtime core may change while its execution model is being completed, but core changes must define general execution semantics rather than product-, integration-, UI-, or capability-specific behavior. Add specialized behavior through its owning Tool, Skill, Provider, Channel, Hook, or service boundary. Document and test every change to the execution model. - **New state machines require an independent owner and need.** Do not introduce a state machine merely to represent workflow steps, UI progress, or a lifecycle already owned elsewhere. A new state machine must correspond to an independently identified object with authoritative transitions and a current behavioral consumer. - **Capability boundaries require real participants.** Introduce a shared capability contract only when it has a current provider and consumer. Keep roles together when they change for the same reason; separate them only when their responsibilities and evolution are genuinely independent. - **Resolve policy before execution.** Defaults, configuration precedence, and policy choices must be resolved explicitly by their owning layer before an operation executes. Execution code consumes resolved inputs and must not hide additional policy decisions in fallbacks. - **Misconfiguration fails at the earliest authoritative point.** Reject an invalid or missing configuration as soon as its owning layer has enough information to determine the error. Do not silently skip the configured behavior, invent a fallback, or defer a known failure into execution. - **Validate at trust boundaries.** Use static types for same-process internal contracts and avoid duplicating runtime validation between already typed layers. Validate data when it enters from configuration, HTTP or WebSocket requests, model or Tool JSON, persistence, files, workers, processes, and external integrations. -- **Data access is bounded and evidence-driven.** Query and loading paths must - define their expected cardinality and enforce filtering, pagination, batching, - and result limits at the layer that owns the complete data operation. Avoid - per-item queries, repeated full materialization, and loading unbounded data - for downstream filtering. -- **Caches require ownership and measured need.** Introduce caching only after - identifying repeated expensive work on a real access path. Every cache must - define its authoritative source, owner, key scope, invalidation rule, capacity - bound, and freshness behavior. +- **Data access is bounded and evidence-driven.** Query and loading paths must define their expected cardinality and enforce filtering, pagination, batching, and result limits at the layer that owns the complete data operation. Avoid per-item queries, repeated full materialization, and loading unbounded data for downstream filtering. +- **Caches require ownership and measured need.** Introduce caching only after identifying repeated expensive work on a real access path. Every cache must define its authoritative source, owner, key scope, invalidation rule, capacity bound, and freshness behavior. - **Ignored failures are narrow and explained.** Catch only the single operation whose specific failure may be ignored, and state what is being ignored and why the primary outcome remains safe. Never use an empty or broad catch to hide unrelated failures. - **Tests enforce behavior, not product truth.** A passing test proves that the implementation matches its asserted behavior; it does not prove that the asserted behavior matches the current product or architecture contract. Update obsolete tests together with an explicitly approved contract change, and never change an expectation merely to make a failure disappear. -- **Non-trivial changes keep code, Agent Notes, and commit history aligned.** Any change to behavior, architecture, a shared contract, Runtime semantics, persistence, security, permissions, compatibility, or engineering process must add or update its owning Agent Note in the same change. The code implements the decision, the Agent Note owns its durable rationale and current contract, and the commit message records the intent, scope, and verification of this change. These three records must not contradict one another. Update an existing owning note instead of creating a duplicate; only mechanical or strictly local changes are exempt. +- **Non-trivial changes keep code, Agent Notes, and commit history aligned.** Any change to behavior, architecture, a shared contract, Runtime semantics, persistence, security, permissions, compatibility, or engineering process must add or update its owning Agent Note in the same change. The code implements the decision, the Agent Note owns its durable rationale and current contract, and the commit message records the intent, scope, and verification of this change. These three records must not contradict one another. Update an existing owning note instead of creating a duplicate; only mechanical or strictly local changes are exempt. Follow the [Agent Note rules](.agents/notes/README.md). ## 3. Change Discipline -- Keep each change scoped to one intent. Do not mix structural refactoring, - behavior changes, compatibility work, and unrelated cleanup. -- Preserve verified behavior unless the task explicitly changes the owning - product or architecture contract. -- Before introducing an abstraction, identify the current owner and consumer. - Delete obsolete code, reuse the existing owner when it already fits, and move - misplaced behavior back to that owner while removing bypass paths. Add a new - layer only when it has an independently changing responsibility and a current - consumer. -- **Delete verified dead code.** Once code, configuration, tests, compatibility - paths, or documentation are confirmed to have no current contract or - production consumer, remove them in the same change. Do not keep - commented-out implementations, speculative fallbacks, or tests that only - preserve deleted behavior. +- Keep each change scoped to one intent. Do not mix structural refactoring, behavior changes, compatibility work, and unrelated cleanup. +- Preserve verified behavior unless the task explicitly changes the owning product or architecture contract. +- Before introducing an abstraction, identify the current owner and consumer. Delete obsolete code, reuse the existing owner when it already fits, and move misplaced behavior back to that owner while removing bypass paths. Add a new layer only when it has an independently changing responsibility and a current consumer. +- **Delete verified dead code.** Once code, configuration, tests, compatibility paths, or documentation are confirmed to have no current contract or production consumer, remove them in the same change. Do not keep commented-out implementations, speculative fallbacks, or tests that only preserve deleted behavior. - Preserve unrelated working-tree changes and user-owned files. - Use repository-relative paths in code, documentation, and instructions. -- When ownership or a boundary changes, update the nearest path-specific - `AGENTS.md` and the corresponding durable documentation. -- Do not add fallback or compatibility paths without a documented reason, - regression coverage, and a removal condition. -- Keep source facts, test evidence, CI evidence, deployment evidence, and - live-system evidence clearly separated. +- When ownership or a boundary changes, update the nearest path-specific `AGENTS.md` and the corresponding durable documentation. +- Do not add fallback or compatibility paths without a documented reason, regression coverage, and a removal condition. +- Keep source facts, test evidence, CI evidence, deployment evidence, and live-system evidence clearly separated. ## 4. Type Checking Everything compiles under `strict: true` with `noImplicitAny`; every remaining `any` explains why narrowing is infeasible. -Public interfaces must be usable without reading their implementation. Types -define structure; owning documentation defines non-obvious behavior, failure, -side effects, ownership, timing, cancellation, and durability. +Public interfaces must be usable without reading their implementation. Types define structure; owning documentation defines non-obvious behavior, failure, side effects, ownership, timing, cancellation, and durability. -Every new or changed automated rule must include positive and negative coverage: -valid cases pass, and representative invalid cases fail for the intended -reason. +Every new or changed automated rule must include positive and negative coverage: valid cases pass, and representative invalid cases fail for the intended reason. ## 5. Quick Command Reference @@ -121,25 +97,19 @@ After code changes, verification scope is determined by the affected contracts a Match evidence to the surface. -Use [`docs/testing.md`](docs/testing.md) to select verification by changed contract. Start with focused checks and expand only when the change crosses a documented boundary. +Select verification by changed contract. Start with focused checks and expand only when the change crosses a documented boundary. -Run checks before pushes via [`clawith-pre-push-checks`](.agents/skills/clawith-pre-push-checks/SKILL.md) and report the exact commands and results. After rebasing, merging, resolving conflicts, or otherwise synchronizing a branch, immediately rerun the checks affected by the resulting diff. Do not merge while required checks are failing. +Run relevant checks before pushes and report the exact commands and results. After rebasing, merging, resolving conflicts, or otherwise synchronizing a branch, immediately rerun the checks affected by the resulting diff. Do not merge while required checks are failing. ## Communication - Lead with the conclusion, result, or blocker. -- Use direct, concrete language and name the actual actor, fact, file, command, - API, state, or behavior. +- Use direct, concrete language and name the actual actor, fact, file, command, API, state, or behavior. - Separate verified repository facts, inference, and unverified live behavior. - Do not narrate internal reasoning, tool choreography, or review history. -- Report only commands and checks actually run, together with relevant - verification gaps. -- Keep responses concise unless risk, ambiguity, or the user requests more - detail. +- Report only commands and checks actually run, together with relevant verification gaps. +- Keep responses concise unless risk, ambiguity, or the user requests more detail. ## Editing these instructions -Keep repository-wide instructions concise, self-contained, and linked to their -owning documentation. Put path-specific rules in the nearest nested -`AGENTS.md`, and do not duplicate rules across instruction files. Add or expand -a root rule only when it must remain available across the repository. +Keep repository-wide instructions concise, self-contained, and linked to their owning documentation. Put path-specific rules in the nearest nested `AGENTS.md`, and do not duplicate rules across instruction files. Add or expand a root rule only when it must remain available across the repository. diff --git a/backend/AGENTS.md b/backend/AGENTS.md index 465c750c3..eeae07514 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -1,22 +1,17 @@ # AGENTS.md — Clawith Backend -These backend-specific rules apply to `backend/**` and supplement the -repository-wide [conventions](../AGENTS.md#2-conventions). +These backend-specific rules apply to `backend/**` and supplement the repository-wide [conventions](../AGENTS.md#2-conventions). -The Backend is a Python 3.11+ FastAPI application built on SQLAlchemy's -asynchronous APIs, PostgreSQL, Redis, and LangGraph with PostgreSQL -checkpoints. It contains the Agent Runtime, product APIs, persistence, -background execution, and external integrations. +The Backend is a Python 3.11+ FastAPI application built on SQLAlchemy's asynchronous APIs, PostgreSQL, Redis, and LangGraph with PostgreSQL checkpoints. It contains the Agent Runtime, product APIs, persistence, background execution, and external integrations. -Project metadata and dependency declarations are defined in `pyproject.toml`; -`uv.lock` records the resolved dependency graph. +Project metadata and dependency declarations are defined in `pyproject.toml`; `uv.lock` records the resolved dependency graph. ## Commands Run Backend commands from `backend/`: | Action | Command | -|---|---| +| --- | --- | | Install project and development dependencies | `uv sync --extra dev` | | Run the development server | `uv run uvicorn app.main:app --reload --port 8000` | | Run a focused test file | `uv run --extra dev pytest tests/<test_file>.py` | @@ -25,12 +20,9 @@ Run Backend commands from `backend/`: | Run static type checks | `uv run --extra dev pyright app` | | Apply database migrations | `uv run alembic upgrade head` | -Use focused Pytest targets during development. Run the complete Backend suite -only when the affected contracts cross multiple Backend areas or when required -by the repository testing policy. +Use focused Pytest targets during development. Run the complete Backend suite only when the affected contracts cross multiple Backend areas or when required by the repository testing policy. -Read [`alembic/AGENTS.md`](alembic/AGENTS.md) before creating or editing a -database migration. +Read [`alembic/AGENTS.md`](alembic/AGENTS.md) before creating or editing a database migration. ## Application layout @@ -56,138 +48,70 @@ app/core/ Cross-cutting security, permissions, errors, events, logging, and app/scripts/ Application maintenance, bootstrap, backfill, and migration tools. ``` -Read the nearest nested `AGENTS.md` before modifying a specialized subtree. -Detailed module structure belongs to that subtree's instruction or owning -architecture document, not this file. +Read the nearest nested `AGENTS.md` before modifying a specialized subtree. Detailed module structure belongs to that subtree's instruction or owning architecture document, not this file. ## Async lifecycle -Represent one asynchronous operation with one lifecycle controller or -transaction. Readiness, cancellation, disposal, reservation, and sentinel state -remain in that owner unless they describe an independently owned object or -settlement point. Do not split one operation into parallel lifecycle state -machines. +Represent one asynchronous operation with one lifecycle controller or transaction. Readiness, cancellation, disposal, reservation, and sentinel state remain in that owner unless they describe an independently owned object or settlement point. Do not split one operation into parallel lifecycle state machines. ## Lifecycle verification -Tests for registration, cancellation, shutdown, and cleanup must observe the -owned resource reaching its terminal or removed state. Asserting only that -`cancel()`, `close()`, `dispose()`, or a cleanup callback was invoked is not -sufficient evidence that work stopped or resources were released. +Tests for registration, cancellation, shutdown, and cleanup must observe the owned resource reaching its terminal or removed state. Asserting only that `cancel()`, `close()`, `dispose()`, or a cleanup callback was invoked is not sufficient evidence that work stopped or resources were released. ## API and service boundaries -API handlers are transport adapters. They parse and validate request data, -establish the authenticated and authorized caller, pass explicit inputs to the -owning service or command-intake boundary, and map the result to the transport -response. Do not put business orchestration, ORM queries, Runtime node calls, -checkpoint mutation, or private lifecycle control into an API handler. +API handlers are transport adapters. They parse and validate request data, establish the authenticated and authorized caller, pass explicit inputs to the owning service or command-intake boundary, and map the result to the transport response. Do not put business orchestration, ORM queries, Runtime node calls, checkpoint mutation, or private lifecycle control into an API handler. -Design shared service contracts for all current consumers. Keep transport-, -UI-, channel-, and provider-specific behavior in the owning adapter or consumer. -Do not widen a public service for one internal caller; keep single-consumer -capabilities private until a real shared contract exists. +Design shared service contracts for all current consumers. Keep transport-, UI-, channel-, and provider-specific behavior in the owning adapter or consumer. Do not widen a public service for one internal caller; keep single-consumer capabilities private until a real shared contract exists. ## Public choices -Do not invent public defaults, modes, operation sets, API fields, event fields, -or persisted formats merely to make an interface appear flexible. Every public -choice must be supported by a current consumer, an owning product or -architecture contract, or established behavior already used by the system. +Do not invent public defaults, modes, operation sets, API fields, event fields, or persisted formats merely to make an interface appear flexible. Every public choice must be supported by a current consumer, an owning product or architecture contract, or established behavior already used by the system. -When that evidence does not exist, require the caller to provide an explicit -value or defer the choice instead of introducing a speculative default or -extension point. +When that evidence does not exist, require the caller to provide an explicit value or defer the choice instead of introducing a speculative default or extension point. ## Model-facing contracts -Write prompts, Tool schemas, Tool results, and model-visible diagnostics from -the model's task perspective. Include the information needed to choose and -complete the next action; do not expose UI state, transport details, database -structure, internal service names, or implementation vocabulary unless the -model must act on that concept. +Write prompts, Tool schemas, Tool results, and model-visible diagnostics from the model's task perspective. Include the information needed to choose and complete the next action; do not expose UI state, transport details, database structure, internal service names, or implementation vocabulary unless the model must act on that concept. -A failure on a model-visible path must return a bounded, actionable result that -identifies the failed subject, the relevant condition, and any safe next action. -Do not silently drop the failure or dump stack traces, raw provider responses, -internal records, or unbounded diagnostic output into model context. +A failure on a model-visible path must return a bounded, actionable result that identifies the failed subject, the relevant condition, and any safe next action. Do not silently drop the failure or dump stack traces, raw provider responses, internal records, or unbounded diagnostic output into model context. -Treat stable model-visible wording and schemas as behavior. Changes require an -update to the owning contract and verification through the assembled model -request or Tool execution path. +Treat stable model-visible wording and schemas as behavior. Changes require an update to the owning contract and verification through the assembled model request or Tool execution path. ## Enforcement -The operation that reads protected data, mutates authoritative state, or causes -an external side effect must obtain and enforce authorization, tenant scope, -limits, and policy decisions from the owning Backend permission model at that -execution boundary. Upstream layers may perform an equivalent preflight for -faster feedback, but Frontend visibility, prompt instructions, Tool-schema -omission, API wrappers, and ordinary call ordering are user-experience guidance, -not security enforcement. +The operation that reads protected data, mutates authoritative state, or causes an external side effect must obtain and enforce authorization, tenant scope, limits, and policy decisions from the owning Backend permission model at that execution boundary. Upstream layers may perform an equivalent preflight for faster feedback, but Frontend visibility, prompt instructions, Tool-schema omission, API wrappers, and ordinary call ordering are user-experience guidance, not security enforcement. -Tests for a denial rule must exercise the real executor or mutation boundary, -including relevant alternate callers that could bypass an upstream check. +Tests for a denial rule must exercise the real executor or mutation boundary, including relevant alternate callers that could bypass an upstream check. ## Independent outcomes -Report independent execution outcomes as separate facts. Acceptance, execution, -persistence, synchronization, delivery, timeout, cancellation, and cleanup may -coexist; do not collapse them into one success flag or infer one outcome from -another. +Report independent execution outcomes as separate facts. Acceptance, execution, persistence, synchronization, delivery, timeout, cancellation, and cleanup may coexist; do not collapse them into one success flag or infer one outcome from another. ## Public result contracts -A public Backend contract has one documented success, failure, cancellation, -and uncertain-outcome model. Adapters normalize provider-, transport-, worker-, -and implementation-specific result forms at the owning boundary before -returning them to consumers. +A public Backend contract has one documented success, failure, cancellation, and uncertain-outcome model. Adapters normalize provider-, transport-, worker-, and implementation-specific result forms at the owning boundary before returning them to consumers. -Consumers depend only on the normalized contract and must not guess whether the -same outcome arrives through an exception, status field, terminal event, empty -value, or transport closure. Preserve internal defects as internal failures -instead of misclassifying them as ordinary provider or business outcomes. +Consumers depend only on the normalized contract and must not guess whether the same outcome arrives through an exception, status field, terminal event, empty value, or transport closure. Preserve internal defects as internal failures instead of misclassifying them as ordinary provider or business outcomes. Test every supported source form through the real consumer-facing boundary. ## Failure containment and blocking decisions -A Tool, Provider, integration, observer, or optional-capability failure does not -block the parent Run or unrelated work by default. Contain the failure at the -owning capability boundary, record its exact outcome, and return a bounded, -actionable error through the public result contract so the model or owning -workflow can decide the next action. +A Tool, Provider, integration, observer, or optional-capability failure does not block the parent Run or unrelated work by default. Contain the failure at the owning capability boundary, record its exact outcome, and return a bounded, actionable error through the public result contract so the model or owning workflow can decide the next action. -Blocking a Turn, Run, downstream handler, or unrelated capability is an -explicit product and Runtime contract. Before introducing new blocking -semantics, identify why safe continuation is impossible, document the affected -contract and recovery behavior, and confirm the decision with the user. +Blocking a Turn, Run, downstream handler, or unrelated capability is an explicit product and Runtime contract. Before introducing new blocking semantics, identify why safe continuation is impossible, document the affected contract and recovery behavior, and confirm the decision with the user. -Security or authorization denial, durable-state corruption, protocol -invalidity, and uncertain irreversible side effects may fail closed. Do not use -these exceptions to turn ordinary Tool or Provider failures into global -failures. +Security or authorization denial, durable-state corruption, protocol invalidity, and uncertain irreversible side effects may fail closed. Do not use these exceptions to turn ordinary Tool or Provider failures into global failures. ## State publication -Publish events, notifications, cache updates, projections, and user-visible -state only after the authoritative operation reaches its documented commit -point. A prepared, accepted, queued, or attempted operation is not a committed -outcome. +Publish events, notifications, cache updates, projections, and user-visible state only after the authoritative operation reaches its documented commit point. A prepared, accepted, queued, or attempted operation is not a committed outcome. -Derived state must be rebuilt or updated from the authoritative committed fact, -not from an optimistic side path. When an external side effect has an uncertain -outcome, record and reconcile that uncertainty instead of publishing success or -blindly repeating the operation. +Derived state must be rebuilt or updated from the authoritative committed fact, not from an optimistic side path. When an external side effect has an uncertain outcome, record and reconcile that uncertainty instead of publishing success or blindly repeating the operation. ## Complete-operation bounds -Apply item, byte, token, time, and concurrency limits at the owner of the -complete returned, persisted, queued, or model-visible result. Include wrappers, -metadata, retries, pagination assembly, and encoded representations when -evaluating the bound; a limit on one intermediate step is not a complete -operation bound. +Apply item, byte, token, time, and concurrency limits at the owner of the complete returned, persisted, queued, or model-visible result. Include wrappers, metadata, retries, pagination assembly, and encoded representations when evaluating the bound; a limit on one intermediate step is not a complete operation bound. -Test limits below, at, and above the boundary, including one oversized item and -multi-byte text where byte limits apply. Reject or truncate only according to -the owning contract, and report truncation explicitly. +Test limits below, at, and above the boundary, including one oversized item and multi-byte text where byte limits apply. Reject or truncate only according to the owning contract, and report truncation explicitly. diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index c27466c40..3986711d1 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -1,24 +1,17 @@ # AGENTS.md — Clawith Frontend -These frontend-specific rules apply to `frontend/**` and supplement the -repository-wide [conventions](../AGENTS.md#2-conventions). +These frontend-specific rules apply to `frontend/**` and supplement the repository-wide [conventions](../AGENTS.md#2-conventions). -The Frontend is a React 19 and TypeScript web application built with Vite. It -provides the user-facing interfaces for configuring, operating, and observing -agents. It consumes Backend and Runtime contracts but does not own execution -lifecycle or security decisions. +The Frontend is a React 19 and TypeScript web application built with Vite. It provides the user-facing interfaces for configuring, operating, and observing agents. It consumes Backend and Runtime contracts but does not own execution lifecycle or security decisions. -Project scripts and dependencies are defined in `package.json`; -`package-lock.json` records the resolved dependency graph. Application source -lives in `src/`, Frontend tests live in `tests/`, static assets live in -`public/`, and `dist/` is generated build output. +Project scripts and dependencies are defined in `package.json`; `package-lock.json` records the resolved dependency graph. Application source lives in `src/`, Frontend tests live in `tests/`, static assets live in `public/`, and `dist/` is generated build output. ## Commands Run Frontend commands from `frontend/`: | Action | Command | -|---|---| +| --- | --- | | Install locked dependencies | `npm ci` | | Run the development server | `npm run dev` | | Run a focused test file | `node --test tests/<test_file>.test.mjs` | @@ -29,9 +22,7 @@ Run Frontend commands from `frontend/`: | Format supported files | `npm run format` | | Build the production bundle | `npm run build` | -Use focused tests during development. Run the complete Frontend suite and -production build when the affected contracts cross multiple Frontend areas or -change assembled user-visible behavior. +Use focused tests during development. Run the complete Frontend suite and production build when the affected contracts cross multiple Frontend areas or change assembled user-visible behavior. ## Application layout @@ -58,131 +49,68 @@ src/utils/ Shared pure helpers. src/assets/ Source-controlled assets imported by the application. ``` -Detailed feature structure belongs to the nearest path-specific instruction or -owning architecture document, not this file. +Detailed feature structure belongs to the nearest path-specific instruction or owning architecture document, not this file. ## State ownership -Each Frontend fact has one state owner. Do not dual-write the same committed -business fact into React Query, Zustand, component state, and browser storage. - -- Remote Backend data belongs to the React Query cache. Mutations update or - invalidate the owning query. -- Cross-route or remount-surviving client interaction state belongs to an - owning Zustand store. -- State used only by one mounted component or feature subtree remains local - React state. -- A user-editable draft may have local state because it is not yet the committed - server fact. Define how the draft initializes, saves, resets, and responds to - a server refresh. -- Durable browser preferences and credentials are accessed through their owning - store or utility. Do not scatter independent `localStorage` or - `sessionStorage` reads and writes across components. -- Backend, Runtime, WebSocket, SSE, and shared-event subscriptions belong to an - owning service or feature hook. Business components consume its values and - callbacks rather than opening a second subscription. The owner handles - reconnection, ordering, deduplication, cancellation, and cleanup. -- Realtime events update or invalidate the same owner used by ordinary reads; - they do not create a second realtime-only representation. - -Derived display values remain pure computations over their authoritative state; -do not persist or subscribe to another independently updated copy. +Each Frontend fact has one state owner. Do not dual-write the same committed business fact into React Query, Zustand, component state, and browser storage. + +- Remote Backend data belongs to the React Query cache. Mutations update or invalidate the owning query. +- Cross-route or remount-surviving client interaction state belongs to an owning Zustand store. +- State used only by one mounted component or feature subtree remains local React state. +- A user-editable draft may have local state because it is not yet the committed server fact. Define how the draft initializes, saves, resets, and responds to a server refresh. +- Durable browser preferences and credentials are accessed through their owning store or utility. Do not scatter independent `localStorage` or `sessionStorage` reads and writes across components. +- Backend, Runtime, WebSocket, SSE, and shared-event subscriptions belong to an owning service or feature hook. Business components consume its values and callbacks rather than opening a second subscription. The owner handles reconnection, ordering, deduplication, cancellation, and cleanup. +- Realtime events update or invalidate the same owner used by ordinary reads; they do not create a second realtime-only representation. + +Derived display values remain pure computations over their authoritative state; do not persist or subscribe to another independently updated copy. ## Component and data-access boundaries -Components render product state and coordinate user interaction. Backend access, -authentication headers, endpoint construction, transport errors, and response -parsing belong to `src/services/` or an owning feature hook; do not add raw -`fetch()` calls or direct credential reads to business components. +Components render product state and coordinate user interaction. Backend access, authentication headers, endpoint construction, transport errors, and response parsing belong to `src/services/` or an owning feature hook; do not add raw `fetch()` calls or direct credential reads to business components. -React Query hooks own remote reads, mutations, cache keys, invalidation, and -loading/error state. Service functions return typed application values or a -documented application error, not raw `Response` objects that force each -consumer to reinterpret the transport contract. +React Query hooks own remote reads, mutations, cache keys, invalidation, and loading/error state. Service functions return typed application values or a documented application error, not raw `Response` objects that force each consumer to reinterpret the transport contract. -Pass components the values and callbacks they need. Do not pass an entire -service, store, Runtime object, or transport client merely to avoid defining the -component contract. +Pass components the values and callbacks they need. Do not pass an entire service, store, Runtime object, or transport client merely to avoid defining the component contract. ## Feature and presentation boundaries -Route pages and feature-level containers own product-flow orchestration. -Reusable presentation components receive typed values, display state, and -callbacks through explicit props; they do not fetch data, interpret Backend or -Runtime lifecycle, mutate shared stores directly, or coordinate unrelated -features. +Route pages and feature-level containers own product-flow orchestration. Reusable presentation components receive typed values, display state, and callbacks through explicit props; they do not fetch data, interpret Backend or Runtime lifecycle, mutate shared stores directly, or coordinate unrelated features. -Keep feature-specific components, hooks, services, types, and utilities close -to their owning feature. Move code into a shared directory only after a current -second consumer proves the shared contract. Do not create generic components or -hooks for hypothetical reuse. +Keep feature-specific components, hooks, services, types, and utilities close to their owning feature. Move code into a shared directory only after a current second consumer proves the shared contract. Do not create generic components or hooks for hypothetical reuse. -When a page becomes large, split it by owned responsibility and data flow, not -by arbitrary line ranges or visual fragments that still require the parent to -pass its entire state. +When a page becomes large, split it by owned responsibility and data flow, not by arbitrary line ranges or visual fragments that still require the parent to pass its entire state. ## Testing -Prefer behavior tests that execute the owning service, reducer, state -transition, or utility and assert its public result. Use source-text contract -tests only for narrow static constraints that cannot yet be exercised through -the current harness; do not use regex matches as evidence that a component -renders correctly or that a user journey works. +Prefer behavior tests that execute the owning service, reducer, state transition, or utility and assert its public result. Use source-text contract tests only for narrow static constraints that cannot yet be exercised through the current harness; do not use regex matches as evidence that a component renders correctly or that a user journey works. Each test asserts the layer it owns: -- Service and state tests cover data transformation, ordering, deduplication, - cache updates, error normalization, and lifecycle transitions. -- Component or browser validation covers rendered content, interaction, focus, - scrolling, responsive layout, and navigation. -- `npm run build` proves TypeScript compilation and production bundling, not - user-visible behavior. +- Service and state tests cover data transformation, ordering, deduplication, cache updates, error normalization, and lifecycle transitions. +- Component or browser validation covers rendered content, interaction, focus, scrolling, responsive layout, and navigation. +- `npm run build` proves TypeScript compilation and production bundling, not user-visible behavior. -When a change affects browser-only behavior that the automated harness cannot -exercise, validate it in a real browser and report the automation gap. Do not -describe a source-contract match or successful build as browser acceptance. +When a change affects browser-only behavior that the automated harness cannot exercise, validate it in a real browser and report the automation gap. Do not describe a source-contract match or successful build as browser acceptance. -Run the focused owning test during development. Add `npm run lint`, -`npm run format:check`, and `npx tsc --noEmit` for changed Frontend code; add the -production build when the change affects application composition, routing, -assets, styles, build configuration, or assembled user-visible behavior. +Run the focused owning test during development. Add `npm run lint`, `npm run format:check`, and `npx tsc --noEmit` for changed Frontend code; add the production build when the change affects application composition, routing, assets, styles, build configuration, or assembled user-visible behavior. ## TypeScript contracts -Keep `strict` TypeScript and `noImplicitAny` enabled. New and changed component -props, hook results, service inputs and outputs, store state, events, and Backend -response models use explicit types. +Keep `strict` TypeScript and `noImplicitAny` enabled. New and changed component props, hook results, service inputs and outputs, store state, events, and Backend response models use explicit types. -Treat external JSON, browser messages, storage values, and third-party payloads -as `unknown` until the owning boundary validates or narrows them. Do not use -`any`, broad index signatures, unchecked casts, non-null assertions, or optional -fields merely to silence a mismatch. When an exception is unavoidable, keep it -at the narrowest boundary and explain why the precise type is unavailable. +Treat external JSON, browser messages, storage values, and third-party payloads as `unknown` until the owning boundary validates or narrows them. Do not use `any`, broad index signatures, unchecked casts, non-null assertions, or optional fields merely to silence a mismatch. When an exception is unavoidable, keep it at the narrowest boundary and explain why the precise type is unavailable. -Define a shared type at the layer that owns the contract. Components and feature -consumers import that type instead of recreating local variants of the same -Backend, Runtime, or state shape. +Define a shared type at the layer that owns the contract. Components and feature consumers import that type instead of recreating local variants of the same Backend, Runtime, or state shape. -Handle closed state and event unions exhaustively. Extensible external inputs -must define explicit unknown-value behavior rather than falling through an -accidental default. +Handle closed state and event unions exhaustively. Extensible external inputs must define explicit unknown-value behavior rather than falling through an accidental default. ## Runtime and mutation presentation -Render Backend and Runtime states according to their documented contracts. Do -not infer completion, success, permission, delivery, or recoverability from an -HTTP success, request acceptance, missing error, assistant text, local timer, or -optimistic UI state. +Render Backend and Runtime states according to their documented contracts. Do not infer completion, success, permission, delivery, or recoverability from an HTTP success, request acceptance, missing error, assistant text, local timer, or optimistic UI state. -Keep accepted, queued, running, waiting, completed, failed, cancelled, -synchronized, and delivered outcomes distinct when the Backend contract -distinguishes them. A mutation invalidates or updates the owning React Query -state only from its documented committed result. +Keep accepted, queued, running, waiting, completed, failed, cancelled, synchronized, and delivered outcomes distinct when the Backend contract distinguishes them. A mutation invalidates or updates the owning React Query state only from its documented committed result. -Use optimistic UI only for reversible presentation or interaction state with a -defined rollback. Do not optimistically publish irreversible external effects, -Runtime completion, permission changes, or durable business results. +Use optimistic UI only for reversible presentation or interaction state with a defined rollback. Do not optimistically publish irreversible external effects, Runtime completion, permission changes, or durable business results. -Preserve canonical Backend error identity, safe message, code, trace, Run, and -retryability fields when available. Do not classify errors by matching English -message fragments. +Preserve canonical Backend error identity, safe message, code, trace, Run, and retryability fields when available. Do not classify errors by matching English message fragments. From 6cb0e794bc078dce13c16299a5f41194ec2a591d Mon Sep 17 00:00:00 2001 From: Y1fe1Zh0u <zhouyifei210@gmail.com> Date: Wed, 26 Aug 2026 14:28:28 +0800 Subject: [PATCH 003/339] Make change evidence follow Clawith contracts Clawith needs a repository-owned quality loop that selects evidence by affected contract instead of reflexively running every suite or accepting narrow checks for incomplete cross-boundary changes. Add the testing policy, pre-push workflow, review and simplification workflows, prose controls, and durable Agent Notes while leaving existing Drone configuration untouched. Constraint: Existing repository-wide Pyright, ESLint, and Prettier baselines are not yet green. Rejected: Copy the DSH workflows verbatim | Clawith does not share its package graph, stack, snapshot, i18n, or publication contracts. Rejected: Add remote quality gates now | local baselines and mechanical validators must be stabilized first. Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep docs/testing.md as the sole owner of evidence semantics and expand remote gates only after their baselines are green. Tested: Five Skill Creator validations; 18 Markdown relative-link checks; Prettier checks; git diff --check; independent code review APPROVE; architecture review CLEAR. Not-tested: Repository-wide Pyright, ESLint, Prettier, and remote GitHub Actions gates remain follow-up work. --- .../2026-08-26-quality-workflow-skills.md | 31 ++++ .../2026-08-26-pre-push-evidence-selection.md | 35 +++++ .agents/skills/clawith-code-review/SKILL.md | 34 +++++ .../clawith-code-review/agents/openai.yaml | 4 + .../clawith-find-simplifications/SKILL.md | 37 +++++ .../agents/openai.yaml | 4 + .../skills/clawith-pre-push-checks/SKILL.md | 134 ++++++++++++++++++ .../agents/openai.yaml | 4 + .../skills/clawith-prose-standard/SKILL.md | 31 ++++ .../clawith-prose-standard/agents/openai.yaml | 4 + .../skills/clawith-trim-cot-leakage/SKILL.md | 31 ++++ .../agents/openai.yaml | 4 + .gitignore | 18 +++ AGENTS.md | 4 +- backend/AGENTS.md | 2 +- docs/testing.md | 71 ++++++++++ frontend/AGENTS.md | 2 +- 17 files changed, 446 insertions(+), 4 deletions(-) create mode 100644 .agents/notes/implemented/process/2026-08-26-quality-workflow-skills.md create mode 100644 .agents/notes/implemented/testing/2026-08-26-pre-push-evidence-selection.md create mode 100644 .agents/skills/clawith-code-review/SKILL.md create mode 100644 .agents/skills/clawith-code-review/agents/openai.yaml create mode 100644 .agents/skills/clawith-find-simplifications/SKILL.md create mode 100644 .agents/skills/clawith-find-simplifications/agents/openai.yaml create mode 100644 .agents/skills/clawith-pre-push-checks/SKILL.md create mode 100644 .agents/skills/clawith-pre-push-checks/agents/openai.yaml create mode 100644 .agents/skills/clawith-prose-standard/SKILL.md create mode 100644 .agents/skills/clawith-prose-standard/agents/openai.yaml create mode 100644 .agents/skills/clawith-trim-cot-leakage/SKILL.md create mode 100644 .agents/skills/clawith-trim-cot-leakage/agents/openai.yaml create mode 100644 docs/testing.md diff --git a/.agents/notes/implemented/process/2026-08-26-quality-workflow-skills.md b/.agents/notes/implemented/process/2026-08-26-quality-workflow-skills.md new file mode 100644 index 000000000..30ad29aa4 --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-26-quality-workflow-skills.md @@ -0,0 +1,31 @@ +# Agent Note: Repository quality workflow Skills + +Status: implemented + +## Problem + +Clawith's repository instructions define contract-chain completion, evidence boundaries, dead-code removal, prose quality, and review expectations, but standing rules alone do not tell an agent how to apply those decisions to a concrete scope. Copying DeepSeek Harness workflows verbatim would introduce Cordis, pnpm, snapshot, stack, and bilingual-document assumptions that Clawith does not use. + +## Decision + +Repository-owned workflows live under `.agents/skills/` and link to the authoritative root instructions, [testing policy](../../../../docs/testing.md), and [Agent Note rules](../../README.md) rather than duplicating those sources. + +The first workflow set contains `clawith-pre-push-checks` for committed outgoing contract chains and evidence selection, `clawith-code-review` for independent correctness and architecture review, `clawith-find-simplifications` for deletion/reuse/ownership repair, `clawith-prose-standard` for complete contract-focused prose, and `clawith-trim-cot-leakage` for removing session-relative reasoning while preserving facts. + +Each Skill has one narrow trigger and workflow. Pre-push may publish only when the enclosing request already authorizes Push. Review is read-only unless fixes are separately authorized. Simplification requires an explicit scope and evidence of current owners and consumers. Prose and CoT workflows never edit archived Agent Notes. + +## Alternatives considered + +**Copy the DSH Skills without adaptation.** Rejected because their package graph, test commands, GitHub Stack workflow, Snapshot Harness, i18n, and archive sealing are not Clawith contracts. + +**Create one repository-quality mega-Skill.** Rejected because review, pre-push, simplification, and prose have different triggers, permissions, evidence, and stopping conditions; loading all instructions for every task would waste context and blur authority. + +**Create Archive, Defensive Pattern, documentation-site, i18n, Snapshot, and generated-catalog workflows immediately.** Rejected because the repository does not yet have current consumers or mechanical infrastructure for those systems. Add them when concrete notes, incidents, publication requirements, or harnesses justify the ownership cost. + +## Consequences + +Agents can apply the repository's quality rules through small task-specific workflows without importing DSH-specific machinery. The selected Skill directories and Agent Note tree must be tracked despite the broader `.agents/` ignore rule. Testing and pre-push form the initial development loop; CI and mechanical Agent Note/Markdown gates remain follow-up enforcement work. + +## Verification + +Every Skill passes the Skill Creator validator, has matching UI metadata, uses repository-relative links, contains no template TODOs, and follows one-physical-line-per-paragraph Markdown formatting. Separate code and architecture perspectives evaluate the complete change before merge readiness; independent reviewers are required for the high-risk surfaces named by the review Skill and used when available elsewhere. diff --git a/.agents/notes/implemented/testing/2026-08-26-pre-push-evidence-selection.md b/.agents/notes/implemented/testing/2026-08-26-pre-push-evidence-selection.md new file mode 100644 index 000000000..5559317c3 --- /dev/null +++ b/.agents/notes/implemented/testing/2026-08-26-pre-push-evidence-selection.md @@ -0,0 +1,35 @@ +# Agent Note: Contract-chain pre-push evidence selection + +Status: implemented + +## Problem + +Clawith lacked a shared rule for deciding which checks an outgoing change required. Agents either ran narrow tests without tracing shared consumers or reflexively ran complete Backend, Frontend, browser, and integration suites. Neither behavior proved that the changed contract was complete, and repository-wide ESLint and Prettier baseline failures made indiscriminate full checks especially noisy. + +## Decision + +The [testing policy](../../../../docs/testing.md) defines what each verification surface proves. The [`clawith-pre-push-checks`](../../../skills/clawith-pre-push-checks/SKILL.md) workflow applies that policy to committed outgoing changes. + +The workflow resolves the real Base, groups the committed diff by behavioral intent, traces every affected contract from authoritative owner through producers, persistence, boundaries, consumers, tests, documentation, and Agent Note, and blocks a Push when the chain is incomplete. It selects the narrowest evidence that would fail for the intended regression and expands only across boundaries the change actually reaches. + +Local unrelated changes remain outside the outgoing verification scope. A local change that belongs to the same outgoing intent but is not committed makes the outgoing change incomplete and blocks the Push. + +The Skill may Push only when the enclosing request already grants that authority. Otherwise it reports `Ready` or `Blocked`. After an authorized Push, remote ref movement, CI, merge readiness, deployment, and live acceptance remain separate facts. + +## Alternatives considered + +**Run all Backend, Frontend, browser, migration, and integration checks before every Push.** Rejected because cost and environmental noise do not establish contract relevance, and broad green suites cannot compensate for a missing producer or consumer. + +**Map changed directories directly to fixed commands.** Rejected because a one-line shared-contract change may require both applications and durable representations, while many multi-file refactors remain local to one behavior. + +**Develop a change-scope script before the workflow.** Rejected for the first version because Git already exposes the required committed and local facts; repeated use should identify which discovery steps warrant mechanical extraction. + +**Let the Skill Push whenever it reports Ready.** Rejected because verification does not broaden the user's authorization to mutate a remote branch. + +## Consequences + +Pre-push verification now evaluates complete contract chains rather than file counts. Historical baseline failures remain visible but do not authorize new violations or unrelated cleanup. The Testing Policy remains the single owner of evidence semantics; the Skill applies it to the outgoing change. The workflow requires semantic Agent judgment for contract closure and Agent Note alignment; CI can enforce only the mechanical portions. + +## Verification + +The Skill and testing policy cross-link each other, use repository-relative paths, distinguish committed outgoing work from unrelated local state, and preserve Push authorization as an external precondition. Existing Drone configuration files remain unchanged, and GitHub Actions retains its current release orchestration. New change-scoped quality evidence runs locally through the pre-push workflow; GitHub Actions may later enforce selected checks remotely when their repository-wide baselines and validators are ready. Pyright, ESLint, Prettier, Markdown, and Agent Note gates remain follow-up work until those prerequisites exist. diff --git a/.agents/skills/clawith-code-review/SKILL.md b/.agents/skills/clawith-code-review/SKILL.md new file mode 100644 index 000000000..f141f41ca --- /dev/null +++ b/.agents/skills/clawith-code-review/SKILL.md @@ -0,0 +1,34 @@ +--- +name: clawith-code-review +description: Review Clawith changes for correctness, security, ownership, contract-chain completeness, test sufficiency, and maintainability. Use for code review, pull-request review, merge readiness, or after a non-trivial implementation is complete. +--- + +# Clawith Code Review + +Review read-only unless the user separately authorizes fixes. Inspect the complete change against its verified Base, not only the last commit or largest file. + +## Load the contract + +Read the root `AGENTS.md`, every path-specific `AGENTS.md` governing the changed files, [the testing policy](../../../docs/testing.md), and [the Agent Note rules](../../notes/README.md). Identify the product or architecture contract the change claims to implement. + +## Trace the change + +Group the diff by behavioral intent. For each intent, trace the authoritative owner, producers, mutations, persistence, API/Event/Tool/worker boundary, consumers, Frontend/model/external representation, errors, compatibility behavior, tests, documentation, and owning Agent Note. + +Reject a broader test suite as compensation for an incomplete chain. Flag duplicated facts, parallel lifecycle state machines, authorization enforced only in UI/Prompt/wrappers, state published before its commit point, and local Tool or Provider failures that block unrelated work without an explicit contract. + +## Review lanes + +Always review from both a code/security/quality perspective and an architecture/devil's-advocate perspective. Keep the findings separate before synthesis. Use independent reviewers when available, and require them for security-, Runtime-, permission-, persistence-, migration-, or cross-layer high-risk changes. + +The code lane checks correctness, security, tenant and permission enforcement, error contracts, data access, performance, dead code, tests, and maintainability. The architecture lane checks fact ownership, boundary placement, state machines, public contracts, long-term coupling, and the strongest counterargument to approval. + +## Evidence and severity + +Every finding cites a current file and line, the violated contract, concrete impact, and a bounded repair. Rate findings `CRITICAL`, `HIGH`, `MEDIUM`, or `LOW`; rate architecture `CLEAR`, `WATCH`, or `BLOCK`. + +Return `REQUEST CHANGES` for any CRITICAL/HIGH correctness or security finding, architecture `BLOCK`, an incomplete contract chain, a missing required Agent Note, or unavailable independent review on a high-risk change. Return `COMMENT` for architecture `WATCH`, non-blocking improvements, or unavailable independent review on a lower-risk change. Return `APPROVE` only when no blocker remains, verification evidence matches the claims, and the required review perspectives were completed. + +## Report + +Lead with the verdict. List blocking findings first, then non-blocking findings, verification reviewed, unverified surfaces, Agent Note alignment, and the final code-review/architecture synthesis. Do not praise, summarize the implementation, or invent issues to fill categories. diff --git a/.agents/skills/clawith-code-review/agents/openai.yaml b/.agents/skills/clawith-code-review/agents/openai.yaml new file mode 100644 index 000000000..b021fe899 --- /dev/null +++ b/.agents/skills/clawith-code-review/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Clawith Code Review" + short_description: "Review Clawith changes against repository contracts." + default_prompt: "Use $clawith-code-review to review the current outgoing change and return a merge verdict." diff --git a/.agents/skills/clawith-find-simplifications/SKILL.md b/.agents/skills/clawith-find-simplifications/SKILL.md new file mode 100644 index 000000000..06ccdf97d --- /dev/null +++ b/.agents/skills/clawith-find-simplifications/SKILL.md @@ -0,0 +1,37 @@ +--- +name: clawith-find-simplifications +description: Find and optionally apply evidence-backed Clawith simplifications through deletion, reuse, and ownership-boundary repair. Use for cleanup, refactoring, dead-code removal, duplicate-state removal, or requests to reduce complexity without changing approved behavior. +--- + +# Clawith Find Simplifications + +Require an explicit scope. Review read-only unless the user authorizes edits. Preserve verified behavior and unrelated working-tree changes. + +## Find candidates + +Look for code, configuration, tests, compatibility paths, abstractions, state machines, caches, wrappers, services, and documentation with no current contract or production consumer. Also look for duplicated facts, duplicated lifecycle control, per-item queries, repeated full materialization, scattered configuration resolution, raw transport behavior in components, and behavior placed outside its authoritative owner. + +Prefer this order: + +```text +Delete obsolete behavior +→ Reuse the existing owner or utility +→ Move misplaced behavior back to its owner and remove bypasses +→ Introduce a new abstraction only for an independently changing responsibility with a current consumer +``` + +## Prove removal is safe + +Search direct and dynamic imports, configuration, registries, routes, workers, background entrypoints, Tool and model schemas, persistence, migrations, API/Event/Wire contracts, Frontend consumers, external integrations, tests, and documentation. A text search with no callers is not sufficient proof when loading or consumption is dynamic. + +Classify each candidate as `delete`, `reuse`, `move-to-owner`, `keep`, or `defer`. State the current owner, consumer evidence, behavior preserved, and checks required. + +## Apply authorized changes + +Lock behavior with the narrowest regression test when existing coverage does not protect it. Make one intent-focused simplification at a time. Delete obsolete implementation, configuration, tests that only preserve deleted behavior, compatibility paths, and stale documentation together. + +A non-trivial simplification adds or updates its owning Agent Note. A complete feature removal preserves why the feature existed, why it no longer justified its surface, what capability is lost, and what conditions would justify reintroduction. + +## Verify and report + +Use [the testing policy](../../../docs/testing.md) and pre-push workflow to select evidence. Report inspected scope, deletions, reuse, boundary repairs, deliberate keeps, deferred candidates, exact checks, and remaining risk. Never measure success by lines deleted alone. diff --git a/.agents/skills/clawith-find-simplifications/agents/openai.yaml b/.agents/skills/clawith-find-simplifications/agents/openai.yaml new file mode 100644 index 000000000..b12e19ee0 --- /dev/null +++ b/.agents/skills/clawith-find-simplifications/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Clawith Simplifications" + short_description: "Find safe deletion, reuse, and boundary repairs." + default_prompt: "Use $clawith-find-simplifications to find safe simplifications in the requested scope." diff --git a/.agents/skills/clawith-pre-push-checks/SKILL.md b/.agents/skills/clawith-pre-push-checks/SKILL.md new file mode 100644 index 000000000..325371bae --- /dev/null +++ b/.agents/skills/clawith-pre-push-checks/SKILL.md @@ -0,0 +1,134 @@ +--- +name: clawith-pre-push-checks +description: Use before pushing or force-pushing a Clawith branch, before claiming that an outgoing change passed its required checks, and again after a rebase, merge, or conflict resolution changes the effective diff. +--- + +# Clawith Pre-push Checks + +Use this Skill to determine whether the complete committed outgoing change closes every affected contract chain and whether the selected evidence proves those contracts at their owning boundaries. + +The Skill does not grant Push authority. When the enclosing user request or workflow already authorizes a Push, a `Ready` result permits the Push procedure below. Otherwise, stop after reporting `Ready` or `Blocked`. + +## Inspect the outgoing change + +Confirm the repository, branch, worktree, and remote state before selecting checks: + +```sh +git rev-parse --show-toplevel +git status --short --branch +git remote -v +git branch -vv +``` + +Resolve the real target and base from the current pull request or branch configuration. When a pull request exists, query its current base instead of assuming `main` or `develop`. Fetch the verified remote ref before computing scope. + +```sh +gh pr view --json baseRefName,headRefName,headRepository +git fetch <remote> <base> +git merge-base HEAD <remote>/<base> +``` + +Inspect committed outgoing work and local worktree state separately: + +```sh +git log --oneline <merge-base>..HEAD +git diff --name-status <merge-base>...HEAD +git diff --cached --name-status +git diff --name-status +git ls-files --others --exclude-standard +``` + +The outgoing Push contains committed changes only. Local changes that are unrelated to the outgoing intent remain outside verification scope and must not be staged or modified. Return `Blocked` when a staged, unstaged, or untracked path belongs to the outgoing intent but has not been committed. + +If no pull request or upstream target exists, resolve the intended target from the enclosing task or repository state before continuing. Do not guess a base. + +## Trace affected contract chains + +Group the committed diff by behavioral intent, not only by directory. For each changed behavior or shared contract, trace: + +```text +Authoritative owner +→ Producers and mutation points +→ Persistence or durable representation +→ API, event, Tool, worker, process, or integration boundary +→ Backend, Frontend, model, or external consumers +→ Error, cancellation, compatibility, and unknown-value behavior +→ Owning tests, documentation, and Agent Note +``` + +Use repository search, imports, schemas, event names, API routes, model and Tool contracts, persistence models, and tests to find real producers and consumers. Do not infer a complete chain from filenames alone. + +A contract chain is closed only when every affected participant changes with the contract, is verified to remain compatible, is deliberately removed with its obsolete paths, or is explicitly outside the change under an owning documented contract. + +Return `Blocked` when a changed authoritative fact or shared contract has an unresolved producer, consumer, persisted representation, error path, test, or owning document. Running broader tests does not compensate for an incomplete implementation chain. + +## Check Agent Note alignment + +Use [the Agent Note rules](../../notes/README.md) to decide whether the outgoing change is non-trivial. Search active Notes before accepting a newly created Note: + +```sh +rg -n "<contract|symbol|feature|decision term>" \ + .agents/notes/proposed .agents/notes/implemented \ + --glob '*.md' \ + --glob '!AGENTS.md' +``` + +For every non-trivial change, require one owning Agent Note in the outgoing commits. Update an existing owner when the decision is unchanged; create a new cross-linked Note when the decision changes. + +Check the three records together: + +```text +Code +→ implements the decision + +Agent Note +→ owns the durable rationale, current contract, alternatives, and consequences + +Commit history +→ records the concrete intent, scope, and verification of this change +``` + +Return `Blocked` when a non-trivial change has no owning Note; a duplicate Note replaces an existing owner; code, Note, and commit intent describe different contracts; an implemented Note retains stale proposal wording or mechanisms; a reversal rewrites the old Note rather than superseding it; a rejected or archived Note is treated as current authority; or the Note omits a real alternative or invents one that was not considered. + +A `proposed` Note may accompany design or work that is not yet the current implementation. Code presented as complete or ready to merge requires the owning Note to describe that implemented behavior in the present tense. + +## Apply the testing policy + +Read and apply [the repository testing policy](../../../docs/testing.md). The policy is the sole owner of what each evidence surface proves, full-suite triggers, historical-baseline treatment, and failure rules; do not restate or replace those decisions in this Skill. + +For each affected contract chain, record the selected commands, the owning behavior each command proves, and why no broader boundary is required. Run the selected checks, read their complete results, and return `Blocked` when a required check fails or a required verification surface remains unavailable. + +Do not repeat a passing check solely because a Commit or Push follows. Rerun it only when the effective diff, environment, dependency graph, generated output, or owning contract has changed. + +## Push authorization and procedure + +Push only when the enclosing user request or workflow already authorizes publishing the branch. Otherwise stop after reporting `Ready` or `Blocked`. + +Before an ordinary Push: + +1. Require every selected check to pass. A required verification gap remains `Blocked`. +2. Require every change belonging to the outgoing intent to be committed. Preserve unrelated local modifications without staging or including them. +3. Fetch the current remote branch and confirm that the expected remote head has not moved. +4. Push the current `HEAD` to the intended remote branch. +5. Verify that the remote branch resolves to the same commit as local `HEAD`. + +For an authorized history rewrite, record the observed remote commit and use an exact `--force-with-lease`; never use raw `--force`. Abort when the remote moved. + +After Push, inspect the pull request checks and commit statuses. Report pending checks as pending. A successful `git push` proves only that the remote ref moved; it does not prove CI, merge readiness, deployment, or live acceptance. + +Do not create empty commits, rewrite history, retarget branches, or toggle pull request state merely to provoke CI without first identifying why the expected check did not run. + +## Report + +Return one final status: + +- `Ready` — the committed outgoing change closes every affected contract chain and the selected evidence passed, but no Push was authorized. +- `Blocked` — a contract-chain gap, Agent Note mismatch, relevant check failure, unresolved target, or required verification gap prevents Push. +- `Pushed` — an authorized Push completed and the remote branch matches local `HEAD`. +- `Pushed, CI pending` — the remote branch matches, but required remote checks are not terminal. +- `Pushed, CI failed` — the Push completed, but a required remote check failed. +- `Pushed, CI not observed` — the remote branch matches, but no authoritative remote check was available or configured for observation. + +Report the verified base and local `HEAD`; outgoing behavioral intents; affected contract chains and owners; owning Agent Notes; commands actually run and exact results; relevant checks not run and why; unrelated local changes only when they could affect handoff; remote branch and commit after Push; and CI, merge, deployment, and live-acceptance status as separate facts. + +Do not report broader success than the collected evidence supports. Keep source facts, local test evidence, remote CI, deployment, and live-system acceptance separate. diff --git a/.agents/skills/clawith-pre-push-checks/agents/openai.yaml b/.agents/skills/clawith-pre-push-checks/agents/openai.yaml new file mode 100644 index 000000000..802051bf7 --- /dev/null +++ b/.agents/skills/clawith-pre-push-checks/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Clawith Pre-push Checks" + short_description: "Validate outgoing contract chains before a Push." + default_prompt: "Use $clawith-pre-push-checks to validate the current outgoing change before Push." diff --git a/.agents/skills/clawith-prose-standard/SKILL.md b/.agents/skills/clawith-prose-standard/SKILL.md new file mode 100644 index 000000000..d35083bf9 --- /dev/null +++ b/.agents/skills/clawith-prose-standard/SKILL.md @@ -0,0 +1,31 @@ +--- +name: clawith-prose-standard +description: Write, review, restore, or trim Clawith Markdown, Agent Notes, AGENTS instructions, code comments, prompts, diagnostics, and user-visible strings while preserving complete contracts and removing repetition or decorative prose. +--- + +# Clawith Prose Standard + +Require an explicit scope. Review tasks report findings without editing; write or fix tasks apply clear changes. Never edit archived Agent Notes. + +## Preserve complete contracts + +Before editing, identify every actor, action, condition, ordering rule, modality, negative guarantee, exception, owner, side effect, failure mode, consequence, and quantitative bound. Remove words only when every relevant proposition survives and the result is clearer. + +Types define structure. Owning prose defines non-obvious behavior, failures, side effects, ownership, timing, cancellation, durability, limits, and safe use. Keep one authoritative explanation and link it elsewhere; do not copy architecture or another module's contract. + +## Write for the owning surface + +- **AGENTS instructions:** concise behavioral guardrails, explicit scope, and links to owning detail. +- **Agent Notes:** Problem, real decision or proposal, actual alternatives, consequences, verification, and named gaps; no invented rationale. +- **Public interfaces and comments:** non-obvious caller or maintainer contract, not code restatement or control-flow narration. +- **Tests:** only non-obvious fixture, platform, real-entry, or observation rationale. +- **Prompts, Tool schemas, diagnostics, and visible strings:** task-relevant concepts from the model or user's perspective; wording is behavior. +- **Reference documentation:** current facts and contracts, not change history or implementation diaries. + +Write directly and name the actual actor, file, API, operation, state, or behavior. Prefer exact terms over metaphors. One prose paragraph occupies one physical source line; use paragraph breaks for separate ideas and preserve lists, tables, and code blocks. + +## Workflow + +Read the owning code or contract before judging prose. Classify each passage as keep, add, trim, restore, restructure, move-to-owner, or defer. Update the owner before derivative text, then inspect analogous passages learned from the same rule. + +Verify relative links, Markdown formatting, changed code examples, model-visible behavior, and the relevant repository gates. Report scope, changes, deliberate keeps, deferred cases, and checks actually run. diff --git a/.agents/skills/clawith-prose-standard/agents/openai.yaml b/.agents/skills/clawith-prose-standard/agents/openai.yaml new file mode 100644 index 000000000..4c1c6c199 --- /dev/null +++ b/.agents/skills/clawith-prose-standard/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Clawith Prose Standard" + short_description: "Write concise, contract-focused Clawith prose." + default_prompt: "Use $clawith-prose-standard to review the requested prose for complete and concise contracts." diff --git a/.agents/skills/clawith-trim-cot-leakage/SKILL.md b/.agents/skills/clawith-trim-cot-leakage/SKILL.md new file mode 100644 index 000000000..49793de73 --- /dev/null +++ b/.agents/skills/clawith-trim-cot-leakage/SKILL.md @@ -0,0 +1,31 @@ +--- +name: clawith-trim-cot-leakage +description: Audit or remove reasoning-transcript leakage from Clawith comments, JSDoc, Markdown, Agent Notes, prompts, and visible prose. Use for AI-sounding change narration, dead draft references, review dialogue, control-flow walkthroughs, hedged planning residue, or session-relative wording. +--- + +# Clawith Trim Chain-of-Thought Leakage + +Read and apply [`clawith-prose-standard`](../clawith-prose-standard/SKILL.md) first. Require an explicit scope. Never edit archived Agent Notes, recorded fixtures, snapshots, or verbatim evidence. + +## The test + +For every suspect passage ask: could a reader at current `HEAD`, with no session transcript, review thread, or uncommitted draft, resolve every reference and verify every claim? If not, restate surviving facts from the repository's current perspective and delete the transcript around them. Delete passages with no durable fact. + +## Leakage classes + +- Dead design-session citations, temporary decision numbers, audit labels, draft sections, or phase names with no committed owner. +- PR, stack, reviewer, or authoring-session narration instead of current behavior. +- “Previously”, “now”, “no longer”, “this version”, or similar change narration in current-state prose. +- Reviewer-addressed defenses such as “this is correct because”; state the invariant or delete the comment when code already shows it. +- Control-flow narration, test walkthroughs, obvious branch proofs, and shortened reasoning summaries. +- Hedges such as “probably fine”, “for now”, or “should be enough” without a real bound or tracked follow-up. + +## Preserve sanctioned facts + +Keep resolvable issue references, Agent Note and incident evidence, required suppression reasons, empty-catch explanations, measured bounds, runtime old/new lifecycle states, and present-tense regression counterfactuals. Fix false explanations; do not delete required rationale merely because it resembles commentary. + +## Workflow + +Audit read-only first and judge every hit semantically. Enumerate each passage's propositions before deletion. Fix the owning source before generated or copied prose. Treat model-visible wording as behavior and require its owning verification rather than silently rewriting it. + +After editing, reread the complete surface, confirm every remaining reference resolves at `HEAD`, run Markdown/link/prose checks for the touched scope, and report preserved facts, removed leakage, and unresolved borderline cases. diff --git a/.agents/skills/clawith-trim-cot-leakage/agents/openai.yaml b/.agents/skills/clawith-trim-cot-leakage/agents/openai.yaml new file mode 100644 index 000000000..63756c8d1 --- /dev/null +++ b/.agents/skills/clawith-trim-cot-leakage/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Trim Clawith CoT Leakage" + short_description: "Remove reasoning transcripts from durable prose." + default_prompt: "Use $clawith-trim-cot-leakage to remove session-relative reasoning from the requested prose." diff --git a/.gitignore b/.gitignore index 4242a0a79..7e11a8eee 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,24 @@ ss-nodes.json _agent/ _agents/ +# Commit the repository-owned Agent Note system and selected Clawith workflows. +!.agents/ +.agents/* +!.agents/notes/ +!.agents/notes/** +!.agents/skills/ +.agents/skills/* +!.agents/skills/clawith-pre-push-checks/ +!.agents/skills/clawith-pre-push-checks/** +!.agents/skills/clawith-code-review/ +!.agents/skills/clawith-code-review/** +!.agents/skills/clawith-find-simplifications/ +!.agents/skills/clawith-find-simplifications/** +!.agents/skills/clawith-prose-standard/ +!.agents/skills/clawith-prose-standard/** +!.agents/skills/clawith-trim-cot-leakage/ +!.agents/skills/clawith-trim-cot-leakage/** + # Internal docs /RELEASE_NOTES.md /.coaligneignore diff --git a/AGENTS.md b/AGENTS.md index 2c964f3d9..363348d70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,9 +97,9 @@ After code changes, verification scope is determined by the affected contracts a Match evidence to the surface. -Select verification by changed contract. Start with focused checks and expand only when the change crosses a documented boundary. +Use [`docs/testing.md`](docs/testing.md) to select verification by changed contract. Start with focused checks and expand only when the change crosses a documented boundary. -Run relevant checks before pushes and report the exact commands and results. After rebasing, merging, resolving conflicts, or otherwise synchronizing a branch, immediately rerun the checks affected by the resulting diff. Do not merge while required checks are failing. +Run checks before pushes via [`clawith-pre-push-checks`](.agents/skills/clawith-pre-push-checks/SKILL.md) and report the exact commands and results. After rebasing, merging, resolving conflicts, or otherwise synchronizing a branch, immediately rerun the checks affected by the resulting diff. Do not merge while required checks are failing. ## Communication diff --git a/backend/AGENTS.md b/backend/AGENTS.md index eeae07514..a5d858a4f 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -20,7 +20,7 @@ Run Backend commands from `backend/`: | Run static type checks | `uv run --extra dev pyright app` | | Apply database migrations | `uv run alembic upgrade head` | -Use focused Pytest targets during development. Run the complete Backend suite only when the affected contracts cross multiple Backend areas or when required by the repository testing policy. +Use focused Pytest targets during development. Use the repository testing policy as the authority for when the complete Backend suite is required. Read [`alembic/AGENTS.md`](alembic/AGENTS.md) before creating or editing a database migration. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 000000000..47bde26ba --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,71 @@ +# Testing Policy + +This document defines what each Clawith verification surface proves and how to select evidence for a change. The [pre-push Skill](../.agents/skills/clawith-pre-push-checks/SKILL.md) applies this policy to the complete outgoing diff. + +## Evidence principles + +Match evidence to the changed contract and the claim being made. Start with the narrowest check that would fail for the intended regression, then expand only across boundaries the change actually reaches. + +Unit tests, static checks, builds, browser validation, CI, deployment, and live acceptance are different facts. None substitutes for another. + +Tests enforce the behavior they assert; they do not decide whether that behavior matches current product or architecture intent. An approved contract change updates code, owning documentation, Agent Note, and tests together. Never change an expectation merely to make a failure disappear. + +## Backend evidence + +- **Focused Pytest:** proves behavior owned by the selected test target and its real collaborators. +- **Ruff:** proves the checked Python scope satisfies configured lint rules; it is not a type or behavior test. +- **Pyright:** proves the checked Python scope satisfies static type contracts; it does not validate external payloads at runtime. +- **Architecture Guard:** proves only the repository rules implemented by `scripts/arch-guard.sh`; a new or changed rule requires positive and negative coverage. +- **Full Backend Pytest:** is appropriate for repository-wide Backend changes, CI diagnosis, or an explicit request; it is not the default response to a local change. + +Prefer real implementations below the expensive or nondeterministic boundary. Mock external providers, network, clocks, or nondeterministic inputs when necessary; keep the owning Service, Runtime, Tool, persistence, and executor path real when those behaviors are the subject. + +## Frontend evidence + +- **Focused Node test:** proves the imported service, reducer, state transition, utility, or narrow source contract named by the test. +- **TypeScript check:** proves static type compatibility across the Frontend project. +- **ESLint and Prettier:** prove lint and formatting compliance for the checked scope; they do not prove user-visible behavior. +- **Production build:** proves TypeScript compilation and Vite production bundling; it does not prove rendering or interaction. +- **Browser validation:** proves rendered content, interaction, focus, scrolling, responsive layout, and navigation in the exercised browser path. + +Prefer behavior tests that execute an owning function or state transition. Source-text regex tests are narrow static guards and must not be reported as component rendering or user-flow evidence. + +## Shared contracts and assembled paths + +A Backend/Frontend API, event, Runtime state, Tool result, or error-contract change requires evidence from every affected owner and consumer. Update and verify both sides rather than treating one side's passing tests as compatibility proof. + +Runtime, Tool, Worker, and lifecycle changes require the focused owning tests plus the real executor or consumer-facing path. Verify durable or external state instead of trusting a model response, callback invocation, or local projection. + +Model-visible prompts, Tool schemas, Tool results, and stable diagnostics are behavior. Verify them at the assembled model-request or Tool-execution path when the change can alter what the model sees. + +## Test the real entry path + +A product-visible behavior requires evidence through the entry path that users, workers, agents, or deployed services actually execute. A directly imported helper, manually constructed service, or mocked transport does not prove API routing, dependency composition, worker startup, Runtime wiring, container startup, or browser integration. + +Use the narrowest real assembled path that crosses the boundary changed by the contract. Keep lower-level tests as supporting evidence. + +## Test resource ownership and cleanup + +Tests that create tasks, workers, subscriptions, connections, temporary files, sandboxes, or external resources own and clean them on success, failure, cancellation, retry, and timeout. Assert that cleanup reaches the terminal or removed state; calling a cleanup method is not sufficient evidence. + +## Database migrations + +Read `backend/alembic/AGENTS.md` before changing a migration. Migration evidence may include single-head validation, migration-specific tests, downgrade/upgrade, fresh-database migration, previous-release upgrade, and deployment-shaped checks; select the surfaces required by the migration contract. + +A source migration file, hash, or successful local import does not prove that a real database can migrate or roll back safely. + +## External and live evidence + +Provider, Channel, OAuth, Tool, browser, and deployment claims that depend on a real external system require an authorized real-system check. Local mocks and CI remain supporting evidence. + +Keep local tests, remote CI, deployed-version proof, service health, and real business acceptance separate. A health response does not prove a workflow, and a successful external request does not prove product reconciliation or delivery. + +## Full-suite policy and historical baselines + +Run complete local suites only when explicitly requested, while diagnosing CI, when the change is irreducibly repository-wide, or when this policy names the full suite as the owning gate. Run the complete Backend suite when an affected contract crosses multiple Backend areas. Run the complete Frontend suite and production build when an affected contract crosses multiple Frontend areas or changes assembled user-visible behavior. + +A known repository-wide baseline failure does not excuse a new violation. Check the affected scope, preserve unrelated work, and report the baseline separately. After a gate reaches a green baseline, later failures are blocking until evidence proves they are environmental or unrelated to the outgoing commits. + +## Reporting + +Report exact commands, results, affected scope, and relevant verification not performed. Do not claim a broader success than the evidence supports. diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 3986711d1..bd691b5ac 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -22,7 +22,7 @@ Run Frontend commands from `frontend/`: | Format supported files | `npm run format` | | Build the production bundle | `npm run build` | -Use focused tests during development. Run the complete Frontend suite and production build when the affected contracts cross multiple Frontend areas or change assembled user-visible behavior. +Use focused tests during development. Use the repository testing policy as the authority for when the complete Frontend suite and production build are required. ## Application layout From b0e0b70713131494694085bdff5ece0c425f5429 Mon Sep 17 00:00:00 2001 From: Y1fe1Zh0u <zhouyifei210@gmail.com> Date: Wed, 26 Aug 2026 14:38:50 +0800 Subject: [PATCH 004/339] Establish a reviewable frontend formatting baseline Apply only mechanical Prettier output where the existing frontend contract suite remains green. Files whose source-text contracts fail after formatting stay outside this batch for separate treatment. Constraint: Preserve all existing runtime behavior and source-contract tests Rejected: Format all 155 files at once | 25 source files caused 31 static contract failures Confidence: high Scope-risk: moderate Reversibility: clean Directive: Resolve the remaining 25 Prettier files without weakening their owning source contracts Tested: changed-file Prettier check; npx tsc --noEmit; npm test (122 passed); npm run build; ESLint baseline unchanged at 1029 errors and 65 warnings Not-tested: Browser interaction and live deployment --- frontend/AGENTS.md | 22 +- frontend/index.html | 37 +- frontend/src/App.tsx | 620 ++- frontend/src/components/AgentBayLivePanel.tsx | 794 +-- frontend/src/components/AgentCredentials.tsx | 858 +-- frontend/src/components/AgentSidePanel.tsx | 667 ++- frontend/src/components/ChannelConfig.tsx | 3386 ++++++++---- frontend/src/components/ConfirmModal.tsx | 124 +- frontend/src/components/CosmicBackground.tsx | 114 +- frontend/src/components/CustomAgentModal.tsx | 1483 +++--- .../src/components/Dialog/DialogProvider.tsx | 430 +- frontend/src/components/ErrorBoundary.tsx | 130 +- .../src/components/ExperienceDetailDrawer.tsx | 461 +- frontend/src/components/FileBrowser.tsx | 1693 ++++-- frontend/src/components/LinearCopyButton.tsx | 142 +- .../src/components/PostHireSettingsModal.tsx | 773 +-- frontend/src/components/TakeControlPanel.tsx | 1151 ++-- frontend/src/components/TalentMarketModal.tsx | 1046 ++-- .../components/WorkspaceOperationPanel.tsx | 3369 +++++++----- frontend/src/components/atlas/AtlasFrame.tsx | 76 +- frontend/src/components/atlas/Button.tsx | 29 +- .../src/components/atlas/ClawithWordmark.tsx | 40 +- .../src/components/atlas/CompassMedallion.tsx | 153 +- .../src/components/atlas/CompassPlate.tsx | 216 +- .../components/atlas/ConstellationFigure.tsx | 108 +- .../src/components/atlas/CosmographyPlate.tsx | 169 +- .../src/components/atlas/HairlineInput.tsx | 51 +- frontend/src/components/atlas/LoneStar.tsx | 116 +- frontend/src/components/atlas/MonoLabel.tsx | 27 +- frontend/src/components/atlas/OrbitPlate.tsx | 232 +- frontend/src/components/atlas/OriginPlate.tsx | 240 +- frontend/src/components/atlas/OrreryPlate.tsx | 129 +- frontend/src/components/atlas/Plate.tsx | 22 +- frontend/src/components/atlas/StarField.tsx | 83 +- frontend/src/components/atlas/UniverseMap.tsx | 512 +- frontend/src/components/atlas/index.ts | 32 +- frontend/src/hooks/useDropZone.ts | 204 +- frontend/src/i18n/index.ts | 52 +- frontend/src/i18n/templateTranslations.ts | 521 +- frontend/src/main.tsx | 58 +- frontend/src/pages/AgentCreate.tsx | 2089 +++++--- frontend/src/pages/AgentDetail.tsx | 135 +- frontend/src/pages/CompanySetup.tsx | 556 +- frontend/src/pages/Dashboard.tsx | 1744 +++--- frontend/src/pages/ForgotPassword.tsx | 392 +- frontend/src/pages/InvitationCodes.tsx | 600 ++- frontend/src/pages/Layout.tsx | 3565 ++++++++----- frontend/src/pages/Login.tsx | 1930 ++++--- frontend/src/pages/Messages.tsx | 286 +- frontend/src/pages/OAuthCallback.tsx | 408 +- frontend/src/pages/OKR.tsx | 4732 ++++++++++------- frontend/src/pages/Onboarding.tsx | 584 +- frontend/src/pages/OpenClawSettings.tsx | 993 ++-- frontend/src/pages/PlatformDashboard.tsx | 1484 ++++-- frontend/src/pages/Plaza.tsx | 1158 ++-- frontend/src/pages/ResetPassword.tsx | 265 +- frontend/src/pages/SSOEntry.tsx | 341 +- frontend/src/pages/VerifyEmail.tsx | 461 +- .../src/pages/agent-detail/AgentDirectory.tsx | 1578 ++++-- .../src/pages/agent-detail/agentDetailTabs.ts | 40 +- .../agent-detail/components/ToolsManager.tsx | 3202 +++++++---- .../agent-detail/hooks/useAgentDetailRoute.ts | 113 +- .../pages/agent-detail/mcpAuthorization.ts | 73 +- .../pages/agent-detail/onboardingKickoff.ts | 35 +- .../pages/agent-detail/sessionVisibility.ts | 24 +- .../pages/agent-detail/tabs/ApprovalsTab.tsx | 353 +- .../src/pages/agent-detail/tabs/MindTab.tsx | 155 +- .../pages/agent-detail/tabs/SettingsTab.tsx | 1500 ++++-- .../src/pages/agent-detail/tabs/SkillsTab.tsx | 1011 ++-- .../src/pages/agent-detail/tabs/ToolsTab.tsx | 34 +- .../src/pages/agent-detail/utils/fetchAuth.ts | 32 +- .../components/CompanyInfoEditors.tsx | 1535 +++--- .../components/EnterpriseKBBrowser.tsx | 47 +- .../pages/enterprise-settings/tabs/OkrTab.tsx | 1155 ++-- .../pages/enterprise-settings/tabs/OrgTab.tsx | 2925 ++++++---- .../enterprise-settings/tabs/SkillsTab.tsx | 3187 +++++++---- .../src/pages/groups/CreateGroupModal.tsx | 324 +- frontend/src/pages/groups/GroupMemoryTab.tsx | 170 +- .../src/pages/groups/GroupSettingsModal.tsx | 420 +- frontend/src/pages/groups/GroupSidePanel.tsx | 375 +- .../src/pages/groups/GroupWorkspaceTab.tsx | 221 +- frontend/src/pages/groups/InlineEdit.tsx | 102 +- .../src/pages/groups/InviteMemberModal.tsx | 257 +- .../src/pages/groups/groupWorkspaceUpload.ts | 101 +- frontend/src/pages/groups/mentionBindings.ts | 318 +- .../src/pages/groups/versionedFileAdapter.ts | 93 +- frontend/src/services/apiError.ts | 328 +- .../src/services/directHistoryPagination.ts | 121 +- frontend/src/stores/index.ts | 67 +- frontend/src/styles/atlas.css | 239 +- frontend/src/types/group.ts | 176 +- frontend/src/types/index.ts | 154 +- frontend/src/types/qrcode.d.ts | 23 +- frontend/src/utils/agentNameValidation.ts | 16 +- frontend/src/utils/clipboard.ts | 46 +- frontend/src/utils/companyRegions.ts | 570 +- frontend/src/utils/formatFileSize.ts | 16 +- frontend/src/utils/openClawInstruction.ts | 14 +- frontend/src/utils/randomUUID.ts | 40 +- frontend/src/utils/theme.ts | 116 +- frontend/src/utils/workspaceFileFormats.ts | 36 +- frontend/tests/agentNameValidation.test.mjs | 26 +- frontend/tests/apiError.test.mjs | 222 +- .../tests/chatMultimodalContract.test.mjs | 12 +- .../directChatObservabilityContract.test.mjs | 148 +- .../tests/directChatScrollContract.test.mjs | 62 +- .../tests/directHistoryPagination.test.mjs | 72 +- frontend/tests/dockerBaseContract.test.mjs | 47 +- .../tests/enterpriseToolVisibility.test.mjs | 12 +- .../executeCodeWorkspacePathContract.test.mjs | 12 +- .../tests/experienceCitationContract.test.mjs | 28 +- .../tests/experienceDraftContract.test.mjs | 40 +- .../tests/groupAnnouncementEditor.test.mjs | 44 +- frontend/tests/groupApiContract.test.mjs | 31 +- .../tests/groupInteractionContract.test.mjs | 106 +- frontend/tests/groupRealtimeContract.test.mjs | 14 +- frontend/tests/groupUnreadContract.test.mjs | 59 +- ...upWorkspaceReconciliationContract.test.mjs | 14 +- .../tests/htmlPreviewSandboxContract.test.mjs | 14 +- .../tests/markdownRendererSecurity.test.mjs | 22 +- frontend/tests/mcpAuthorization.test.mjs | 112 +- frontend/tests/mentionBindings.test.mjs | 141 +- frontend/tests/onboardingKickoff.test.mjs | 31 +- frontend/tests/randomUUID.test.mjs | 20 +- .../runtimeModelSettingsContract.test.mjs | 62 +- frontend/tests/sessionRuntimeState.test.mjs | 571 +- frontend/tests/sessionVisibility.test.mjs | 72 +- frontend/tests/versionedFileAdapter.test.mjs | 129 +- frontend/tsconfig.json | 56 +- frontend/vite.config.ts | 94 +- 130 files changed, 40905 insertions(+), 24803 deletions(-) diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index bd691b5ac..44a4997b5 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -10,17 +10,17 @@ Project scripts and dependencies are defined in `package.json`; `package-lock.js Run Frontend commands from `frontend/`: -| Action | Command | -| --- | --- | -| Install locked dependencies | `npm ci` | -| Run the development server | `npm run dev` | -| Run a focused test file | `node --test tests/<test_file>.test.mjs` | -| Run the complete Frontend test suite | `npm test` | -| Run static type checks | `npx tsc --noEmit` | -| Run lint checks | `npm run lint` | -| Check formatting | `npm run format:check` | -| Format supported files | `npm run format` | -| Build the production bundle | `npm run build` | +| Action | Command | +| ------------------------------------ | ---------------------------------------- | +| Install locked dependencies | `npm ci` | +| Run the development server | `npm run dev` | +| Run a focused test file | `node --test tests/<test_file>.test.mjs` | +| Run the complete Frontend test suite | `npm test` | +| Run static type checks | `npx tsc --noEmit` | +| Run lint checks | `npm run lint` | +| Check formatting | `npm run format:check` | +| Format supported files | `npm run format` | +| Build the production bundle | `npm run build` | Use focused tests during development. Use the repository testing policy as the authority for when the complete Frontend suite and production build are required. diff --git a/frontend/index.html b/frontend/index.html index d163e9b4b..16c4cd73e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,20 +1,21 @@ -<!DOCTYPE html> +<!doctype html> <html lang="zh-CN"> + <head> + <meta charset="UTF-8" /> + <link rel="icon" type="image/png" href="/logo.png" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <meta name="description" content="Clawith — 企业数字员工平台" /> + <title>Clawith + + + + - - - - - - Clawith - - - - - - -
- - - - \ No newline at end of file + +
+ + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 80db0abbf..f492fcdf6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,306 +1,392 @@ -import { Routes, Route, Navigate } from 'react-router-dom'; -import { useAuthStore } from './stores'; -import { Suspense, lazy, useEffect, useLayoutEffect, useState, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; -import { authApi } from './services/api'; +import { Routes, Route, Navigate } from "react-router-dom"; +import { useAuthStore } from "./stores"; +import { + Suspense, + lazy, + useEffect, + useLayoutEffect, + useState, + useRef, +} from "react"; +import { useTranslation } from "react-i18next"; +import { authApi } from "./services/api"; -const Login = lazy(() => import('./pages/Login')); -const ForgotPassword = lazy(() => import('./pages/ForgotPassword')); -const ResetPassword = lazy(() => import('./pages/ResetPassword')); -const VerifyEmail = lazy(() => import('./pages/VerifyEmail')); -const CompanySetup = lazy(() => import('./pages/CompanySetup')); -const Onboarding = lazy(() => import('./pages/Onboarding')); -const Layout = lazy(() => import('./pages/Layout')); -const Dashboard = lazy(() => import('./pages/Dashboard')); -const Plaza = lazy(() => import('./pages/Plaza')); -const AgentDetail = lazy(() => import('./pages/AgentDetail')); -const AgentCreate = lazy(() => import('./pages/AgentCreate')); -const Messages = lazy(() => import('./pages/Messages')); -const EnterpriseSettings = lazy(() => import('./pages/EnterpriseSettings')); -const InvitationCodes = lazy(() => import('./pages/InvitationCodes')); -const AdminCompanies = lazy(() => import('./pages/AdminCompanies')); -const OAuthCallback = lazy(() => import('./pages/OAuthCallback')); -const SSOEntry = lazy(() => import('./pages/SSOEntry')); -const OKR = lazy(() => import('./pages/OKR')); -const GroupsPage = lazy(() => import('./pages/groups/GroupsPage')); +const Login = lazy(() => import("./pages/Login")); +const ForgotPassword = lazy(() => import("./pages/ForgotPassword")); +const ResetPassword = lazy(() => import("./pages/ResetPassword")); +const VerifyEmail = lazy(() => import("./pages/VerifyEmail")); +const CompanySetup = lazy(() => import("./pages/CompanySetup")); +const Onboarding = lazy(() => import("./pages/Onboarding")); +const Layout = lazy(() => import("./pages/Layout")); +const Dashboard = lazy(() => import("./pages/Dashboard")); +const Plaza = lazy(() => import("./pages/Plaza")); +const AgentDetail = lazy(() => import("./pages/AgentDetail")); +const AgentCreate = lazy(() => import("./pages/AgentCreate")); +const Messages = lazy(() => import("./pages/Messages")); +const EnterpriseSettings = lazy(() => import("./pages/EnterpriseSettings")); +const InvitationCodes = lazy(() => import("./pages/InvitationCodes")); +const AdminCompanies = lazy(() => import("./pages/AdminCompanies")); +const OAuthCallback = lazy(() => import("./pages/OAuthCallback")); +const SSOEntry = lazy(() => import("./pages/SSOEntry")); +const OKR = lazy(() => import("./pages/OKR")); +const GroupsPage = lazy(() => import("./pages/groups/GroupsPage")); function ProtectedRoute({ children }: { children: React.ReactNode }) { - const token = useAuthStore((s) => s.token); - const user = useAuthStore((s) => s.user); - if (!token) return ; - // Force company setup for users without a tenant - if (user && !user.tenant_id) return ; - - // Force email verification if not active/verified - if (user && !user.is_active) return ; - - return <>{children}; + const token = useAuthStore((s) => s.token); + const user = useAuthStore((s) => s.user); + if (!token) return ; + // Force company setup for users without a tenant + if (user && !user.tenant_id) return ; + + // Force email verification if not active/verified + if (user && !user.is_active) + return ( + + ); + + return <>{children}; } function CompanyAdminRoute({ children }: { children: React.ReactNode }) { - const user = useAuthStore((s) => s.user); - const canAccessCompanySettings = user?.role === 'platform_admin' || user?.role === 'org_admin' || !!(user as any)?.is_platform_admin; - if (!canAccessCompanySettings) return ; - return <>{children}; + const user = useAuthStore((s) => s.user); + const canAccessCompanySettings = + user?.role === "platform_admin" || + user?.role === "org_admin" || + !!(user as any)?.is_platform_admin; + if (!canAccessCompanySettings) return ; + return <>{children}; } /* ─── Notification Bar ─── */ -type NotificationBarConfig = { enabled: boolean; text: string; updated_at?: string | null }; +type NotificationBarConfig = { + enabled: boolean; + text: string; + updated_at?: string | null; +}; type NotificationBarUpdateEvent = CustomEvent; -const notificationBarClass = 'has-notification-bar'; -const notificationBarRevisionKey = (config: Pick) => - btoa(encodeURIComponent(`${config.text}::${config.updated_at || ''}`)); -const notificationBarSessionDismissKey = (config: Pick) => - `notification_bar_dismissed_session_${notificationBarRevisionKey(config)}`; -const notificationBarPersistentDismissKey = (config: Pick) => - `notification_bar_dismissed_persistent_${notificationBarRevisionKey(config)}`; +const notificationBarClass = "has-notification-bar"; +const notificationBarRevisionKey = ( + config: Pick, +) => btoa(encodeURIComponent(`${config.text}::${config.updated_at || ""}`)); +const notificationBarSessionDismissKey = ( + config: Pick, +) => `notification_bar_dismissed_session_${notificationBarRevisionKey(config)}`; +const notificationBarPersistentDismissKey = ( + config: Pick, +) => + `notification_bar_dismissed_persistent_${notificationBarRevisionKey(config)}`; function NotificationBar() { - const { i18n } = useTranslation(); - const isChinese = i18n.language?.startsWith('zh'); - const [config, setConfig] = useState(null); - const [dismissed, setDismissed] = useState(false); - const [showDismissMenu, setShowDismissMenu] = useState(false); - - const textRef = useRef(null); - const containerRef = useRef(null); - const dismissMenuRef = useRef(null); - const [isMarquee, setIsMarquee] = useState(false); + const { i18n } = useTranslation(); + const isChinese = i18n.language?.startsWith("zh"); + const [config, setConfig] = useState(null); + const [dismissed, setDismissed] = useState(false); + const [showDismissMenu, setShowDismissMenu] = useState(false); - useEffect(() => { - fetch('/api/enterprise/system-settings/notification_bar/public') - .then(r => r.ok ? r.json() : null) - .then(d => { if (d) setConfig(d); }) - .catch(() => { }); - }, []); + const textRef = useRef(null); + const containerRef = useRef(null); + const dismissMenuRef = useRef(null); + const [isMarquee, setIsMarquee] = useState(false); - useEffect(() => { - const handleUpdate = (event: Event) => { - const next = (event as NotificationBarUpdateEvent).detail; - if (!next) return; - setConfig(next); - setShowDismissMenu(false); - if (next.text) { - const persistentKey = notificationBarPersistentDismissKey(next); - const sessionKey = notificationBarSessionDismissKey(next); - setDismissed(!!localStorage.getItem(persistentKey) || !!sessionStorage.getItem(sessionKey)); - } else { - setDismissed(false); - } - if (!next.enabled || !next.text) { - document.body.classList.remove(notificationBarClass); - } - }; - - window.addEventListener('notification-bar-updated', handleUpdate); - return () => window.removeEventListener('notification-bar-updated', handleUpdate); - }, []); - - // Check sessionStorage for dismissal (keyed by text so new messages re-show) - useEffect(() => { - if (config?.text) { - const persistentKey = notificationBarPersistentDismissKey(config); - const sessionKey = notificationBarSessionDismissKey(config); - setDismissed(!!localStorage.getItem(persistentKey) || !!sessionStorage.getItem(sessionKey)); - } - }, [config?.text, config?.updated_at]); + useEffect(() => { + fetch("/api/enterprise/system-settings/notification_bar/public") + .then((r) => (r.ok ? r.json() : null)) + .then((d) => { + if (d) setConfig(d); + }) + .catch(() => {}); + }, []); - useEffect(() => { - if (!showDismissMenu) return; - const handleClickOutside = (event: MouseEvent) => { - const target = event.target as Node; - if (dismissMenuRef.current?.contains(target)) return; - setShowDismissMenu(false); - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, [showDismissMenu]); + useEffect(() => { + const handleUpdate = (event: Event) => { + const next = (event as NotificationBarUpdateEvent).detail; + if (!next) return; + setConfig(next); + setShowDismissMenu(false); + if (next.text) { + const persistentKey = notificationBarPersistentDismissKey(next); + const sessionKey = notificationBarSessionDismissKey(next); + setDismissed( + !!localStorage.getItem(persistentKey) || + !!sessionStorage.getItem(sessionKey), + ); + } else { + setDismissed(false); + } + if (!next.enabled || !next.text) { + document.body.classList.remove(notificationBarClass); + } + }; - // Manage body class: add when visible, remove when hidden or dismissed - const isVisible = !!config?.enabled && !!config?.text && !dismissed; - useLayoutEffect(() => { - document.documentElement.style.setProperty('--notification-bar-height', isVisible ? '32px' : '0px'); - if (isVisible) { - document.body.classList.add(notificationBarClass); - } else { - document.body.classList.remove(notificationBarClass); - } - return () => { - document.body.classList.remove(notificationBarClass); - document.documentElement.style.setProperty('--notification-bar-height', '0px'); - }; - }, [isVisible]); + window.addEventListener("notification-bar-updated", handleUpdate); + return () => + window.removeEventListener("notification-bar-updated", handleUpdate); + }, []); - // Dynamic marquee if text is too wide - useEffect(() => { - if (!isVisible) return; - const checkWidth = () => { - if (textRef.current && containerRef.current) { - // Determine if text is wider than its container - setIsMarquee(textRef.current.scrollWidth > containerRef.current.clientWidth); - } - }; - // Small delay to ensure DOM is fully rendered - const timer = setTimeout(checkWidth, 100); - window.addEventListener('resize', checkWidth); - return () => { - clearTimeout(timer); - window.removeEventListener('resize', checkWidth); - }; - }, [isVisible, config?.text]); + // Check sessionStorage for dismissal (keyed by text so new messages re-show) + useEffect(() => { + if (config?.text) { + const persistentKey = notificationBarPersistentDismissKey(config); + const sessionKey = notificationBarSessionDismissKey(config); + setDismissed( + !!localStorage.getItem(persistentKey) || + !!sessionStorage.getItem(sessionKey), + ); + } + }, [config?.text, config?.updated_at]); - if (!isVisible) return null; + useEffect(() => { + if (!showDismissMenu) return; + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + if (dismissMenuRef.current?.contains(target)) return; + setShowDismissMenu(false); + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [showDismissMenu]); - const dismissForSession = () => { - if (!config) return; - const key = notificationBarSessionDismissKey(config); - sessionStorage.setItem(key, '1'); - document.body.classList.remove(notificationBarClass); - setDismissed(true); - setShowDismissMenu(false); + // Manage body class: add when visible, remove when hidden or dismissed + const isVisible = !!config?.enabled && !!config?.text && !dismissed; + useLayoutEffect(() => { + document.documentElement.style.setProperty( + "--notification-bar-height", + isVisible ? "32px" : "0px", + ); + if (isVisible) { + document.body.classList.add(notificationBarClass); + } else { + document.body.classList.remove(notificationBarClass); + } + return () => { + document.body.classList.remove(notificationBarClass); + document.documentElement.style.setProperty( + "--notification-bar-height", + "0px", + ); }; + }, [isVisible]); - const dismissPersistently = () => { - if (!config) return; - const key = notificationBarPersistentDismissKey(config); - localStorage.setItem(key, '1'); - document.body.classList.remove(notificationBarClass); - setDismissed(true); - setShowDismissMenu(false); + // Dynamic marquee if text is too wide + useEffect(() => { + if (!isVisible) return; + const checkWidth = () => { + if (textRef.current && containerRef.current) { + // Determine if text is wider than its container + setIsMarquee( + textRef.current.scrollWidth > containerRef.current.clientWidth, + ); + } }; + // Small delay to ensure DOM is fully rendered + const timer = setTimeout(checkWidth, 100); + window.addEventListener("resize", checkWidth); + return () => { + clearTimeout(timer); + window.removeEventListener("resize", checkWidth); + }; + }, [isVisible, config?.text]); - // Calculate dynamic duration: longer text = longer animation so speed is consistent - const duration = config ? Math.max(20, config.text.length * 0.2) + 's' : '20s'; + if (!isVisible) return null; - return ( -
-
- - {config!.text} - -
-
- - {showDismissMenu && ( -
- - -
- )} -
-
- ); -} + const dismissForSession = () => { + if (!config) return; + const key = notificationBarSessionDismissKey(config); + sessionStorage.setItem(key, "1"); + document.body.classList.remove(notificationBarClass); + setDismissed(true); + setShowDismissMenu(false); + }; -export default function App() { - const { token, setAuth, user } = useAuthStore(); - const [loading, setLoading] = useState(true); + const dismissPersistently = () => { + if (!config) return; + const key = notificationBarPersistentDismissKey(config); + localStorage.setItem(key, "1"); + document.body.classList.remove(notificationBarClass); + setDismissed(true); + setShowDismissMenu(false); + }; - useEffect(() => { - // Initialize theme on app mount (ensures login page gets correct theme) - const savedTheme = localStorage.getItem('theme') || 'light'; - document.documentElement.setAttribute('data-theme', savedTheme); + // Calculate dynamic duration: longer text = longer animation so speed is consistent + const duration = config + ? Math.max(20, config.text.length * 0.2) + "s" + : "20s"; - // Cross-domain tenant switch: the backend appends ?token= to the redirect URL - // so the new domain receives a fresh scoped token. Consume it here (before any other - // auth logic) so it always takes precedence over a stale token in localStorage. - // - // IMPORTANT: Only apply this on paths that do NOT use ?token= for their own purposes. - // /reset-password and /verify-email both receive a one-time token for their own flow — - // consuming it here as a session JWT would call /auth/me, fail, log out the user, - // and redirect them to /login instead of showing the correct page. - const urlParams = new URLSearchParams(window.location.search); - const urlToken = urlParams.get('token'); - const currentPath = window.location.pathname; - const pathsWithOwnToken = ['/reset-password', '/verify-email']; - let effectiveToken = token; + return ( +
+
+ + {config!.text} + +
+
+ + {showDismissMenu && ( +
+ + +
+ )} +
+
+ ); +} - if (urlToken && !pathsWithOwnToken.includes(currentPath)) { - // Persist the new token and update the zustand store's in-memory value - localStorage.setItem('token', urlToken); - useAuthStore.setState({ token: urlToken, user: null }); - effectiveToken = urlToken; +export default function App() { + const { token, setAuth, user } = useAuthStore(); + const [loading, setLoading] = useState(true); - // Remove token from URL to prevent it from leaking into browser history - // and to avoid re-applying it on a manual page refresh. - urlParams.delete('token'); - const cleanSearch = urlParams.toString(); - const cleanUrl = window.location.pathname - + (cleanSearch ? `?${cleanSearch}` : '') - + window.location.hash; - window.history.replaceState({}, '', cleanUrl); - } + useEffect(() => { + // Initialize theme on app mount (ensures login page gets correct theme) + const savedTheme = localStorage.getItem("theme") || "light"; + document.documentElement.setAttribute("data-theme", savedTheme); + // Cross-domain tenant switch: the backend appends ?token= to the redirect URL + // so the new domain receives a fresh scoped token. Consume it here (before any other + // auth logic) so it always takes precedence over a stale token in localStorage. + // + // IMPORTANT: Only apply this on paths that do NOT use ?token= for their own purposes. + // /reset-password and /verify-email both receive a one-time token for their own flow — + // consuming it here as a session JWT would call /auth/me, fail, log out the user, + // and redirect them to /login instead of showing the correct page. + const urlParams = new URLSearchParams(window.location.search); + const urlToken = urlParams.get("token"); + const currentPath = window.location.pathname; + const pathsWithOwnToken = ["/reset-password", "/verify-email"]; + let effectiveToken = token; - if (effectiveToken && !user) { - authApi.me() - .then((u) => setAuth(u, effectiveToken!)) - .catch(() => useAuthStore.getState().logout()) - .finally(() => setLoading(false)); - } else { - setLoading(false); - } - }, []); + if (urlToken && !pathsWithOwnToken.includes(currentPath)) { + // Persist the new token and update the zustand store's in-memory value + localStorage.setItem("token", urlToken); + useAuthStore.setState({ token: urlToken, user: null }); + effectiveToken = urlToken; + // Remove token from URL to prevent it from leaking into browser history + // and to avoid re-applying it on a manual page refresh. + urlParams.delete("token"); + const cleanSearch = urlParams.toString(); + const cleanUrl = + window.location.pathname + + (cleanSearch ? `?${cleanSearch}` : "") + + window.location.hash; + window.history.replaceState({}, "", cleanUrl); + } - if (loading) { - return ( -
- 加载中... -
- ); + if (effectiveToken && !user) { + authApi + .me() + .then((u) => setAuth(u, effectiveToken!)) + .catch(() => useAuthStore.getState().logout()) + .finally(() => setLoading(false)); + } else { + setLoading(false); } + }, []); + if (loading) { return ( - <> - - 加载中...}> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - +
+ 加载中... +
); + } + + return ( + <> + + + 加载中... + + } + > + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } + /> + + + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } + /> + } /> + } /> + } + /> + + + + + ); } diff --git a/frontend/src/components/AgentBayLivePanel.tsx b/frontend/src/components/AgentBayLivePanel.tsx index 66ad7d60a..ad8e2f256 100644 --- a/frontend/src/components/AgentBayLivePanel.tsx +++ b/frontend/src/components/AgentBayLivePanel.tsx @@ -1,88 +1,140 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import TakeControlPanel from './TakeControlPanel'; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import TakeControlPanel from "./TakeControlPanel"; /* ── Types ── */ export interface LivePreviewState { - desktop?: { screenshotUrl: string }; - browser?: { screenshotUrl: string }; - code?: { output: string }; - transfer?: { - fromType?: string; - fromPath?: string; - toType?: string; - toPath?: string; - status?: 'running' | 'done' | 'error'; - result?: string; - updatedAt?: number; - }; + desktop?: { screenshotUrl: string }; + browser?: { screenshotUrl: string }; + code?: { output: string }; + transfer?: { + fromType?: string; + fromPath?: string; + toType?: string; + toPath?: string; + status?: "running" | "done" | "error"; + result?: string; + updatedAt?: number; + }; } export const MAX_LIVE_CODE_OUTPUT_CHARS = 120_000; -const LIVE_CODE_TRUNCATED_NOTICE = '\n\n[... older live output truncated ...]\n'; +const LIVE_CODE_TRUNCATED_NOTICE = + "\n\n[... older live output truncated ...]\n"; export function appendLiveCodeOutput(existing: string, chunk: string): string { - const next = existing + chunk; - if (next.length <= MAX_LIVE_CODE_OUTPUT_CHARS) return next; - - const keepChars = Math.max(0, MAX_LIVE_CODE_OUTPUT_CHARS - LIVE_CODE_TRUNCATED_NOTICE.length); - return LIVE_CODE_TRUNCATED_NOTICE + next.slice(-keepChars); + const next = existing + chunk; + if (next.length <= MAX_LIVE_CODE_OUTPUT_CHARS) return next; + + const keepChars = Math.max( + 0, + MAX_LIVE_CODE_OUTPUT_CHARS - LIVE_CODE_TRUNCATED_NOTICE.length, + ); + return LIVE_CODE_TRUNCATED_NOTICE + next.slice(-keepChars); } interface Props { - liveState: LivePreviewState; - visible: boolean; - onToggle: () => void; - agentId?: string; // needed for Take Control - sessionId?: string; // needed for Take Control - /** Called by TC panel on close to push the latest screenshot into liveState */ - onLiveUpdate?: (env: 'browser' | 'desktop', screenshotDataUri: string) => void; - /** Called when user clicks Clear in the code output panel */ - onClearCode?: () => void; - /** Called when user clicks Close to dismiss the code panel */ - onCloseCode?: () => void; + liveState: LivePreviewState; + visible: boolean; + onToggle: () => void; + agentId?: string; // needed for Take Control + sessionId?: string; // needed for Take Control + /** Called by TC panel on close to push the latest screenshot into liveState */ + onLiveUpdate?: ( + env: "browser" | "desktop", + screenshotDataUri: string, + ) => void; + /** Called when user clicks Clear in the code output panel */ + onClearCode?: () => void; + /** Called when user clicks Close to dismiss the code panel */ + onCloseCode?: () => void; } /* ── Tab Icons (Linear-style minimal SVGs) ── */ const TabIcons = { - desktop: ( - - - - - ), - browser: ( - - - - - - - - ), - code: ( - - - - ), + desktop: ( + + + + + ), + browser: ( + + + + + + + + ), + code: ( + + + + ), }; const CollapseIcon = ( - - - + + + ); const ExpandIcon = ( - - - + + + ); -type TabType = 'desktop' | 'browser' | 'code'; +type TabType = "desktop" | "browser" | "code"; /* ── Constants for resize constraints ── */ -const MIN_WIDTH = 300; // minimum panel width in px +const MIN_WIDTH = 300; // minimum panel width in px const MAX_WIDTH_VW = 0.65; // maximum panel width as fraction of viewport width /** @@ -91,293 +143,357 @@ const MAX_WIDTH_VW = 0.65; // maximum panel width as fraction of viewport width * so we use the viewport width minus sidebar instead of a fixed value. */ function calcHalfContainerWidth(): number { - // Try to measure the actual chat container - const container = document.querySelector('.chat-container') as HTMLElement | null; - if (container) { - return Math.max(MIN_WIDTH, Math.floor(container.clientWidth / 2)); - } - // Fallback: guess sidebar is ~60px, split the remaining viewport in half - return Math.max(MIN_WIDTH, Math.floor((window.innerWidth - 60) / 2)); + // Try to measure the actual chat container + const container = document.querySelector( + ".chat-container", + ) as HTMLElement | null; + if (container) { + return Math.max(MIN_WIDTH, Math.floor(container.clientWidth / 2)); + } + // Fallback: guess sidebar is ~60px, split the remaining viewport in half + return Math.max(MIN_WIDTH, Math.floor((window.innerWidth - 60) / 2)); } -export default function AgentBayLivePanel({ liveState, visible, onToggle, agentId, sessionId, onLiveUpdate, onClearCode, onCloseCode }: Props) { - const { t } = useTranslation(); - - // Keep a ref to the latest onLiveUpdate so TakeControl callbacks always - // call the current version, even when captured in stale closures. - const onLiveUpdateRef = useRef(onLiveUpdate); - useEffect(() => { - onLiveUpdateRef.current = onLiveUpdate; - }); - - // Take Control state - const [showTakeControl, setShowTakeControl] = useState(false); - - // Determine available tabs from live state - const availableTabs: TabType[] = []; - if (liveState.desktop) availableTabs.push('desktop'); - if (liveState.browser) availableTabs.push('browser'); - if (liveState.code) availableTabs.push('code'); - - const [activeTab, setActiveTab] = useState('desktop'); - const codeEndRef = useRef(null); - - const [panelWidth, setPanelWidth] = useState(() => calcHalfContainerWidth()); - const panelRef = useRef(null); - - // Recalculate on window resize to keep approximate 50% split - useEffect(() => { - const onResize = () => { - // Only auto-resize if user hasn't manually dragged - if (!isDragging.current && !userResized.current) { - setPanelWidth(calcHalfContainerWidth()); - } - }; - window.addEventListener('resize', onResize); - return () => window.removeEventListener('resize', onResize); - }, []); - const isDragging = useRef(false); - const userResized = useRef(false); // Once user manually drags, stop auto-resizing - const dragStartX = useRef(0); - const dragStartWidth = useRef(0); - - // Track latest data to auto-switch tabs when new activity arrives - const prevDesktopUrl = useRef(liveState.desktop?.screenshotUrl); - const prevBrowserUrl = useRef(liveState.browser?.screenshotUrl); - const prevCodeLength = useRef(liveState.code?.output?.length || 0); - - useEffect(() => { - // Switch to the tab that just received a new update - if (liveState.desktop?.screenshotUrl !== prevDesktopUrl.current) { - setActiveTab('desktop'); - prevDesktopUrl.current = liveState.desktop?.screenshotUrl; - } - if (liveState.browser?.screenshotUrl !== prevBrowserUrl.current) { - setActiveTab('browser'); - prevBrowserUrl.current = liveState.browser?.screenshotUrl; - } - const currentCodeLength = liveState.code?.output?.length || 0; - if (currentCodeLength !== prevCodeLength.current) { - setActiveTab('code'); - prevCodeLength.current = currentCodeLength; - } - - // Fallback: If current tab is completely gone, switch to first available - if (availableTabs.length > 0 && !availableTabs.includes(activeTab)) { - setActiveTab(availableTabs[0]); - } - }, [ - liveState.desktop?.screenshotUrl, - liveState.browser?.screenshotUrl, - liveState.code?.output, - availableTabs, - activeTab - ]); - - // Auto-scroll code output - useEffect(() => { - if (activeTab === 'code') { - codeEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - } - }, [liveState.code?.output]); - - /* ── Drag logic for the left resize handle ── */ - const handleDragMouseDown = useCallback((e: React.MouseEvent) => { - e.preventDefault(); - isDragging.current = true; - dragStartX.current = e.clientX; - dragStartWidth.current = panelWidth; - - // Set cursor state on body to prevent flicker while dragging - document.body.style.cursor = 'col-resize'; - document.body.style.userSelect = 'none'; - }, [panelWidth]); - - useEffect(() => { - const onMouseMove = (e: MouseEvent) => { - if (!isDragging.current) return; - // Moving left (smaller clientX) increases panel width - const delta = dragStartX.current - e.clientX; - const maxWidth = window.innerWidth * MAX_WIDTH_VW; - const newWidth = Math.min(maxWidth, Math.max(MIN_WIDTH, dragStartWidth.current + delta)); - setPanelWidth(newWidth); - }; - - const onMouseUp = () => { - if (!isDragging.current) return; - isDragging.current = false; - userResized.current = true; // User manually chose a width; stop auto-resizing - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - }; - - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - return () => { - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - }; - }, []); - - // Collapsed toggle button (shown when panel is hidden) - if (!visible) { - if (availableTabs.length === 0) return null; - return ( - - ); +export default function AgentBayLivePanel({ + liveState, + visible, + onToggle, + agentId, + sessionId, + onLiveUpdate, + onClearCode, + onCloseCode, +}: Props) { + const { t } = useTranslation(); + + // Keep a ref to the latest onLiveUpdate so TakeControl callbacks always + // call the current version, even when captured in stale closures. + const onLiveUpdateRef = useRef(onLiveUpdate); + useEffect(() => { + onLiveUpdateRef.current = onLiveUpdate; + }); + + // Take Control state + const [showTakeControl, setShowTakeControl] = useState(false); + + // Determine available tabs from live state + const availableTabs: TabType[] = []; + if (liveState.desktop) availableTabs.push("desktop"); + if (liveState.browser) availableTabs.push("browser"); + if (liveState.code) availableTabs.push("code"); + + const [activeTab, setActiveTab] = useState("desktop"); + const codeEndRef = useRef(null); + + const [panelWidth, setPanelWidth] = useState(() => calcHalfContainerWidth()); + const panelRef = useRef(null); + + // Recalculate on window resize to keep approximate 50% split + useEffect(() => { + const onResize = () => { + // Only auto-resize if user hasn't manually dragged + if (!isDragging.current && !userResized.current) { + setPanelWidth(calcHalfContainerWidth()); + } + }; + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }, []); + const isDragging = useRef(false); + const userResized = useRef(false); // Once user manually drags, stop auto-resizing + const dragStartX = useRef(0); + const dragStartWidth = useRef(0); + + // Track latest data to auto-switch tabs when new activity arrives + const prevDesktopUrl = useRef(liveState.desktop?.screenshotUrl); + const prevBrowserUrl = useRef(liveState.browser?.screenshotUrl); + const prevCodeLength = useRef(liveState.code?.output?.length || 0); + + useEffect(() => { + // Switch to the tab that just received a new update + if (liveState.desktop?.screenshotUrl !== prevDesktopUrl.current) { + setActiveTab("desktop"); + prevDesktopUrl.current = liveState.desktop?.screenshotUrl; + } + if (liveState.browser?.screenshotUrl !== prevBrowserUrl.current) { + setActiveTab("browser"); + prevBrowserUrl.current = liveState.browser?.screenshotUrl; + } + const currentCodeLength = liveState.code?.output?.length || 0; + if (currentCodeLength !== prevCodeLength.current) { + setActiveTab("code"); + prevCodeLength.current = currentCodeLength; } - const tabLabels: Record = { - desktop: 'Desktop', - browser: 'Browser', - code: 'Code', + // Fallback: If current tab is completely gone, switch to first available + if (availableTabs.length > 0 && !availableTabs.includes(activeTab)) { + setActiveTab(availableTabs[0]); + } + }, [ + liveState.desktop?.screenshotUrl, + liveState.browser?.screenshotUrl, + liveState.code?.output, + availableTabs, + activeTab, + ]); + + // Auto-scroll code output + useEffect(() => { + if (activeTab === "code") { + codeEndRef.current?.scrollIntoView({ behavior: "smooth" }); + } + }, [liveState.code?.output]); + + /* ── Drag logic for the left resize handle ── */ + const handleDragMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + isDragging.current = true; + dragStartX.current = e.clientX; + dragStartWidth.current = panelWidth; + + // Set cursor state on body to prevent flicker while dragging + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + }, + [panelWidth], + ); + + useEffect(() => { + const onMouseMove = (e: MouseEvent) => { + if (!isDragging.current) return; + // Moving left (smaller clientX) increases panel width + const delta = dragStartX.current - e.clientX; + const maxWidth = window.innerWidth * MAX_WIDTH_VW; + const newWidth = Math.min( + maxWidth, + Math.max(MIN_WIDTH, dragStartWidth.current + delta), + ); + setPanelWidth(newWidth); + }; + + const onMouseUp = () => { + if (!isDragging.current) return; + isDragging.current = false; + userResized.current = true; // User manually chose a width; stop auto-resizing + document.body.style.cursor = ""; + document.body.style.userSelect = ""; }; + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + return () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + }; + }, []); + + // Collapsed toggle button (shown when panel is hidden) + if (!visible) { + if (availableTabs.length === 0) return null; return ( -
- {/* Drag handle on the left edge */} -
+ {ExpandIcon} + + + ); + } + + const tabLabels: Record = { + desktop: "Desktop", + browser: "Browser", + code: "Code", + }; + + return ( +
+ {/* Drag handle on the left edge */} +
+ + {/* Header with tabs and collapse button */} +
+
+ {availableTabs.map((tab) => ( + + ))} +
+ {/* Take Control button — shown when browser/desktop has data */} + {agentId && + sessionId && + (activeTab === "browser" || activeTab === "desktop") && ( + + )} + {/* Clear button for code output */} + {activeTab === "code" && liveState.code && onClearCode && ( + + )} + {/* Close button for code panel */} + {activeTab === "code" && liveState.code && onCloseCode && ( + + )} + +
+ + {/* Content area */} +
+ {activeTab === "desktop" && liveState.desktop && ( +
+ Desktop preview - - {/* Header with tabs and collapse button */} -
-
- {availableTabs.map((tab) => ( - - ))} -
- {/* Take Control button — shown when browser/desktop has data */} - {agentId && sessionId && (activeTab === 'browser' || activeTab === 'desktop') && ( - - )} - {/* Clear button for code output */} - {activeTab === 'code' && liveState.code && onClearCode && ( - - )} - {/* Close button for code panel */} - {activeTab === 'code' && liveState.code && onCloseCode && ( - - )} - +
+ + Live
- - {/* Content area */} -
- {activeTab === 'desktop' && liveState.desktop && ( -
- Desktop preview -
- - Live -
-
- )} - - {activeTab === 'browser' && liveState.browser && ( -
- Browser preview -
- - Live -
-
- )} - - {activeTab === 'code' && liveState.code && ( -
-
{liveState.code.output}
-
-
- )} - - {/* Fallback: no content yet for the active tab */} - {((activeTab === 'desktop' && !liveState.desktop) || - (activeTab === 'browser' && !liveState.browser) || - (activeTab === 'code' && !liveState.code)) && ( -
- - {TabIcons[activeTab]} - - Waiting for {tabLabels[activeTab].toLowerCase()} activity... -
- )} +
+ )} + + {activeTab === "browser" && liveState.browser && ( +
+ Browser preview +
+ + Live
- - {/* Take Control fullscreen panel */} - {showTakeControl && agentId && sessionId && ( - computer session, browser tab => browser session - envType={activeTab === 'desktop' ? 'computer' : 'browser'} - onClose={() => setShowTakeControl(false)} - onLastScreenshot={(dataUri) => { - // Use the ref to always call the LATEST onLiveUpdate, - // avoids React closure-staleness in async handleCancel. - const env = activeTab === 'desktop' ? 'desktop' : 'browser'; - console.log('[LivePanel] Received last screenshot from TC, size:', dataUri.length, 'env:', env, 'onLiveUpdate:', !!onLiveUpdateRef.current); - if (onLiveUpdateRef.current) { - onLiveUpdateRef.current(env, dataUri); - } - }} - /> - )} -
- ); +
+ )} + + {activeTab === "code" && liveState.code && ( +
+
{liveState.code.output}
+
+
+ )} + + {/* Fallback: no content yet for the active tab */} + {((activeTab === "desktop" && !liveState.desktop) || + (activeTab === "browser" && !liveState.browser) || + (activeTab === "code" && !liveState.code)) && ( +
+ {TabIcons[activeTab]} + + Waiting for {tabLabels[activeTab].toLowerCase()} activity... + +
+ )} +
+ + {/* Take Control fullscreen panel */} + {showTakeControl && agentId && sessionId && ( + computer session, browser tab => browser session + envType={activeTab === "desktop" ? "computer" : "browser"} + onClose={() => setShowTakeControl(false)} + onLastScreenshot={(dataUri) => { + // Use the ref to always call the LATEST onLiveUpdate, + // avoids React closure-staleness in async handleCancel. + const env = activeTab === "desktop" ? "desktop" : "browser"; + console.log( + "[LivePanel] Received last screenshot from TC, size:", + dataUri.length, + "env:", + env, + "onLiveUpdate:", + !!onLiveUpdateRef.current, + ); + if (onLiveUpdateRef.current) { + onLiveUpdateRef.current(env, dataUri); + } + }} + /> + )} +
+ ); } diff --git a/frontend/src/components/AgentCredentials.tsx b/frontend/src/components/AgentCredentials.tsx index 943349e15..32ae6abd9 100644 --- a/frontend/src/components/AgentCredentials.tsx +++ b/frontend/src/components/AgentCredentials.tsx @@ -7,408 +7,548 @@ * Linear-style design with card-based credential list and modal editor. */ -import { useCallback, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { credentialApi } from '../services/api'; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { credentialApi } from "../services/api"; /* ── Types ── */ interface Credential { - id: string; - agent_id: string; - credential_type: string; - platform: string; - display_name: string; - status: string; - cookies_updated_at: string | null; - last_login_at: string | null; - last_injected_at: string | null; - has_cookies: boolean; - created_at: string; - updated_at: string; + id: string; + agent_id: string; + credential_type: string; + platform: string; + display_name: string; + status: string; + cookies_updated_at: string | null; + last_login_at: string | null; + last_injected_at: string | null; + has_cookies: boolean; + created_at: string; + updated_at: string; } interface FormData { - credential_type: string; - platform: string; - display_name: string; - cookies_json: string; + credential_type: string; + platform: string; + display_name: string; + cookies_json: string; } const EMPTY_FORM: FormData = { - credential_type: 'website', - platform: '', - display_name: '', - cookies_json: '', + credential_type: "website", + platform: "", + display_name: "", + cookies_json: "", }; /* ── Icons ── */ const PlusIcon = ( - - - + + + ); const KeyIcon = ( - - - - + + + + ); const TrashIcon = ( - - - + + + ); const EditIcon = ( - - - + + + ); const CloseIcon = ( - - - + + + ); const CookieIcon = ( - - - - - - + + + + + + ); /* ── Component ── */ interface Props { - agentId: string; + agentId: string; } export default function AgentCredentials({ agentId }: Props) { - const { t } = useTranslation(); - const [credentials, setCredentials] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - - // Modal state - const [showModal, setShowModal] = useState(false); - const [editingId, setEditingId] = useState(null); - const [form, setForm] = useState({ ...EMPTY_FORM }); - const [saving, setSaving] = useState(false); - const [formError, setFormError] = useState(''); - - // Delete confirmation - const [deletingId, setDeletingId] = useState(null); - - // Status badge styles - using translation keys - const getStatusConfig = useCallback((status: string) => { - const configs: Record = { - active: { bg: 'rgba(52, 199, 89, 0.12)', text: '#34c759', labelKey: 'agent.credentials.status.active' }, - expired: { bg: 'rgba(255, 149, 0, 0.12)', text: '#ff9500', labelKey: 'agent.credentials.status.expired' }, - needs_relogin: { bg: 'rgba(255, 59, 48, 0.12)', text: '#ff3b30', labelKey: 'agent.credentials.status.needs_relogin' }, - }; - return configs[status] || configs.active; - }, []); - - // Relative time helper using translations - const timeAgo = useCallback((dateStr: string | null): string => { - if (!dateStr) return ''; - const diff = Date.now() - new Date(dateStr).getTime(); - const mins = Math.floor(diff / 60000); - if (mins < 1) return t('agent.credentials.timeAgo.justNow'); - if (mins < 60) return t('agent.credentials.timeAgo.minutes', { count: mins }); - const hours = Math.floor(mins / 60); - if (hours < 24) return t('agent.credentials.timeAgo.hours', { count: hours }); - const days = Math.floor(hours / 24); - return t('agent.credentials.timeAgo.days', { count: days }); - }, [t]); - - const fetchCredentials = useCallback(async () => { - try { - setLoading(true); - const data = await credentialApi.list(agentId); - setCredentials(data); - } catch (e: any) { - setError(e.message || t('agent.credentials.error')); - } finally { - setLoading(false); - } - }, [agentId, t]); - - useEffect(() => { - fetchCredentials(); - }, [fetchCredentials]); - - const handleAdd = () => { - setEditingId(null); - setForm({ ...EMPTY_FORM }); - setFormError(''); - setShowModal(true); - }; - - const handleEdit = (cred: Credential) => { - setEditingId(cred.id); - setForm({ - credential_type: cred.credential_type, - platform: cred.platform, - display_name: cred.display_name, - cookies_json: '', // Never pre-fill cookies - }); - setFormError(''); - setShowModal(true); - }; - - const handleSave = async () => { - if (!form.platform.trim()) { - setFormError(t('agent.credentials.platformRequired')); - return; - } - - // Validate cookies JSON if provided - if (form.cookies_json.trim()) { - try { - const parsed = JSON.parse(form.cookies_json); - if (!Array.isArray(parsed)) { - setFormError(t('agent.credentials.cookiesInvalid')); - return; - } - } catch { - setFormError(t('agent.credentials.cookiesJsonInvalid')); - return; - } - } - - setSaving(true); - setFormError(''); - - try { - // Build payload — only include non-empty fields for updates - const payload: any = { - credential_type: form.credential_type, - platform: form.platform.trim(), - display_name: form.display_name.trim(), - }; - if (form.cookies_json.trim()) payload.cookies_json = form.cookies_json.trim(); - - if (editingId) { - await credentialApi.update(agentId, editingId, payload); - } else { - await credentialApi.create(agentId, payload); - } - - setShowModal(false); - await fetchCredentials(); - } catch (e: any) { - setFormError(e.message || t('agent.credentials.saveError')); - } finally { - setSaving(false); - } + const { t } = useTranslation(); + const [credentials, setCredentials] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + // Modal state + const [showModal, setShowModal] = useState(false); + const [editingId, setEditingId] = useState(null); + const [form, setForm] = useState({ ...EMPTY_FORM }); + const [saving, setSaving] = useState(false); + const [formError, setFormError] = useState(""); + + // Delete confirmation + const [deletingId, setDeletingId] = useState(null); + + // Status badge styles - using translation keys + const getStatusConfig = useCallback((status: string) => { + const configs: Record< + string, + { bg: string; text: string; labelKey: string } + > = { + active: { + bg: "rgba(52, 199, 89, 0.12)", + text: "#34c759", + labelKey: "agent.credentials.status.active", + }, + expired: { + bg: "rgba(255, 149, 0, 0.12)", + text: "#ff9500", + labelKey: "agent.credentials.status.expired", + }, + needs_relogin: { + bg: "rgba(255, 59, 48, 0.12)", + text: "#ff3b30", + labelKey: "agent.credentials.status.needs_relogin", + }, }; - - const handleDelete = async (id: string) => { - try { - await credentialApi.delete(agentId, id); - setDeletingId(null); - await fetchCredentials(); - } catch (e: any) { - setError(e.message || t('agent.credentials.deleteError')); + return configs[status] || configs.active; + }, []); + + // Relative time helper using translations + const timeAgo = useCallback( + (dateStr: string | null): string => { + if (!dateStr) return ""; + const diff = Date.now() - new Date(dateStr).getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return t("agent.credentials.timeAgo.justNow"); + if (mins < 60) + return t("agent.credentials.timeAgo.minutes", { count: mins }); + const hours = Math.floor(mins / 60); + if (hours < 24) + return t("agent.credentials.timeAgo.hours", { count: hours }); + const days = Math.floor(hours / 24); + return t("agent.credentials.timeAgo.days", { count: days }); + }, + [t], + ); + + const fetchCredentials = useCallback(async () => { + try { + setLoading(true); + const data = await credentialApi.list(agentId); + setCredentials(data); + } catch (e: any) { + setError(e.message || t("agent.credentials.error")); + } finally { + setLoading(false); + } + }, [agentId, t]); + + useEffect(() => { + fetchCredentials(); + }, [fetchCredentials]); + + const handleAdd = () => { + setEditingId(null); + setForm({ ...EMPTY_FORM }); + setFormError(""); + setShowModal(true); + }; + + const handleEdit = (cred: Credential) => { + setEditingId(cred.id); + setForm({ + credential_type: cred.credential_type, + platform: cred.platform, + display_name: cred.display_name, + cookies_json: "", // Never pre-fill cookies + }); + setFormError(""); + setShowModal(true); + }; + + const handleSave = async () => { + if (!form.platform.trim()) { + setFormError(t("agent.credentials.platformRequired")); + return; + } + + // Validate cookies JSON if provided + if (form.cookies_json.trim()) { + try { + const parsed = JSON.parse(form.cookies_json); + if (!Array.isArray(parsed)) { + setFormError(t("agent.credentials.cookiesInvalid")); + return; } - }; - - return ( -
- {/* Header */} -
-
- {KeyIcon} - {t('agent.credentials.title')} - {credentials.length} + } catch { + setFormError(t("agent.credentials.cookiesJsonInvalid")); + return; + } + } + + setSaving(true); + setFormError(""); + + try { + // Build payload — only include non-empty fields for updates + const payload: any = { + credential_type: form.credential_type, + platform: form.platform.trim(), + display_name: form.display_name.trim(), + }; + if (form.cookies_json.trim()) + payload.cookies_json = form.cookies_json.trim(); + + if (editingId) { + await credentialApi.update(agentId, editingId, payload); + } else { + await credentialApi.create(agentId, payload); + } + + setShowModal(false); + await fetchCredentials(); + } catch (e: any) { + setFormError(e.message || t("agent.credentials.saveError")); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (id: string) => { + try { + await credentialApi.delete(agentId, id); + setDeletingId(null); + await fetchCredentials(); + } catch (e: any) { + setError(e.message || t("agent.credentials.deleteError")); + } + }; + + return ( +
+ {/* Header */} +
+
+ {KeyIcon} + {t("agent.credentials.title")} + {credentials.length} +
+ +
+ + {/* Description */} +

{t("agent.credentials.description")}

+ + {/* Error */} + {error &&
{error}
} + + {/* Credential list */} + {loading ? ( +
+ {t("agent.credentials.loading")} +
+ ) : credentials.length === 0 ? ( +
+ {KeyIcon} + {t("agent.credentials.empty")} +
+ ) : ( +
+ {credentials.map((cred) => { + const statusConfig = getStatusConfig(cred.status); + return ( +
+
+
{cred.platform}
+ + {t(statusConfig.labelKey)} +
- -
- - {/* Description */} -

- {t('agent.credentials.description')} -

- - {/* Error */} - {error &&
{error}
} - - {/* Credential list */} - {loading ? ( -
{t('agent.credentials.loading')}
- ) : credentials.length === 0 ? ( -
- {KeyIcon} - {t('agent.credentials.empty')} + {cred.display_name && ( +
+ {cred.display_name} +
+ )} +
+ {cred.has_cookies && ( + + {CookieIcon} + {t("agent.credentials.meta.cookies")}{" "} + {cred.cookies_updated_at + ? `(${timeAgo(cred.cookies_updated_at)})` + : ""} + + )} + {cred.last_injected_at && ( + + {t("agent.credentials.meta.injected")}{" "} + {timeAgo(cred.last_injected_at)} + + )}
- ) : ( -
- {credentials.map((cred) => { - const statusConfig = getStatusConfig(cred.status); - return ( -
-
-
- {cred.platform} -
- - {t(statusConfig.labelKey)} - -
- {cred.display_name && ( -
{cred.display_name}
- )} -
- {cred.has_cookies && ( - - {CookieIcon} - {t('agent.credentials.meta.cookies')} {cred.cookies_updated_at ? `(${timeAgo(cred.cookies_updated_at)})` : ''} - - )} - {cred.last_injected_at && ( - - {t('agent.credentials.meta.injected')} {timeAgo(cred.last_injected_at)} - - )} -
-
- - -
- - {/* Delete confirmation */} - {deletingId === cred.id && ( -
- {t('agent.credentials.deleteConfirm.title', { platform: cred.platform })} -
- - -
-
- )} -
- ); - })} +
+ +
- )} - - {/* Add/Edit Modal */} - {showModal && ( -
setShowModal(false)}> -
e.stopPropagation()}> -
-

{editingId ? t('agent.credentials.modal.editTitle') : t('agent.credentials.modal.addTitle')}

- -
- -
- {formError &&
{formError}
} - - - - - - - -