Merge upstream/main into feat/shiki-re-highlighting-performance-dd3a

Conflict: packages/ui/src/components/chat/markdown/markdownCore.ts

main added per-image-mode markdown parsers (`imageMode` threaded through
`parseBlock` and into the block cache key); this branch replaced the
identity-keyed block cache with a content-addressed LRU. Resolution keeps the
content-addressed cache and folds `imageMode` into the content key, so the
`inline` and `label` renderings of the same source cannot answer for each other.
This commit is contained in:
Serhii Dziupin
2026-08-17 16:44:05 +03:00
466 changed files with 25608 additions and 10309 deletions
@@ -0,0 +1,93 @@
---
name: changelog-authoring
description: Use when drafting or updating user-facing CHANGELOG.md entries for the OpenChamber `[Unreleased]` section, including the VS Code extension changelog, summarizing changes since the latest git tag.
license: MIT
compatibility: opencode
---
## Overview
Draft user-facing bullet points for the `## [Unreleased]` section that summarize changes since the latest git tag up to `HEAD`.
Two files are maintained:
- `CHANGELOG.md` — main app (Web, Desktop, Mobile/PWA, shared UI).
- `packages/vscode/CHANGELOG.md` — VS Code extension only.
Only update the `[Unreleased]` bullets. Never add a new release header.
## Gather Context First
Read recent release sections for style. Determine the latest tag (or initial commit fallback), then inspect every commit and changed path through `HEAD`:
```bash
BASE=$(git describe --tags --abbrev=0 2>/dev/null || git rev-list --max-parents=0 HEAD)
git log --oneline "$BASE"..HEAD
git diff --stat "$BASE"..HEAD
```
Context gathering is complete when each user-visible change has evidence, platform reach, and contributor identity where available.
## Squashed PR Merges
A squashed merge commit often collapses a whole PR into a single terse subject line that omits valuable detail. When a commit looks like a squashed PR merge (subject ending in `(#123)`, or a `Merge pull request #123` commit), inspect the PR itself — its title and description usually carry the real user-facing context.
Use `gh pr view <number> --json number,title,body,author,mergedAt` for PR evidence.
- Prefer the PR description over the squashed commit subject when the description explains the user-visible change more accurately.
- Do not copy PR descriptions verbatim; distill them into the changelog style below.
- Use PR author/metadata to attribute contributor credit (see Contributor Credit).
- If `gh` is unavailable or the PR cannot be fetched, fall back to the commit message and diff, and note any uncertainty rather than inventing details.
## Writing Style
- Match the tone and level of detail of the existing changelog.
- Write like release notes for real users, not marketing. Be concrete and plain-spoken.
- Avoid generic payoff clauses ("making X faster", "improving reliability", "for a smoother workflow", "so you can...") unless the diff clearly proves that exact user-visible outcome.
- Prefer short direct bullets: what changed, where users see it, and only one obvious consequence.
- Omit internal implementation details; do not replace them with vague benefits. If a technical change has no user-visible effect, omit it or group under a plain reliability bullet.
- Avoid internal component names unless users see them (ex: "VS Code extension", "Desktop app", "Web app").
- Use area prefixes in the main changelog when they help grouping (e.g., "Chat:", "VSCode:", "Settings:", "Git:", "Terminal:", "Mobile:", "UI:").
- Do not include commit hashes, file paths, or implementation notes in changelog text.
- Do not mention low-level mechanics ("local refs first", "source of truth", "route", "store", "cache", "payload", "ref resolution"). Translate only when there is a clear user-facing symptom.
- Avoid LinkedIn-style language. Bad: "commit review is faster and branch history is more reliable." Better: "commit history can now show file diffs inline."
## Highlights and Ordering
- Sort bullets by user impact, not commit order. Breaking changes first, then significant new capabilities or broad user-visible improvements, then smaller features, fixes, and visual polish.
- Mark only the strongest highlights with a bold area prefix, such as `- **Chat attachments:** ...`. Usually the first 13 bullets; fewer when the release lacks substantial changes, more only when clearly justified.
- Treat a change as a highlight only when it introduces a substantial user-facing capability, materially changes a common workflow, or fixes a severe/widespread problem. Do not bold merely because a bullet is first, has a large diff, or was hard to implement.
- Keep related platform bullets together only when that does not push a more important change too far down.
- Rank highlights independently in each changelog. A main-app highlight is not automatically a VS Code highlight.
## VS Code Changelog Rules
- Craft entries only for behavior present in the VS Code extension. Exclude Desktop, Web, Mobile/PWA, and main-app-only UI.
- Do not copy shared/main bullets here unless changed files or code paths show the feature exists in the extension.
- Focus on core UI improvements and VS Code integration.
- Do NOT use "VSCode:" or "VS Code:" prefixes in this file.
- When unsure whether a change reaches the extension, leave it out.
## Contributor Credit
- Credit contributors inline with "(thanks to @username)" at the end of the bullet.
- Find usernames from commit authors (GitHub username, not email) or PR metadata when available.
- Skip credit when the contributor is `btriapitsyn` (repo owner).
## Completion Criteria
- For every bullet: "Could a user point to this in the UI or behavior?" If not, rewrite or drop it.
- For every VS Code bullet: verify the change applies to the extension, not just shared web UI or server code.
- For every bold bullet: "Would a user reasonably call this a headline change?" If not, unbold or move it lower.
- Read the finished list top to bottom; confirm each bullet is no more important than those above it, except where keeping related platform bullets together improves readability.
- Do not bundle unrelated changes to reduce bullet count. Prefer omitting minor internal fixes over vague catch-all sentences.
- Mention mostly-internal refactors only when there is a concrete user-visible fix; otherwise add no bullet.
The lists are complete when every bullet is supported by inspected evidence, points to user-observable behavior, is ranked by impact, appears only in changelogs whose runtime receives it, and credits eligible contributors.
## Workflow
1. Gather repo style and complete git/PR context.
2. Propose the new `[Unreleased]` bullet list for the main `CHANGELOG.md`.
3. Propose the VS Code-specific `[Unreleased]` list for `packages/vscode/CHANGELOG.md`.
4. Edit both files to update their respective `[Unreleased]` sections.
+13 -36
View File
@@ -17,36 +17,19 @@ Use this skill for terminal CLI work only (for example `packages/web/bin/*`).
Do not use this skill for web UI or VS Code webview styling work.
## Mandatory Rules
## Mode Contract
1. **Validation first**
- Safety and correctness checks must run in all modes.
- Prompts may help collect input, but cannot be the only guard.
Run safety and correctness validation before presentation in every mode. Prompts collect missing input; they never enforce policy alone.
2. **Mode parity is required**
- Behavior must be equivalent in:
- Interactive TTY
- Non-interactive shells
- `--quiet`
- `--json`
- Fully pre-specified flags
- Invalid operations must fail deterministically with non-zero exit code.
| Mode | Prompt | Output | Failure |
|---|---|---|---|
| Interactive TTY | Allowed when input is missing | Framed human output | Concise human error, non-zero exit |
| Fully specified flags | None required | Human output | Same policy and exit semantics |
| Non-TTY/piped | Never | Deterministic script-safe output | Non-zero without hanging |
| `--quiet` | Never | Essential result only | Concise error, non-zero exit |
| `--json` | Never | JSON only, including warnings/errors | JSON failure payload, non-zero exit |
3. **Prompt guard contract**
- Only prompt when all are true:
- stdout is TTY
- not `--quiet`
- not `--json`
- not automated/non-interactive context
4. **Output contract**
- `--json`: machine-readable output only.
- `--quiet`: suppress non-essential output only.
- Neither mode weakens policy enforcement.
5. **Cancellation contract**
- Handle prompt cancellation with `isCancel` + `cancel(...)`.
- Handle SIGINT cleanly and use consistent exit semantics.
Handle prompt cancellation with `isCancel` + `cancel(...)` and SIGINT with consistent exit semantics.
## Clack Primitive Standard
@@ -140,9 +123,9 @@ Quiet output should still be complete enough for scripts and quick human scannin
- Prefer this style over boxed notes for routine follow-up actions.
- Reserve `note`/boxed callouts for rare, high-context guidance where a long paragraph is truly necessary.
## Parity Verification Matrix
## Completion Criteria
For each command/subcommand, manually verify:
Every command/subcommand must have a tested answer for:
1. default interactive TTY output
2. `--quiet` output (minimal but informative)
@@ -154,13 +137,7 @@ For each command/subcommand, manually verify:
Load `references/snippets.md` when implementing prompt guards, non-interactive fallback, spinner lifecycle, or JSON/human output branching.
## Implementation Checklist
1. Add or update core validators first.
2. Ensure validators execute in all modes.
3. Add interactive Clack UX only as enhancement.
4. Verify parity between interactive and non-interactive flows.
5. Ensure script-safe deterministic failure behavior.
Implementation is complete when validators run before every mode branch, interactive Clack UX is only an enhancement, and all five cases above produce deterministic output and exit behavior.
## References
+6 -6
View File
@@ -5,16 +5,16 @@ description: Use when changing Electron main/preload code, desktop IPC, native w
# Desktop Shell
## Read First
## Required Context
Read `packages/electron/README.md` and nearby `packages/electron` code before editing.
Read `packages/electron/README.md` and nearby `packages/electron` code before editing. Context gathering is complete when each changed behavior is assigned to main, preload, renderer/shared UI, or web/runtime ownership.
Load `ui-api-decoupling` when a native change adds or alters a renderer-facing capability, `RuntimeAPIs`, runtime auth/URL behavior, or shared bridge contract. This skill owns the Electron privilege boundary; `ui-api-decoupling` owns the shared UI/runtime contract.
## Runtime Boundary
- Electron boots `@openchamber/web` in the same Node process and loads the UI over loopback. Do not introduce a sidecar server process.
- Keep OpenCode feature backends and shared domain logic in web/server or runtime APIs.
- Keep Electron focused on inherently native behavior: windows, menus, dialogs, notifications, updater, deep links, runtime host switching, privileged IPC, SSH, and tunnel lifecycle.
- Shared renderer-facing contracts belong in `packages/ui`; shared server behavior belongs in `packages/web`.
- Keep renderer contracts and domain logic in `packages/ui`, server behavior in `packages/web`, and Electron focused on inherently native behavior: windows, menus, dialogs, notifications, updater, deep links, runtime host switching, privileged IPC, SSH, and tunnel lifecycle.
- Electron is the desktop release target.
## IPC And Security
@@ -47,4 +47,4 @@ Non-user-visible child processes must never flash a console window.
## Validation
Run the Electron package type-check/lint commands from `package.json` and focused tests. For startup, preload, routing, or packaging changes, test both HMR development and bundled UI mode. For Windows process work, inspect the complete process tree and verify no console flash; a successful command alone is insufficient.
Run focused Electron tests and package checks. For startup, preload, routing, or packaging changes, completion requires both HMR development and bundled UI validation. For Windows process work, completion requires inspection of the complete process tree with no console flash; command success alone is insufficient.
+12 -28
View File
@@ -79,7 +79,7 @@ const onDragEnd = (e: DragEndEvent) => {
IDs must be **stable per item** (derive from the item's identity, e.g. `type:name`), never the array index — index ids break tracking after the first move.
## Minimal working pattern (wrapping, variable width, desktop + touch)
## Minimal Wiring
```tsx
import { DndContext, MouseSensor, TouchSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core';
@@ -97,41 +97,21 @@ const Item: React.FC<{ id: string; label: string; onClick: () => void }> = ({ id
);
};
const Row: React.FC<{ items: Item[]; onReorder: (next: Item[]) => void }> = ({ items, onReorder }) => {
const sensors = useSensors(
useSensor(MouseSensor, { activationConstraint: { distance: 8 } }),
useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 6 } }),
);
const onDragEnd = (e: DragEndEvent) => {
const { active, over } = e;
if (!over || active.id === over.id) return;
const from = items.findIndex(i => i.id === active.id);
const to = items.findIndex(i => i.id === over.id);
onReorder(arrayMove(items, from, to));
};
return (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
<SortableContext items={items.map(i => i.id)} strategy={rectSortingStrategy}>
<div className="flex flex-wrap gap-2">
{items.map(i => <Item key={i.id} id={i.id} label={i.label} onClick={i.onClick} />)}
</div>
</SortableContext>
</DndContext>
);
};
// Configure sensors per Rule 3, reorder onDragEnd per Rule 5, and choose the
// SortableContext strategy from Rule 2. This item wiring preserves item width.
```
A clickable element can be draggable at the same time: keep `onClick` on the button and the activation constraint (distance/delay) lets a plain click/tap through.
## Pitfalls we already hit (don't repeat)
## Symptom Index
| Symptom | Cause | Fix |
|---------|-------|-----|
| Dragged item **stretches** to the target slot width | `CSS.Transform.toString` applies scaleX/scaleY | Use `CSS.Translate.toString` (Rule 1) |
| On narrow/multi-row: items **don't reflow to other rows, overlap**, unclear drop target | `horizontalListSortingStrategy` on a wrapping row | Use `rectSortingStrategy` (Rule 2) |
| Dragged item **stretches** to the target slot width | Scale from `CSS.Transform.toString` | Rule 1 |
| On narrow/multi-row: items **don't reflow to other rows, overlap**, unclear drop target | Single-row strategy on wrapping layout | Rule 2 |
| **"Maximum update depth exceeded"** during drag + dragged element floats **offset from the cursor** | Live-reorder in `onDragOver` (empty strategy + `setState` each over) oscillates A↔B with variable sizes; the empty `DragOverlay` we paired with it was mispositioned | Don't reorder in `onDragOver`. Reorder once in `onDragEnd` (Rule 5). Only reach for live-reorder if you truly need physical row-reflow, and then guard against oscillation. |
| Touch drag scrolls the page instead of dragging | Missing `touch-action: none` | Add `touch-none` (Rule 4) |
| Touch: every finger move drags, or tap doesn't register | Single `PointerSensor` with distance | Split into MouseSensor + TouchSensor(delay) (Rule 3) |
| Touch drag scrolls the page instead of dragging | Missing touch ownership | Rule 4 |
| Touch: every finger move drags, or tap doesn't register | One sensor for mouse and touch | Rule 3 |
## If `rectSortingStrategy` still isn't crisp enough
@@ -142,3 +122,7 @@ Reordering variable-width chips across wrapped rows is a documented rough edge i
- Variable-width wrapping chips: `packages/ui/src/components/chat/DraftPresetChips.tsx`
- Single-row tab strip: `packages/ui/src/components/ui/sortable-tabs-strip.tsx`
- Library: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities` (already in `packages/ui/package.json`)
## Completion Criteria
Verify every applicable rule on desktop and touch. Wrapping layouts must preserve item width, reflow across rows, allow taps and scrolling before long-press activation, and reorder exactly once on drag end with stable IDs.
+6 -16
View File
@@ -9,8 +9,6 @@ description: Use when creating or modifying OpenChamber UI text, labels, buttons
User-facing UI text must go through `@/lib/i18n`; do not hardcode English strings in components.
Use this skill for any React UI change that adds or edits visible text, accessible labels, placeholders, tooltips, toasts, dialogs, settings labels, navigation labels, or empty/error states.
## Translate everything immediately (no English placeholders)
Every key you add to a non-English dictionary MUST contain a real translation in that language — never the English source string as a stand-in. There is NO "leave it in English for now" convention in this project; if an agent told you there was, it was wrong. Copying the English value into `es.ts`/`fr.ts`/`ko.ts`/`pl.ts`/`pt-BR.ts`/`uk.ts`/`zh-CN.ts`/`zh-TW.ts` is a defect, not a deferral. The app ships every locale at once, so an untranslated key is a visible bug for those users.
@@ -104,20 +102,11 @@ date
: t('dialog.delete.description', { count })
```
## What Counts As UI Text
## Translation Boundary
- Button and menu labels
- Settings labels and descriptions
- Placeholder text
- Tooltip content
- Dialog titles/descriptions/actions
- Toast title/description/action labels
- Empty/error/loading states
- `aria-label`, `title`, image `alt` text when user-facing
Translate visible text, placeholders, tooltips, dialogs, toasts, empty/error/loading states, and user-facing `aria-label`, `title`, and `alt` text.
## Exceptions
Do not translate:
Keep these literal:
- Product names: `OpenChamber`, `OpenCode`, `GitHub`
- Protocol/tool acronyms: `MCP`, `SSE`, `WebSocket`, `API`
@@ -125,10 +114,11 @@ Do not translate:
- File paths, command names, environment variables
- User/generated content
## Review Checklist
## Completion Criteria
- No new hardcoded user-facing English in changed UI files.
- Every new key exists in all dictionaries.
- Every new key exists in all dictionaries with a real translation.
- All translated values are resolved inside a reactive render/hook boundary.
- No locale state added to broad/shared stores.
- No full app remount for locale changes.
- Locale switch preserves current UI state.
@@ -9,15 +9,11 @@ description: Use when implementing, fixing, refactoring, or otherwise modifying
Make the smallest complete change and validate at the narrowest level that covers the real risk.
Identify existing behavior covered by tests or callers; preserve it unless the requested change explicitly replaces it.
## Before Editing
1. Read the nearest `DOCUMENTATION.md` and package `README.md` when present.
2. Inspect nearby implementation and tests before introducing a pattern.
3. Load every additional project skill whose trigger matches the change.
4. Classify the highest applicable change risk below.
5. Identify affected consumers, runtimes, persisted data, and public exports.
1. Inspect nearby implementation, callers, and tests before introducing a pattern.
2. Classify every applicable change risk below.
3. Identify every affected consumer, runtime, persisted format, and public export. This step is complete only when each risk has an owner and required validation.
When instructions materially conflict, stop and resolve the conflict instead of silently choosing one.
@@ -33,30 +29,23 @@ When instructions materially conflict, stop and resolve the conflict instead of
Apply every matching category. Do not escalate local work into workspace-wide ritual, and do not treat a type-only export as local merely because it emits no JavaScript.
## Mandatory Rules
## Structural Discipline
- Identify existing behavior covered by tests or callers; preserve it unless explicitly replaced.
- Do not add dependencies unless explicitly requested.
- Do not add compatibility paths without a concrete persisted or external consumer.
- Enforce security and correctness in core logic, not only UI controls or prompts.
- Never add, persist, or log secrets, bearer tokens, pairing data, or sensitive user content.
- Make data loss, partial failure, rollback, and fallback behavior explicit.
- Update owning documentation when module ownership, contracts, or invariants change.
- Complete the cumulative validation required by every applicable risk category.
## Engineering Preferences
- Prefer the smallest correct change; avoid drive-by refactors.
- Keep orchestration entrypoints thin and move domain logic to focused modules.
- Preserve behavior established by callers and tests unless the request replaces it. Keep the diff scoped to the complete requested behavior.
- Make the normal use-case path read top to bottom in domain terms. Keep orchestration entrypoints thin and move mechanics or domain logic behind focused, intention-revealing boundaries.
- Pull complexity downward only when a boundary hides meaningful mechanics, owns an invariant, isolates a proven integration, or captures stable repetition. Do not spread obvious code across pass-through layers.
- Prefer explicit dependencies and dependency injection over hidden module coupling.
- Follow local TypeScript types; avoid `any`, blind casts, and guessed payload shapes.
- Prefer early returns and explicit branches over nested conditionals.
- Reject invalid inputs and broken preconditions early so the valid path stays flat. Do not force a numeric happy-path/error-path ratio when correctness requires substantial failure handling.
- Require evidence before adding retries, caches, compatibility paths, lifecycle machinery, or generalized race handling. Security, data-loss, destructive-operation, and concurrency invariants still require proactive design when the risk is inherent to the operation.
- Make partial failure, rollback, cleanup, and user-visible outcomes explicit for destructive or multi-step work.
## Review Prompts
Before broadening a change, ask:
- Is the new abstraction reused or merely possible to reuse?
- What concrete complexity, invariant, stable repetition, or boundary does each new helper, interface, layer, and file pay for?
- Is the code in the package that owns the behavior?
- Does the change alter shared UI contracts across web, desktop, VS Code, or mobile?
- Does it change persisted data, IDs, routes, exports, generated files, or package entrypoints?
@@ -75,8 +64,6 @@ Do not hide a required architectural migration behind a local heuristic. Do not
## Validation Matrix
Use `package.json` scripts as the command source of truth.
| Change | Minimum validation |
|---|---|
| Executable source | Focused tests plus package-scoped type-check and lint |
@@ -108,15 +95,4 @@ For type-only shared contracts, validate compile-time consumers. Add runtime ser
- Run focused regression tests for the changed contract.
- Preserve unrelated changes encountered in shared files.
- Re-read the owning docs and update them when the implementation changed their truth.
- Do not claim runtime, relay, performance, or platform correctness from type-check/lint alone.
## Common Failure Modes
| Failure | Correction |
|---|---|
| Refactoring nearby code while fixing one bug | Keep the diff scoped unless the nearby change is required |
| Adding a helper used once | Keep direct code until reuse or composability is real |
| Swallowing an error for smoother UX | Preserve the failure signal and handle presentation separately |
| Updating a bridge without all runtimes | Load the runtime/API skill and make parity explicit |
| Running only broad checks | Add focused tests that exercise the changed behavior |
| Running only focused checks after a shared-contract change | Add workspace-wide validation |
- Perform a final simplification pass: remove speculative branches, shallow wrappers, stale compatibility, and names that do not clarify intent.
@@ -11,6 +11,8 @@ Optimize the amount and frequency of work before optimizing individual operation
**Core principle:** Make expensive work structurally unnecessary. A fast inner function still freezes the app when called millions of times on the main thread.
Load `sync-state-invariants` when an optimization changes state authority, reconciliation, optimistic data, event ordering, cache lifecycle, or destructive cleanup. This skill owns measured cost; `sync-state-invariants` owns state correctness.
## Start With A Performance Contract
Define before editing:
@@ -27,6 +29,8 @@ Do not optimize against a toy fixture when the report provides production scale.
## Workflow
Complete the numbered workflow in order. An optimization is complete only when the exact measured scenario meets its budget and separate correctness checks preserve every applicable state, identity, layout, and lifecycle transition.
### 0. Trust The Measurement Before Trusting The Number
A measurement setup that is wrong produces clean, confident, wrong numbers, and
@@ -65,6 +69,8 @@ validity checks ran.
Do not infer a bottleneck from code appearance when a trace or counter can identify it.
Treat every proposed optimization as a hypothesis. Memoization, caches, indexes, workers, scheduling, retries, and lifecycle machinery must address an observed cost or failure in the measured path; “could be slow” or “might race” is not evidence. Keep only the smallest mechanism that meets the contract, except where an inherent security, data-loss, destructive-operation, or concurrency invariant requires proactive protection.
**Never accept an "after" without a "before" on the identical scenario and
build.** Measuring a fixed build against a remembered number, a different
scenario, or a nearby baseline proves nothing: the mechanism you changed may
@@ -193,6 +199,8 @@ Add a cache only when all are explicit:
- runtime/project/user isolation where identities can collide;
- proof that caching removes enough work to meet the budget.
Do not introduce a cache merely to make an abstraction reusable or prepare for future consumers. First prove repeated work in the real path; then place the cache with the narrowest owner and lifetime that can invalidate it correctly.
A cache inside an `O(consumers × entities × candidates)` loop is a mitigation, not automatically a complete fix.
## Repository Tooling
@@ -276,24 +284,6 @@ Ship a bounded cache-only or local mitigation under deadline pressure only when:
If the interaction remains above budget, do not call the mitigation the completed performance fix.
## Common Rationalizations
| Rationalization | Reality |
|---|---|
| "The helper is cheap" | Multiply it by events, entities, candidates, and consumers. |
| "No component rerendered" | Selectors and equality comparisons may still burn CPU. |
| "`useMemo` fixes it" | Memoization does not help when dependencies churn or consumers duplicate work. |
| "The cache made it 10× faster" | Compare the result with the interaction budget, not only the baseline. |
| "Projects are few" | Identify the dimension that is large and the dimensions multiplying it. |
| "Move it to a worker" | Moving waste changes responsiveness, not total cost or data correctness. |
| "Empty means nothing exists" | Empty after failure or partial loading is not authoritative absence. |
| "We can optimize later" | Add a scale regression now or the multiplier will return. |
| "The profile is clean" | Prove the instrument fired and the renderer was not throttled. A disabled instrument looks identical to a fast app. |
| "It is much faster now" | Against which baseline, on which build, in which scenario? Re-run the unchanged build. |
| "Most of the time is `(program)`" | The sampler cannot see native work. Read the timeline trace. |
| "It does not reproduce here" | Compare your scale to the reporter's on the dimension the code keys on. |
| "It cannot hurt to keep the change" | An unmeasured change is unvalidated complexity that hides the path from the next investigation. |
## Exit Checklist
- [ ] Measurement validity established: no throttling, instruments confirmed firing, workload comparable.
+8 -13
View File
@@ -11,6 +11,8 @@ OpenChamber has a private relay: a client (mobile app, browser, another desktop)
Architecture overview: `packages/web/server/lib/relay/DOCUMENTATION.md`. Code: `packages/ui/src/lib/relay/` (client + shared, TS) and `packages/web/server/lib/relay/` (host, JS).
Load `ui-api-decoupling` when the change adds or alters a shared runtime API, URL/auth contract, bridge, proxy, or runtime-switch behavior. This skill owns relay mechanics; `ui-api-decoupling` owns the shared UI/runtime boundary.
**Why this skill exists:** relay bugs do not show up in normal testing. The event stream is SSE (which behaves differently from WebSockets), so a new WebSocket feature is often the *first* real WebSocket to cross the tunnel on mobile — and it fails there while working everywhere else. We have fixed the same class of bug across several iterations. The rules below are those lessons.
## The core mental model
@@ -20,7 +22,7 @@ Architecture overview: `packages/web/server/lib/relay/DOCUMENTATION.md`. Code: `
- HTTP and SSE authenticate with the client's **bearer token** (a header). They "just work" through the tunnel for any allowlisted `/api/*`, `/auth/*`, `/health` path.
- **WebSockets cannot send headers.** They authenticate with a short-lived **URL-scoped token** (`oc_url_token`) that must be minted first and passed as a query parameter. This is the source of most relay WS bugs.
## Rules for adding or changing a WebSocket endpoint
## WebSocket Endpoint Branch
Adding a new WS endpoint (or porting one, e.g. the planned terminal port) requires ALL of these, or it breaks over the relay:
@@ -32,19 +34,19 @@ Adding a new WS endpoint (or porting one, e.g. the planned terminal port) requir
4. **Do not touch origin handling.** The server rejects WS upgrades whose `Origin` it does not trust. Over the tunnel the host dials loopback and presents the loopback origin (`http://127.0.0.1:<port>`), which the server trusts as same-origin — this already covers every allowlisted WS path. **Never reintroduce reliance on `window.location.origin`**: in the iOS WKWebView it is `"null"`/empty for the custom scheme, so forwarding it produces a 403.
5. **Test over the relay, not just direct/desktop.** A new WS may be the first WebSocket the mobile client runs through the tunnel (events are SSE-locked on Capacitor). Passing on desktop or a direct connection proves nothing about the relay path.
## Rules for the tunnel/crypto/codec internals
## Wire Format And Codec Branch
- **Two implementations must stay byte-compatible.** The E2EE and framing exist as TS (`packages/ui/src/lib/relay/{crypto,handshake,tunnel-codec}.ts`, normative) and a JS host mirror (`packages/web/server/lib/relay/{e2ee,tunnel-codec}.js`). Any wire-format, frame-type, handshake, or batching change must update **both** and keep `packages/web/server/lib/relay/cross-compat.test.js` green.
- **Frame types live in `protocol.ts`** and must match across `protocol.ts`, `tunnel-codec.ts`, and `tunnel-codec.js`. Adding a frame type without mirroring it corrupts the stream on one side.
- **Frame batching is capability-negotiated** in the handshake with a legacy fallback, so mixed client/host app versions still interoperate. Preserve the negotiation and the single-frame fallback; do not make batching unconditional.
- **The encrypted-frame counter/IV is per-direction and strictly increasing.** One encrypted WS message = one encrypt call = one counter tick. Keep encrypt+send serialized per direction; do not reorder or parallelize it.
## Rules for the runtime transport layer
## Runtime Transport Branch
- Relay mode routes through `runtime-switch` (activates the tunnel singleton), `runtime-fetch` (routes runtime requests through it), `runtime-url`/`runtime-socket` (tunnel-backed URLs/sockets), and `runtime-auth` (mints the URL token through the tunnel). When refactoring any of these, preserve the relay branch and the direct-URL/Electron-realtime-proxy branches — they must remain byte-identical in behavior for non-relay runtimes.
- **The host dispatcher never injects credentials.** Tunneled requests carry the client's own token; the server authenticates them. Do not add host-side auth shortcuts, and do not trust loopback source address as authentication (relay traffic arrives at loopback but represents remote clients).
## Reconnect pacing
## Reconnect Branch
For indefinite SSE/WebSocket reconnect loops:
@@ -56,17 +58,10 @@ For indefinite SSE/WebSocket reconnect loops:
Blind short retries on hidden, offline, unauthorized, or stale-path clients waste battery and flood server logs.
## Testing guidance (a stub that skips auth/origin hides the exact bugs)
## Verification
- Exercise the real auth and origin gates. An end-to-end test whose stub server accepts any WS upgrade will pass while the real server rejects it — this is precisely how the origin-check bug shipped. When writing a relay integration test, mirror the real gates (`ensureSessionToken` via `oc_url_token`, `isRequestOriginAllowed`) or run against the real server pieces.
- Run relay tests per file (`bun test <file>`); the suite has order sensitivity.
- Validate both sides: `packages/ui` `type-check`/`lint`, and `node --check` on changed JS host files.
## Quick checklist before finishing relay-adjacent work
- [ ] New WS endpoint added to `ALLOWED_WS_PATHS` AND `isUrlAuthWebSocketPath`?
- [ ] UI opens it via `openRuntimeWebSocket`, not `new WebSocket`?
- [ ] URL token minted before the WS connects?
- [ ] No new dependence on `window.location.origin`?
- [ ] Wire/codec/handshake change mirrored in TS and JS, cross-compat test green?
- [ ] Direct and relay paths both still work; verified over the relay on the transport that actually uses it?
Completion requires every applicable branch above: WS path allowlists/auth/origin and real relay exercise; mirrored TS/JS wire changes with cross-compat coverage; preserved direct and relay runtime branches; or reconnect pacing under offline, hidden, permanent-failure, recovery, and abort conditions.
+10 -25
View File
@@ -7,44 +7,34 @@ description: Use when working with the OpenChamber iOS Simulator app without ope
Use `serve-sim` to stream and control a booted Apple Simulator from the terminal. It captures the simulator framebuffer, serves a browser preview, and exposes CLI controls for taps, typing, gestures, hardware buttons, rotation, memory warnings, permissions, camera injection, and accessibility inspection.
## OpenChamber Defaults
## Scripted Workflow
- Mobile package: `packages/mobile`
- iOS bundle id: `com.openchamber.app`
- Headless env wrapper: `packages/mobile/scripts/with-mobile-env.mjs`
- iOS simulator helper: `packages/mobile/scripts/ios-sim.mjs`
- Preferred scripts:
- `bun run mobile:build:ios:simulator`
- `bun run mobile:sim:run`
- `bun run mobile:sim:serve`
- `bun run mobile:sim:list`
- `bun run mobile:sim:kill`
- `bun run mobile:sim:dev` — foreground build + run + stream in one command (`--no-build` to skip the build); intended for the user, agents should prefer the discrete scripts above
Run the discrete scripts from the repository root so each step has an observable completion boundary:
## Workflow
1. Build the simulator app without opening Xcode:
1. Build the simulator app:
```sh
bun run mobile:build:ios:simulator
```
2. Boot a simulator if needed, install, and launch the app:
2. Boot if needed, install, and launch:
```sh
bun run mobile:sim:run
```
3. Start the browser stream in detached JSON mode:
3. Start the detached browser stream:
```sh
bun run mobile:sim:serve
```
Surface the returned `url` to the user. It normally starts at `http://127.0.0.1:3100`; always use the `url` from the JSON output rather than assuming the port.
Surface the returned JSON `url`; it is the only authoritative stream address.
4. Stop helpers when finished unless the user asks to keep them running:
```sh
bun run mobile:sim:kill
```
## Direct CLI Controls
Completion means the app launched, the returned stream URL was surfaced, requested interactions were verified, and helpers were stopped or intentionally left running.
## Manual Controls
- Tap normalized coordinates: `bunx serve-sim tap 0.5 0.5`
- Type focused text: `bunx serve-sim type "hello"`
@@ -64,9 +54,4 @@ Coordinates are normalized `0..1`, not pixels. Prefer `tap` for simple taps; do
- Node 18+.
- At least one simulator can be booted with `xcrun simctl`.
## Anti-Patterns
- Do not open Xcode just to build/install/launch during agent work; use the scripts above.
- Do not parse human output from `serve-sim`; use `-q` for JSON.
- Do not leave helper streams running unintentionally.
- Do not guess coordinates after accessibility lookup fails; report the missing target instead.
Use the scripts above instead of opening Xcode for build/install/launch. Consume JSON output rather than parsing human output. If accessibility lookup cannot identify a target, report the missing target instead of guessing coordinates.
+2 -2
View File
@@ -36,7 +36,7 @@ shape is genuinely missing.
| Field rows, checkboxes, radios, chips, selects, inputs, numeric steppers, info hints | `references/controls.md` |
| Adding/moving controls, pages, availability, anchors, or search entries | `references/search.md` |
Load every matching reference before editing.
Load each reference whose task branch applies; reference loading is complete when layout, control, and search implications are each classified.
## Quick Primitive Selection
@@ -77,7 +77,7 @@ Every stable Settings control addition or move must consider search in the same
Dynamic entity rows normally are not indexed. Load `references/search.md` for exact rules.
## Review Checklist
## Completion Criteria
- Built from shared primitives; no ad-hoc page/section/row markup.
- Explanatory text hidden behind `info`; warnings/syntax/status still visible.
+9 -17
View File
@@ -5,9 +5,9 @@ description: Use when changing session synchronization, bootstrap or reconnect s
# Sync State Invariants
## Read First
## Required Context
Read `packages/ui/src/sync/DOCUMENTATION.md` and the nearest owning module documentation before editing.
Read `packages/ui/src/sync/DOCUMENTATION.md` and the nearest owning module documentation before editing. Context gathering is complete when every changed state has an identified owner, authority, scope, and lifecycle.
## Sources Of Truth
@@ -22,6 +22,10 @@ Classify every input before deriving state:
Prefer deterministic authoritative records over heuristics. Derive live behavior from live channels, not historical anomalies.
Give each state and its invariants one owner. Callers request domain transitions from that owner; they do not inspect one field, mutate another collection, and repair status externally. Split ownership only when the states have genuinely independent lifecycles.
Represent mutually exclusive lifecycle states with discriminated unions or equally precise contracts. Avoid boolean/nullable field combinations that permit impossible states. Reject invalid transitions at the owning boundary so downstream reducers and effects receive trusted state.
## Failure Is Not Empty
Any authoritative loader whose result can replace, delete, or clear state must distinguish failure from successful empty data.
@@ -53,6 +57,7 @@ Inferring destructive cleanup from disappearance between snapshots requires an e
## Event Reducers
- Make the valid transition path explicit and flat. Return early for irrelevant entities and semantic no-ops; assert or reject transitions that violate an established invariant.
- Clone only fields the event mutates; preserve every unrelated reference.
- Return no change for semantically identical events.
- Gate scans behind cheap event/entity checks.
@@ -76,6 +81,7 @@ For streaming-frequency work, also load `performance-engineering`.
## Optimistic Updates
- Keep optimistic promotion, reconciliation, and rollback behavior behind the store/module that owns both visible and shadow state; do not expose collections for callers to mutate independently.
- Insert optimistic data into the visible store and a separate shadow tracker.
- Use client-generated IDs accepted and echoed by the server to reconcile in place.
- Remove optimistic data from both visible and shadow state on failure.
@@ -133,7 +139,7 @@ When state exists in memory and one or more persistent stores, define an explici
## Verification
Cover the relevant lifecycle, not only static state:
Cover every applicable lifecycle branch, not only static state. Verification is complete when failure cannot masquerade as empty success, stale or partial data cannot cause destructive replacement, and each transition remains with its owner:
- fresh bootstrap and successful empty result;
- fetch failure preserving prior state;
@@ -147,17 +153,3 @@ Cover the relevant lifecycle, not only static state:
- identity-preserving moves/category changes and runtime/scope changes resetting cleanup baselines;
- create, update, move, archive, and delete mutations surviving responses started before those mutations;
- missing versus empty persistence, malformed payloads, out-of-order writes, hydration races, and lifecycle durability behavior.
## Red Flags
- Fetch helper catches and returns `[]`.
- Historical message/session data drives a live spinner.
- One failed entity blocks or clears all entities.
- Light polling overwrites fields it did not fetch.
- Queue reads current model/agent at send time.
- New session lookup assumes SSE already indexed it.
- Optimistic data has no shadow entry or rollback.
- Snapshot-difference cleanup treats its first startup snapshot as a disappearance event.
- Eviction runs on the acquisition path, or a cache limit is raised in response to a request loop.
- Missing or malformed persistence becomes authoritative empty state.
- Debounced writes are canceled on owner/lifecycle change without completing against the captured owner or an explicit durability/data-loss contract.
+6 -25
View File
@@ -10,7 +10,7 @@ description: Use when creating or modifying OpenChamber UI components, styling,
- Use semantic OpenChamber theme tokens; never hardcode hex colors or generic Tailwind palette colors.
- Use shared UI primitives before introducing feature-local controls.
- Use the shared `Button`; do not create button wrappers such as `ButtonSmall` or `ButtonLarge`.
- Every dropdown-style value-picker trigger (shows current value, opens a picker) takes its chrome from `dropdownTriggerVariants` in `packages/ui/src/components/ui/dropdown-trigger.ts` (sizes: `sm` dense h-6, `default` forms h-8; native `SelectTrigger` consumes it). Call sites add layout classes only (width/truncation) — never re-declare border/radius/bg/hover. Deliberately chrome-less pickers (chat composer, headers) are the only exception.
- Every dropdown-style value-picker trigger takes its chrome from `dropdownTriggerVariants` in `packages/ui/src/components/ui/dropdown-trigger.ts`; call sites add layout classes only. Deliberately chrome-less pickers in composers or headers are the exception.
- Use the sprite-based `Icon`; never import icons directly from `@remixicon/react`.
- Apply hover tokens only to interactive elements.
- Use status colors only for actual status/feedback.
@@ -24,7 +24,7 @@ description: Use when creating or modifying OpenChamber UI components, styling,
| Adding, converting, storing, or generating icons | `references/icons.md` |
| Adding built-in or custom themes | `references/adding-themes.md` |
Load every matching reference before editing. Settings work must also load `settings-ui-patterns`; user-facing or accessible text must load `locale-ui-patterns`.
Load every matching reference before editing. User-facing or accessible text must load `locale-ui-patterns`. Settings composition is owned by `settings-ui-patterns`, which declares `theme-system` as its one-way companion.
## Token Decision
@@ -73,30 +73,11 @@ Use `IconName` for icon values stored in arrays, objects, state, or config. `Ico
## Animation Contract
Animate only `transform` and `opacity`. The compositor drives those; every other
property recalculates style on each frame for as long as the animation runs, and
geometry properties add layout on top. Measured on this repository's fixture,
identical at any element count from 1 to 32:
Animate only `transform` and `opacity`. Use `transform: rotate(...)`, not the individual `rotate` property. Non-composited properties recalculate style continuously; geometry also triggers layout, and wrappers, `will-change`, `contain`, or stepped timing do not remove that cost. Animate only while conveying live information.
| Animated property | Style recalculations/sec | Layouts/sec |
|---|---|---|
| `transform`, `opacity`, `filter` | 0 | 0 |
| `rotate` (the individual property) | 60 | 0 |
| `background-position`, `border-color`, `box-shadow` | 60 | 0 |
| `width` and other geometry | 60 | 60 |
For any other technique, load `performance-engineering` and `scripts/perf/DOCUMENTATION.md`, measure it with `bun run profile:animation`, and add a fixture variant when needed. This skill owns animation styling; `performance-engineering` owns performance evidence.
- `rotate: 360deg` is not a cheap synonym for `transform: rotate(360deg)`.
Prefer the `transform` form.
- Cost applies for the entire time an animation runs, so an indicator tied to a
long-running operation pays it continuously. An indicator that is not
conveying anything should not be animating.
- `will-change`, wrapper elements, `contain`, and `steps()` timing do not make a
non-composited property cheap. Only changing the property does.
- Verify with `bun run profile:animation` rather than reasoning about it; add a
variant to `scripts/perf/animation-fixture.html` for a technique not covered.
See `scripts/perf/DOCUMENTATION.md`.
## Verification
## Completion Criteria
- Animations are limited to `transform` and `opacity`, or their cost was measured and accepted.
- No hardcoded/palette colors were introduced.
@@ -104,4 +85,4 @@ identical at any element count from 1 to 32:
- Icons use `Icon`/`IconName`, and generated sprite changes are intentional.
- Hover, selection, primary, and status semantics are distinct.
- Light/dark/high-contrast and long-text states remain legible.
- Relevant type-check, visual/runtime validation, and generated-asset checks ran.
- Every applicable contract and loaded task reference was verified with relevant type-check, visual/runtime validation, and generated-asset checks.
@@ -43,6 +43,15 @@ export const presetThemes: Theme[] = [
bun run type-check && bun run lint && bun run build
```
## Authoring Tools
Both do the mechanical work of steps 12 and are run by hand:
- `node scripts/convert-vscode-theme.cjs <vscode-theme.json>` converts a VS Code
theme into this format and registers it in `presets.ts`.
- `node scripts/harmonize-theme.mjs <theme.json> [--write]` aligns accent roles
to one chroma/lightness target in OKLCH so borrowed colors read as one family.
## Key Files
- Theme types: `packages/ui/src/types/theme.ts`
+10 -2
View File
@@ -11,6 +11,7 @@ description: Use when creating or modifying OpenChamber shared UI data access, O
- OpenChamber-owned HTTP capabilities use `RuntimeAPIs` where runtime-specific behavior exists, otherwise explicit OpenChamber routes through `runtimeFetch`.
- Browser/realtime consumers use shared runtime URL/socket helpers.
- Shared UI never hardcodes localhost, ports, API origins, credentials, or one runtime's transport assumptions.
- Treat runtime adapters as the imperative shell: they own transport, auth, serialization, and platform mechanics. Shared feature code receives trusted contracts and owns domain decisions.
## Classify First
@@ -27,7 +28,7 @@ description: Use when creating or modifying OpenChamber shared UI data access, O
| Task | Required reference |
|---|---|
| Iframes, downloads, raw images, object URLs, URL tokens, preview proxy/subresources | `references/browser-assets-and-auth.md` |
| Iframes, downloads, raw images, object URLs, URL tokens | `references/browser-assets-and-auth.md` |
| Adding runtime capabilities, VS Code behavior, Electron privilege/security, unsupported runtime behavior | `references/runtime-parity.md` |
| Locating implementations, route registration, runtime switching, or focused tests | `references/implementation-map.md` |
@@ -45,6 +46,9 @@ Load every matching reference before editing.
8. **Authoritative fetches must signal failure.** Do not convert failure into a valid empty value that callers use to clear state.
9. **Keep privileges at the native/runtime boundary.** UI visibility and prompts are not authorization.
10. **Confirm trust-boundary mutations.** Host imports, credential writes, privileged deep links, and runtime switching require explicit user intent.
11. **Parse at the boundary.** Treat external, persisted, bridge, IPC, and network payloads as unknown until a schema, parser, or narrow constructor produces the trusted type consumed by shared code. Do not validate fields and then continue passing the raw payload.
12. **Model the real contract.** Prefer precise result/state unions and required dependencies over loose strings, boolean combinations, optional callback bags, `any`, or repeated casts. Make unsupported runtime behavior and failure distinct from valid empty success.
13. **Keep adapters deep and bridges thin.** Hide meaningful protocol or platform mechanics behind an intention-revealing runtime operation; do not add pass-through layers that only rename SDK, fetch, or bridge calls.
## HTTP Decision Rules
@@ -59,7 +63,7 @@ await runtimeFetch('/api/fs/raw', { query: { path } });
Do not immediately fetch a URL produced by `getRuntimeUrlResolver()`. Use the resolver only when the browser/realtime API itself consumes the URL:
```ts
const iframeSrc = getRuntimeUrlResolver().authenticatedAsset('/api/preview/frame');
const imageSrc = getRuntimeUrlResolver().authenticatedAsset('/api/fs/raw?path=diagram.png');
const eventUrl = getRuntimeUrlResolver().sse('/api/event');
```
@@ -69,6 +73,8 @@ Plain `fetch` is reserved for intentional external origins that are not the acti
Review runtime base URL, auth, SDK clients, terminal/realtime transports, stores, session memory, and caches. Key caches by runtime identity where IDs, paths, or URLs can collide. Reset or reconnect affected state through the established runtime-switch flow.
Re-parse values obtained after a switch at their owning boundary. A type established for one runtime response does not make cached raw data from another runtime trustworthy.
## Common Anti-Patterns
| Avoid | Use |
@@ -80,6 +86,8 @@ Review runtime base URL, auth, SDK clients, terminal/realtime transports, stores
| Web-only shared route | Explicit VS Code/mobile decision |
| Returning `[]` after authoritative fetch failure | Throw or distinct failure result |
| Rebuilding SDK `Request` from URL only | Preserve original request body/headers/signal |
| Component validates unknown JSON then passes it onward | Adapter parses once and returns a trusted contract |
| Boolean/nullable combinations for exclusive outcomes | Discriminated result or state union |
## Verification
@@ -28,12 +28,20 @@ Browser-owned URLs cannot attach the normal `Authorization` header. Use short-li
- Add browser-readable GET or realtime paths to the narrow allowlist in `packages/web/server/lib/ui-auth/ui-auth.js`.
- Add allowlist tests; never allow arbitrary `/api/*` URL-token access.
## Preview Iframes And Rewritten Resources
## Showing Somebody Else's Page
- Use preview proxy helpers so preview and URL tokens propagate to rewritten resources and redirects.
- Strip legacy client-token query parameters before forwarding upstream.
- Do not use `postMessage('*')`; target the known preview origin.
- Preserve CSP where possible. If injecting a bridge, prefer a per-response nonce and remove only directives that block framing or the bridge.
OpenChamber does not rewrite third-party HTML to display it. Rewriting a page to
serve it under our origin and a path prefix breaks every absolute URL on it, and
recovering from that means encoding knowledge of each framework's dev-server
internals — which ages badly and fails silently.
- The in-app browser renders a real Chromium `<webview>` (`packages/ui/src/components/browser/`).
- A dev server on a remote OpenChamber host is reached by binding a local port
and tunnelling raw bytes (`packages/web/server/lib/dev-tunnel/`), so the page
keeps its own origin at the root of its own host.
- Runtimes without a Chromium host fall back to a plain iframe that can display
a page but cannot inspect one. State that limit; do not emulate around it.
- Do not use `postMessage('*')`; target a known origin.
- Re-resolve browser URLs after runtime switches; do not retain URLs minted for an old runtime.
## Security Tests
@@ -43,4 +51,4 @@ Prefer focused coverage in:
- `packages/ui/src/lib/runtime-url.test.ts`
- `packages/ui/src/lib/runtime-auth.test.ts`
- `packages/web/server/lib/ui-auth/ui-auth.test.js`
- `packages/web/server/lib/preview/proxy-runtime.test.js`
- `packages/web/server/lib/dev-tunnel/tunnel.test.js`
@@ -42,7 +42,7 @@ Review every cache keyed only by session ID, directory, URL, or entity ID. Add r
- URL/auth: `packages/ui/src/lib/runtime-url.test.ts`, `runtime-auth.test.ts`
- Server auth: `packages/web/server/lib/ui-auth/ui-auth.test.js`
- Generic proxy: `packages/web/server/opencode-proxy.test.js`
- Preview proxy: `packages/web/server/lib/preview/proxy-runtime.test.js`
- Dev-server tunnel: `packages/web/server/lib/dev-tunnel/tunnel.test.js`
- VS Code bridge: `packages/vscode/webview/api/bridge.test.ts`
- VS Code proxy: `packages/vscode/src/bridge-proxy-runtime.test.js`
@@ -0,0 +1,80 @@
---
name: writing-for-agents
description: Writing documents for agents. Use when creating or editing skills or modifying AGENTS.md.
author: Matt Pocock
---
Reference for writing any document an agent consumes — a skill, an `AGENTS.md`, a doc reached by a pointer. The packaging differs; the writing does not: the same levers make each one predictable — the agent taking the same _process_ every run, not producing the same output.
## Context pointers
A **context pointer** is a reference held in the agent's context that names some out-of-context material and encodes the condition for reaching it. A skill's description is one; a line in `AGENTS.md` naming a doc is the same object. The pointer's _wording_, not its target, decides when the agent reaches the material — and how reliably. A must-have target behind a weakly worded pointer is a variance bug: sharpen the wording first, and inline the material only if sharpening fails.
A pointer does two jobs — state what the material is, and list the **branches** that should trigger reaching it (a branch is a distinct case the document handles, so different runs take different paths through it). Every word of an always-loaded pointer costs on every turn, so it earns even harder pruning than the body:
- **Front-load the leading word** — the pointer is where it does its triggering work.
- **One trigger per branch.** Synonyms that rename a single branch are one branch written twice; collapse them and keep only genuinely distinct branches.
- **Cut identity the body already carries.**
## The two loads
Every document and pointer you add spends one of two budgets:
- **Context load** — the cost of always-loaded material on the agent's window: an `AGENTS.md` line, a skill description, anything sitting in context every turn, spending tokens and attention whether or not it fires.
- **Cognitive load** — the cost on the human: which documents exist and when to reach for each. The human is the index. Not a cost to minimise — it is the price of human agency; spend it where human judgement matters, remove it where it does not.
Material reached only through a pointer escapes context load at the price of the pointer's own line; material with no pointer at all rides entirely on cognitive load.
## Information hierarchy
A document is built from two content types — **steps** (the ordered actions the agent performs) and **reference** (definitions, rules, facts consulted on demand) — that mix freely: all steps (a recipe), all reference (a review's rules, this skill), or both. The core decision is where each piece sits on the **information hierarchy**, a ladder ranked by how immediately the agent needs the material:
1. **In-file step** — the primary tier: what the agent does, in order.
2. **In-file reference** — consulted on demand. Often a legitimately flat peer-set (every rule of a review on one rung) — a fine arrangement, not a smell.
3. **Disclosed reference** — pushed out into a separate file, reached by a context pointer, loaded only when the pointer fires. Spans a sibling file in the same folder through fully external reference that lives anywhere and any document can point at.
Push too little down and the top bloats; push too much and you hide material the agent actually needs. That tension is the whole decision.
**Progressive disclosure** is the move down the ladder — out of the main file and behind a pointer — so the top stays legible. Not primarily a token optimisation: it is how the hierarchy is protected. Branching is the cleanest disclosure test: inline what every branch needs, and push behind a pointer what only some branches reach. When a document has steps, in-file reference that should be disclosed buries them and turns attending to them into a coin-flip — a variance lever, not just a legibility one.
**Co-location** is the within-file companion: where the ladder decides _how far down_ a piece sits, co-location decides _what sits beside it_ once there. Keep a concept's definition, rules, and caveats under one heading rather than scattered, so reading one part brings its neighbours with it. The test: the document should read like documentation written for the agent — grouped material reads that way; scattered material does not. (Distinct from duplication: that repeats one meaning in two places; scattering fragments one meaning across many.)
**Sprawl** is the failure mode here: a document simply too long, even when every line is live and unique. Attention thins across the excess, and every extra line is one more to keep relevant. The cure is the ladder: disclose reference behind pointers, and split by branch or sequence so each path carries only what it needs.
## Steps and completion criteria
Every step ends on a **completion criterion** — the condition that tells the agent the work is done. Two properties make it a lever:
- **Clarity** — can the agent tell done from not-done? A vague bound ("understanding reached") invites **premature completion**: ending the step before it is genuinely done, attention slipping to _being done_. The visible steps still ahead — the **post-completion steps** — supply the pull; the criterion's clarity is the resistance. Defend in order: **sharpen the bound first** (local and cheap); only if it is irreducibly fuzzy _and_ you observe the rush, hide the later steps by splitting the sequence — and hiding only works across a real context boundary (a hand-off or a subagent dispatch; an inline call leaves the later steps in context and clears nothing).
- **Demand** — how much it requires. "Every modified model accounted for" forces thorough work where "produce a change list" does not. Demand drives **legwork** — the digging the agent does within the work, latent in the wording rather than written as its own step — and it is not step-bound: "every rule applied" binds a body of flat reference just as "every step done" binds a sequence, which is how an all-reference document still carries an exhaustiveness bar.
The strongest criteria are both checkable and exhaustive.
## When to split
Splitting one document into two spends one of the two loads, so split only when the cut earns it:
- **By sequence** — split a run of steps where the post-completion steps tempt the agent to rush the one in front of it. Keeping them out of view drives more legwork on the current task. Beware the reverse: merging sequences exposes each step's later steps to what follows, inviting premature completion.
## Leading words
A **leading word** is a compact concept already living in the model's pretraining that the agent thinks with while running the document (_lesson_, _fog of war_, _tracer bullets_). Repeated as a token, never as a sentence, it accumulates a distributed definition and anchors a whole region of behaviour in the fewest tokens, by recruiting priors the model already holds. Coining your own works if you define it clearly, but a made-up word recruits no priors — you pay in definition tokens what a pretrained word gives free; reach for an existing word first.
It anchors twice. In the body, _execution_: the agent reaches for the same behaviour every time the word appears, and inside flat reference it focuses attention on a class of thing to look for. In a pointer, _invocation_: when the same word lives in your prompts, your docs, and your codebase, the agent links that shared language to the material and reaches it more reliably.
Hunt for opportunities to refactor with leading words. A triad spelled out at three sites, a pointer spending a sentence to gesture at one idea — each is a passage begging to collapse into a single token:
- "fast, deterministic, low-overhead" → _tight_ (a _tight_ loop).
- "a loop you believe in" → _red_ — a fuzzy gate becomes a binary observable state (the loop goes _red_ on the bug, or it doesn't).
You win twice: fewer tokens, and a sharper hook for the agent to hang its thinking on. Assume every document is carrying restatements that leading words retire — go find them.
**Negation** is the failure mode beside this lever: steering by prohibition drags the forbidden behaviour into context and makes it _more_ available, not less. _Don't think of an elephant_, and the elephant is all there is; the negation is a weak modifier the strongly-activated concept overruns, so the ban half-reads as an instruction to do the thing. Prompt the **positive** — state the target behaviour ("write one-line comments") so the banned one is never spoken. A prohibition earns its place only as a hard guardrail you cannot phrase positively; even then, pair it with the positive target so attention lands on what to do.
## Pruning
- Keep each meaning in a **single source of truth**: one authoritative place, so changing the behaviour is a one-place edit. **Duplication** — the same meaning in more than one place — costs maintenance and tokens, and inflates a meaning's prominence on the ladder past its real rank. (The accidental inverse of a leading word, which repeats a token on purpose, never the meaning.)
- For cross-document guidance, name one canonical owner. Other documents point to it and state only their local consequence; they do not restate the shared rule.
- The **environment** is a source of truth too — `package.json` scripts, config files, the directory layout, `--help` output — and a document that restates it is a **cache**: a copy of a lookup, earning its load only when the lookup is expensive. Cache what the agent cannot find by looking: the unwritten convention, the reason behind a choice, the gotcha no config confesses. Leave the one-file, one-command lookups to the environment, where they cannot go stale.
- Check every line for **relevance**: does it still bear on what the document does? A line loses relevance by never bearing on the task (mere exposition, or a branch that should be disclosed) or by going stale as the behaviour or world it describes changes. Shorter documents are easier to keep relevant. Without a pruning discipline the default fate is **sediment**: stale layers that settle because adding feels safe and removing feels risky, until you must core down through them to find what is still live.
- Hunt **no-ops** sentence by sentence: an instruction the model already obeys by default pays load to say nothing. The test — does it change behaviour versus the default? — is model-relative, not reader-relative: two people disagreeing about a no-op disagree about the default, and settle it by running the document, not by debate. When a sentence fails, delete the whole sentence rather than trim words from it. The test also grades leading words: a word too weak to beat the default (_be thorough_ when the agent is already thorough-ish) is a no-op, and the fix is a stronger word (_relentless_), not a different technique.
+3
View File
@@ -32,6 +32,9 @@ jobs:
- name: Lint
run: bun run lint
- name: Tests
run: bun run test
- name: Electron Linux packaging unit tests
working-directory: packages/electron
run: |
+360
View File
@@ -0,0 +1,360 @@
---
description: Create an anti-slop lint cleanup PR from the next generated batch
agent: build
---
You are working in the OpenChamber repository.
Goal: reduce anti-slop Oxlint findings in a small, reviewable maintenance PR.
This task can run unattended on a schedule, so it must be safe to start at any moment and must stop cleanly when there is nothing to do.
First, verify the worktree is safe to use:
`git status --porcelain`
If the output is not empty, stop immediately and report that the worktree has uncommitted changes. Do not stash, reset, discard, commit, or switch branches. Local work in progress must never end up in a maintenance PR.
Then run:
`bun run deslop -- next-batch --min-issues 60 --max-issues 120`
Use the command output as the source of truth for this task scope.
If the output contains `NO BATCH AVAILABLE`, stop immediately and report the printed reason. Do not create a branch, do not create a pull request, and do not look for other work. Concurrency is already handled: the command excludes files claimed by other active batches and refuses to exceed the active-batch limit.
Background: anti-slop is a vendored Oxlint plugin at `tools/oxlint/anti-slop/`, configured in `oxlint.config.ts`. It rejects low-evidence typing: unjustified type assertions, `unknown`/`object`/`Record<string, unknown>` contracts, ad hoc `typeof` narrowing, conditional `{}` spreads, and module mocking. Fixing a finding means giving the code real type evidence, never hiding the symptom.
Workflow:
- Before generating the batch, switch to `main` and pull the latest remote changes.
- Read the `next-batch` output carefully.
- Use the exact `Run ID`, `Batch name`, `Branch name`, and `PR title` printed by the command.
- Create the branch using the printed `Branch name`.
- Work only on the selected files listed in the batch output.
- Treat the selected files as complete-file scope. Do not cherry-pick only the first N findings.
- Read each selected file fully before editing it. These findings sit on type contracts, so a local edit can change behavior at a distant call site.
- Fix as many findings as practical in the selected files. Your default should be to fix selected findings, not to skip them.
## What a good fix looks like
Every finding is the same underlying complaint: the code claims less about a value than it actually knows. A good fix restores the missing knowledge. A bad fix hides the complaint while the knowledge stays missing. The rule cannot tell the difference, so you must.
Before editing, answer one question for the value in question: where does it actually come from? There are only three answers, and each has one correct fix.
1. It comes from code in this repository. The real type already exists somewhere upstream. Find it and use it. No parsing, no assertion.
2. It crosses an I/O boundary: HTTP response, `postMessage`, file contents, `localStorage`, a child process, the OpenCode SDK edge. Parse it once at that boundary, then let the parsed type flow onward untouched.
On parsing style, follow local precedent and do not introduce a new one. `zod` is declared as a dependency but is not currently used in the source, so a maintenance PR is the wrong place to start spreading it. Unless the file or package you are editing already parses with a schema library, write a small local parse function that takes the raw input, returns the domain type or `undefined`, and lives next to the boundary it guards. If you believe a schema library is genuinely warranted, skip the finding and say so in the PR body instead of introducing the pattern yourself.
3. It is genuinely dynamic, such as a plugin registry keyed by arbitrary strings. Then keep the open key but make the value type precise, and say so in the contract's name.
### `no-unsafe-dictionary-type`
Bad, and the most common lazy fix. The shape is known; the annotation throws it away.
```ts
type QuotaSnapshot = Record<string, unknown>;
function readLimit(snapshot: QuotaSnapshot) {
return snapshot.limit;
}
```
Good. Name the contract and state the fields the code actually reads.
```ts
type QuotaSnapshot = {
limit: number;
used: number;
resetsAt: string;
};
function readLimit(snapshot: QuotaSnapshot) {
return snapshot.limit;
}
```
Also good, when keys really are open but values are not.
```ts
type ProviderQuotas = Record<string, QuotaSnapshot>;
```
Still bad, and does not count as a fix:
```ts
type QuotaSnapshot = Record<string, any>;
type QuotaSnapshot = { [key: string]: object };
type QuotaSnapshot = Record<string, string | number | boolean | null>;
```
The third one is the sneaky one. Widening to a union of primitives satisfies the rule without describing anything. If you cannot name the fields, that is a signal the value is unparsed I/O; go to the boundary and parse it.
### `no-unknown-parameters`, `no-unknown-returns`, `no-unknown-type-aliases`
Bad. The function accepts anything and immediately guesses.
```ts
function applyThemeMessage(message: unknown) {
const theme = message as { themeId: string };
setTheme(theme.themeId);
}
```
Good. Parse at the boundary; the domain function receives a real type.
```ts
type ThemeMessage = { themeId: string };
function parseThemeMessage(data: MessageEvent["data"]): ThemeMessage | undefined {
if (data === null || typeof data !== "object") return undefined;
const themeId = Reflect.get(data, "themeId");
return typeof themeId === "string" ? { themeId } : undefined;
}
function applyThemeMessage(message: ThemeMessage) {
setTheme(message.themeId);
}
window.addEventListener("message", (event) => {
const message = parseThemeMessage(event.data);
if (message === undefined) return;
applyThemeMessage(message);
});
```
The parse function itself will still report `no-runtime-typeof` and `no-reflect-get`, because it is doing exactly what those rules describe. That is expected and acceptable: the checks are now concentrated in one named boundary function instead of scattered through domain logic, and the domain function above is genuinely typed. Report these remaining findings in the PR body rather than hiding them. Do not silence them with inline suppressions.
Note what changed at runtime: a malformed message is now ignored instead of silently producing `undefined` deeper in the call stack. That is a deliberate behavior decision and it belongs in the PR body. Never introduce a throw on a path that previously degraded quietly.
The `cause` convention is the single allowed exception: `unknown` is correct for an error cause.
### `no-known-value-widening`
Bad. The annotation erases the known keys, so callers lose autocomplete and typo safety.
```ts
const settingsBySlug: Record<string, SettingsSection> = {
appearance: appearanceSection,
keybindings: keybindingsSection,
};
```
Good. Keep inference and validate the shape.
```ts
const settingsBySlug = {
appearance: appearanceSection,
keybindings: keybindingsSection,
} satisfies Record<string, SettingsSection>;
```
`satisfies` checks every value against the contract while preserving the literal keys. Reach for it before anything else here.
### `no-chained-type-assertions` and `no-widen-then-assert`
Bad. The precise type existed and was thrown away, then guessed back.
```ts
const raw = loadSession() as unknown as SessionSnapshot;
```
Good. Fix the upstream contract so the round trip is unnecessary.
```ts
const snapshot = loadSession();
```
If `loadSession` genuinely returns something imprecise, that function is the real defect. Fix it there when it is inside the batch scope; if it is outside, make the minimal supporting change and say so in the PR body.
### `require-safety-comment-for-type-assertion`
The first move is always to delete the assertion, not to document it. Only a small minority of these findings deserve a comment.
Bad, and an automatic rejection at review:
```ts
// SAFETY: this is safe.
const session = value as Session;
// SAFETY: value is a Session.
const session = value as Session;
// SAFETY: required by TypeScript.
const session = value as Session;
```
These say nothing. A valid comment names the check that already ran and the line or function that ran it, so a reviewer can verify the claim without trusting you.
Good:
```ts
const parsed = sessionSchema.safeParse(payload);
if (!parsed.success) return undefined;
// SAFETY: sessionSchema.safeParse above confirmed every field of Session.
const session = parsed.data as Session;
```
If you cannot write such a sentence truthfully, you do not have an assertion problem, you have a missing check. Add the check.
### `no-conditional-empty-object-spread`
This one changes behavior more often than it looks, so read the consumer before editing.
Bad:
```ts
const body = {
sessionId,
...(title !== undefined ? { title } : {}),
};
```
Good, when the consumer distinguishes a missing key from an explicit `undefined`, which is true for anything serialized to JSON or merged over defaults:
```ts
const body: CreateSessionBody = { sessionId };
if (title !== undefined) body.title = title;
```
Good, when the consumer treats both the same:
```ts
const body = { sessionId, title };
```
Choosing wrongly here sends `"title": null` or drops a field on a real API call. If you cannot determine which behavior the consumer needs by reading it, skip the finding and say why.
### `no-runtime-typeof`
Bad. An ad hoc check in the middle of domain logic.
```ts
function resolveHost(stored: unknown) {
if (typeof stored === "string") return stored;
return DEFAULT_HOST;
}
```
Good. Read and validate where the value enters the program, then branch on real domain values.
```ts
function readStoredHost(): string {
const stored = localStorage.getItem(STORED_HOST_KEY);
return stored !== null && stored.length > 0 ? stored : DEFAULT_HOST;
}
```
Here the fix removed the check entirely, because `localStorage.getItem` already has a precise contract: `string | null`. The original `unknown` was self-inflicted. Look for this case first; it is more common than it seems.
When a real check is unavoidable, keep it inside one named boundary function as shown above, and accept that the boundary function keeps its finding. What is not acceptable is spreading the same check across domain code, or renaming it into a type predicate so it reads as intentional while nothing was actually established.
### `no-module-mocking`
Bad. The test mocks a module and therefore tests the mock.
```ts
mock.module("../lib/runtimeFetch", () => ({ runtimeFetch: async () => ({ ok: true }) }));
```
Good. Pass the dependency in, and let the test supply a real function.
```ts
async function loadStatus(fetchStatus: () => Promise<StatusResponse>) {
return fetchStatus();
}
test("returns the fetched status", async () => {
const status = await loadStatus(async () => ({ ok: true }));
expect(status.ok).toBe(true);
});
```
If introducing the seam would restructure production code well beyond the batch, skip the finding and say so. Do not fake a seam you do not believe in.
## How to know your fix is real
Before moving to the next finding, check all four:
- The code now knows something it did not know before. If you only rearranged syntax, it is not a fix.
- No new `any`, no new assertion, no new broad union invented to satisfy the checker.
- If you added parsing, you decided explicitly what happens on invalid input, and that decision is written in the PR body.
- If you changed a type used elsewhere, you searched for its call sites and updated them, rather than casting at the call site.
Handle findings deliberately instead of skipping them: for parsing work, add the smallest schema that covers the fields actually used; for contract changes, follow call sites with search and update them; for tests, prefer real seams over widened fixtures.
## Finish the file
A selected file is finished when it has zero anti-slop findings for the enabled rules, or when every remaining finding has an individual, specific reason to stay.
This matters beyond tidiness. A file left half-fixed will be selected again by a later batch, producing a second pull request over the same file, with its own template, its own review, and its own merge. Every finding you defer costs the repository owner a future review cycle. Treat "I fixed the easy half" as an incomplete task, not a delivery.
So, before you consider a selected file done:
- Re-run `bun run deslop -- file <path>` and read what is left.
- If findings remain, they must be the genuinely hard ones, and you must be able to explain each one specifically. "Requires a broader refactor" is only acceptable when you name the refactor, the module boundary it crosses, and why doing it here would make the change unreviewable.
- A group of findings sharing one root cause counts as one reason, and that root cause is usually worth fixing. If eleven findings in a file all come from one untyped parser, fixing that parser is the point of the batch, not a reason to skip.
- Leaving more than roughly a quarter of a file's findings behind means you have not finished. Either finish them or explain, per group, why the file was a bad selection in the first place.
Skip a finding only when the fix would require unclear behavior changes, or a change so large it would stop the pull request from being reviewable. Difficulty alone is not a reason. If skipped, give the specific reason in the PR body under `## Non-goals`.
Hard prohibitions. Each of these makes the lint output greener while making the code worse, and each is grounds for rejecting the whole PR:
- Do not disable, downgrade, or ignore anti-slop rules, in configuration or with inline comments.
- Do not add `any`, widen a type, or add an assertion in order to satisfy a rule.
- Do not write a generic or placeholder `// SAFETY:` comment. A comment that does not name a real, already-performed check is worse than the original finding.
- Do not invent a union of primitives to escape a dictionary rule.
- Do not move a rejected `typeof` check into a hand-written type predicate to get it out of the linter's way.
- Do not delete code, tests, or fields to make a finding disappear.
- Do not rename a symbol solely to dodge `no-shape-in-symbol-names`; rename it to what it actually is.
- Do not introduce a throw where the previous code degraded quietly. A parse failure on a path that used to fall back must keep falling back.
- Do not introduce a schema library, a new utility module, or a new architectural pattern as part of a lint cleanup.
- Do not edit `oxlint.config.ts` or `tools/oxlint/anti-slop/`.
- Do not edit `CHANGELOG.md`, package versions, or release metadata. This is internal maintenance with no user-facing change.
- Do not fix findings outside the selected files.
After edits, run:
`bun run deslop -- check-batch --run <run-id>`
Then validate the packages you actually touched, not the whole workspace. For each affected package run its own checks, for example:
`bun run --cwd packages/ui type-check`
`bun run --cwd packages/ui lint`
`bun run --cwd packages/ui test`
Workspace-wide `bun run type-check` and `bun run lint` are CI's job. Run them locally only when a change crosses package boundaries or touches shared contracts.
For files that TypeScript does not cover, such as server or CLI JavaScript, run the focused tests for that surface instead, for example `bun run --cwd packages/web test`.
Validation and delivery:
- Confirm selected files have fewer findings than before.
- Confirm `Findings outside selected files delta` is not positive. If it is, you introduced new findings elsewhere; fix them before continuing.
- If validation fails, fix failures only if the fixes stay within the task scope. Otherwise stop and report the blocker.
- Commit the changes with a concise message.
- Push the branch.
- Create exactly one PR with `gh pr create` using the exact printed `PR title`.
- After the PR is created, switch back to `main` and pull the latest remote changes again.
PR requirements. The repository has a mandatory pull request template at `.github/PULL_REQUEST_TEMPLATE.md`, and `AGENTS.md` requires it to be completed with concrete evidence for the final PR HEAD. Read the template and `CONTRIBUTING.md` before writing the description. Use every template heading, in the template's order, and do not invent replacement headings. Fill each section as follows.
- Use the exact printed `PR title`.
- `## Intent`: state that this is an unattended maintenance batch, name the `Run ID`, `Batch name`, and `Branch name`, and say what behavior changes. When nothing observable changes, say so explicitly rather than leaving it implied.
- `## Non-goals`: the findings left unfixed in the selected files, findings elsewhere in the repository, and any refactor you deliberately did not start. Give the reason for each, not just the count.
- `## Affected surfaces`: the packages, runtimes, user-visible states, and persisted or external contracts the diff reaches. Name every runtime the changed code runs in, and explain why an apparently applicable runtime is unaffected.
- `## Repository guidance`: fill the table. List the `AGENTS.md` rules you followed, every project skill that matched the change, required skill references you read, and the nearest `README.md` or `DOCUMENTATION.md` for the touched modules. For each row explain why it applies and how the change complies. Do not list filenames without explanation.
- `## Validation`: fill the table with the exact commands you ran and their results, including `check-batch` and every package-scoped type-check, lint, and test command, naming the packages. Record failures honestly, including pre-existing failures unrelated to this PR, and say which checks you did not run. Do not claim runtime behavior from type-check or lint alone.
- `## Visual evidence`: these PRs usually have no visible change, so explain concretely why the diff cannot affect rendered behavior. If anything user-visible did change, attach before/after evidence for the affected states.
- `## Risks and failure behavior`: cover what breaks if a change is wrong, how to roll it back, and any compatibility, data, performance, or cross-runtime concern. This is where every behavior-affecting decision belongs: each parsing decision you introduced and what now happens on invalid input, each `// SAFETY:` comment you added with the invariant it documents, and any change to whether an object key is present. State "None identified" only with a concrete reason.
Add a `## Manual testing recommendations` section after the template sections, with focused checks for the changed behavior, based on the selected files and actual edits. Type-contract changes can alter runtime behavior at call sites, so name the affected surfaces concretely.
Also state, inside `## Intent`, the selected files and how many findings `check-batch` reports as fixed and remaining.
Constraints:
- Keep the PR small and reviewable.
- Do not auto-merge.
- Do not modify unrelated files except minimal supporting changes required by selected-file fixes.
- Do not run broad formatting.
- Leave the batch's run directory intact after creating the PR. `next-batch` prints its location. That directory is both the handoff for the review follow-up task and the claim that stops another batch, including the React Doctor pipeline, from touching the same files. Deleting it early lets a parallel batch collide with this PR. Never delete it by hand; use `bun run deslop -- release --run <run-id>`.
- If you stop before creating a PR for any reason, release the claim with `bun run deslop -- release --run <run-id>` so the files return to the pool.
+70
View File
@@ -0,0 +1,70 @@
---
description: Follow up on an anti-slop PR by addressing review feedback
agent: build
---
You are working in the OpenChamber repository.
Goal: follow up on an existing anti-slop maintenance PR, address Greptile/review bot feedback, and clean up the local batch handoff files when done.
This task can run unattended on a schedule, so it must be safe to start at any moment and must stop cleanly when there is nothing to do.
First, verify the worktree is safe to use:
`git status --porcelain`
If the output is not empty, stop immediately and report that the worktree has uncommitted changes. Do not stash, reset, discard, or switch branches.
List the active batches:
`bun run deslop -- active`
The listing may include batches owned by the React Doctor pipeline; those are shown as `[pipeline rd]`. Never touch them.
Workflow:
- If there are no active batches, stop and report that there is nothing to follow up.
- Each active batch corresponds to one open PR. Read its `batch.json` for `runId`, `branchName`, `batchName`, `prTitle`, and selected files.
- Use `gh` to find the open PR for each batch branch.
- Work on the oldest batch that has an open PR with unaddressed feedback. If several qualify, handle exactly one and leave the rest.
- If a batch's PR was already merged or closed, do not treat it as follow-up work. Release its claim with `bun run deslop -- release --run <run-id>` so its files return to the pool, then continue looking.
- If no batch has an open PR with actionable feedback, stop and report that.
- Switch to the batch branch using the exact `branchName`.
- Pull or update the branch from remote if needed.
- Use `gh` to inspect PR review comments, PR issue comments, review threads if available, and check run summaries if relevant.
- Focus specifically on Greptile/review bot feedback and actionable reviewer comments.
- Pay particular attention to comments questioning whether a type contract is now wrong, whether a `// SAFETY:` comment is accurate, or whether a call site was missed. These are the likely real defects in this kind of PR.
- Address actionable comments with minimal follow-up fixes.
- Keep changes within the original selected files whenever possible.
- If a review comment requires changes outside the selected files, make only the minimal required supporting change.
- Do not perform unrelated cleanup.
- Do not rewrite the original PR.
- Do not force-push.
- Do not disable, downgrade, or ignore anti-slop rules, and do not add `any`, widen a type, or add an assertion to satisfy a reviewer comment.
- Follow the same fix standards as the original batch task, described in `.opencode/commands/as-fixes.md` under "What a good fix looks like" and "Hard prohibitions". Read that section before editing. Review pressure is exactly when a laundered fix is most tempting.
After fixes, run:
`bun run deslop -- check-batch --run <run-id>`
Then re-run the package-scoped checks for the packages you touched, for example `bun run --cwd packages/ui type-check`, `bun run --cwd packages/ui lint`, and `bun run --cwd packages/ui test`. Workspace-wide checks are CI's job.
Delivery:
- Commit follow-up fixes with a concise message.
- Push the branch.
- Reply to addressed review comments using `gh`.
- For each specific review comment you addressed, reply with what was changed and the follow-up commit hash.
- If the feedback was a general PR comment, add one general PR comment summarizing what was addressed, commit hashes, and validation results.
- Update the PR description so it stays true for the final HEAD: refresh `## Validation` with the checks you re-ran, and move any new behavior change into `## Risks and failure behavior`. Keep every heading of `.github/PULL_REQUEST_TEMPLATE.md` intact, and preserve content the repository owner added by hand, including screenshots. Read the live description before editing and merge into it rather than overwriting.
- If a comment is intentionally not addressed, reply with a concise reason.
- Do not release the batch while its PR is still open and awaiting review. The claim is what keeps parallel batches off these files.
- Release the batch only once its PR has been merged or closed: `bun run deslop -- release --run <run-id>`.
- After the follow-up is complete, switch back to `main` and pull the latest remote changes.
Constraints:
- Work on exactly one anti-slop batch PR.
- Prefer the oldest batch with an open PR.
- Do not auto-merge.
- Do not close the PR.
- Do not edit `CHANGELOG.md`, package versions, or release metadata.
- Do not release or delete handoff directories for batches you did not handle.
- If validation fails and cannot be fixed safely within scope, leave the batch claimed and report the blocker.
-58
View File
@@ -1,58 +0,0 @@
---
description: Draft user-facing CHANGELOG.md entries for [Unreleased]
agent: build
---
You are updating @CHANGELOG.md and @packages/vscode/CHANGELOG.md.
Goal: write user-facing bullet points for the `## [Unreleased]` section that summarize the changes since the latest git tag up to `HEAD`.
Style rules:
- Match the writing style of the existing changelog (tone + level of detail).
- Write like release notes for actual users, not a marketing summary. Be concrete and plain-spoken.
- Avoid generic payoff clauses like "making X faster", "improving reliability", "for a smoother workflow", or "so you can..." unless the diff clearly proves that exact user-visible outcome.
- Prefer short direct bullets: what changed, where users see it, and only one consequence if it is obvious.
- Avoid internal implementation details, but do not replace them with vague benefits. If a technical change has no clear user-visible effect, omit it or group it under a plain reliability bullet.
- Avoid internal component names unless users see them (ex: "VS Code extension", "Desktop app", "Web app").
- For @packages/vscode/CHANGELOG.md: Craft entries specifically for behavior that is present in the VS Code extension. Exclude Desktop app, Web app, Mobile/PWA, and main-app-only UI. Do not copy shared/main changelog bullets into this file unless changed files or code paths show the feature exists in the extension. Focus on core UI improvements and VS Code integration. Do NOT use "VSCode:" or "VS Code:" prefixes in this file.
- Prefer grouping by platform only if it reads better.
- No new release header; only update the `[Unreleased]` bullets.
- Don't include implementation notes, commit hashes, or file paths in the changelog text.
- Use area prefixes when helpful for grouping in the main @CHANGELOG.md (e.g., "Chat:", "VSCode:", "Settings:", "Git:", "Terminal:", "Mobile:", "UI:").
- Credit contributors inline using "(thanks to @username)" at the end of the bullet. Find contributor usernames from commit authors (not email, but a github username) or PR metadata when available. Skip if contributor is btriapitsyn, since this is a repo owner.
Highlights and ordering:
- Review several recent release sections before drafting. Match how they reserve bold area prefixes for release highlights and order the remaining bullets by user importance.
- Sort bullets by user impact, not commit order. Put breaking changes first, then the most significant new capabilities or broad user-visible improvements, followed by smaller features, fixes, and visual polish.
- Mark only the strongest release highlights with a bold area prefix, such as `- **Chat attachments:** ...`. Usually this is the first 1-3 bullets, but use fewer when the release does not contain enough substantial changes and more only when clearly justified.
- Treat a change as a highlight when it introduces a substantial user-facing capability, materially changes a common workflow, or fixes a severe/widespread user-facing problem. Do not bold a bullet merely because it is first, has a large diff, or was difficult to implement.
- Keep related platform bullets together only when that does not push a more important change too far down the list.
- Rank highlights independently in the main and VS Code changelogs. A main-app highlight is not automatically a VS Code highlight, and the extension may have different top changes.
Quality checks before editing:
- For every bullet, ask: "Could a user point to this in the UI or behavior?" If not, rewrite it or drop it.
- For every VS Code bullet, verify the change applies to the extension, not just shared web UI or server code. When unsure, leave it out of @packages/vscode/CHANGELOG.md.
- For every bold bullet, ask: "Would a user reasonably describe this as one of the release's headline changes?" If not, remove the bold styling or move it lower.
- Read the finished list top to bottom and confirm that each bullet is no more important than the bullets above it, except where keeping closely related platform bullets together improves readability.
- Do not mention low-level mechanics such as "local refs first", "source of truth", "route", "store", "cache", "payload", or "ref resolution". Translate only when there is a clear user-facing symptom.
- Do not bundle unrelated changes just to reduce bullet count. It is better to omit minor internal fixes than to create a vague catch-all sentence.
- Avoid LinkedIn-style language. Bad: "commit review is faster and branch history is more reliable." Better: "commit history can now show file diffs inline." Bad: "installed-state accuracy is improved." Better: "the skills list now matches OpenCode's installed skills more closely."
Determine the base version:
- Use the latest tag (ex: `v1.3.2`) as the base.
- Inspect all commits after the base up to `HEAD`.
Repo context for style:
!`head -140 CHANGELOG.md`
Git context (base tag, commits, changed files):
!`BASE=$(git describe --tags --abbrev=0 2>/dev/null || git rev-list --max-parents=0 HEAD); echo "Base: $BASE"; echo "Commits since base: $(git rev-list --count "$BASE"..HEAD)"; echo "Diff stats: $(git diff --shortstat "$BASE"..HEAD)"; echo; echo "=== Top 30 commits ==="; git log --oneline -30 "$BASE"..HEAD; echo; echo "=== Changed files ==="; git diff --stat "$BASE"..HEAD`
Additional hints (optional, use only if needed):
- If there are breaking changes or user-visible behavior changes, call them out first.
- If changes are mostly internal refactors, mention them only when there is a concrete user-visible fix. Otherwise do not add a changelog bullet for them.
Now:
1) Propose the new `[Unreleased]` bullet list for the main @CHANGELOG.md.
2) Propose the VS Code-specific `[Unreleased]` list for @packages/vscode/CHANGELOG.md.
3) Edit both files to update their respective `[Unreleased]` sections.
+133
View File
@@ -0,0 +1,133 @@
---
description: Deeply review every open maintenance PR and fix it to completion, not by commenting
agent: build
---
You are working in the OpenChamber repository.
Goal: take every open automated maintenance pull request and bring it to a state where a human reviewer would merge it without a single objection. You are the intelligence layer between unattended batch tasks and the repository owner. The batch tasks optimize for a metric; you optimize for the code being right.
You do not leave review comments. You do the work. A finding you notice and do not fix is a failure of this task.
## Scope of the run
In scope: every open pull request whose head branch starts with `anti-slop/` or `react-doctor/`.
Find them:
`gh pr list --state open --search "head:anti-slop/" --json number,title,headRefName,url`
`gh pr list --state open --search "head:react-doctor/" --json number,title,headRefName,url`
Work through them one at a time, oldest first. Finish a PR completely before starting the next. Do not interleave.
This task ends only when every PR in the list has been reviewed, fixed, validated, pushed, and its description updated. Do not stop at the first one. Do not stop because a PR looks acceptable at a glance; that judgement comes after reading the diff, not before.
## Before you start
Verify the worktree is clean:
`git status --porcelain`
If the output is not empty, stop immediately and report it. Do not stash, reset, or discard anything.
Read `AGENTS.md`, and read `.opencode/commands/as-fixes.md` in full, including the sections "What a good fix looks like" and "Hard prohibitions". Those describe the standard the anti-slop PRs were supposed to meet. Your job includes verifying they actually met it.
Load every project skill matching the code you end up touching, exactly as `AGENTS.md` requires. These PRs reach into sync, stores, UI, runtime, and CLI code, and the applicable skill is determined by what you change, not by the fact that this is maintenance work.
## Working on one PR
Check out the branch and bring it up to date with `main`:
`gh pr checkout <number>`
`git merge origin/main`
If the merge conflicts, resolve it correctly by reading both sides. Never resolve a conflict by taking one side wholesale to save time.
Then read the entire diff against `main`, not just the changed lines:
`git diff origin/main...HEAD`
For every file in the diff, open the file itself and read the surrounding code. These PRs change type contracts and component structure, so a line that looks correct in isolation is frequently wrong in context.
## What you are looking for
Treat the PR body's claims as unverified. Re-run the checks yourself; do not trust reported results.
Correctness of the change itself:
- Did the change alter runtime behavior? Effect cleanup, hook dependencies, component extraction, conditional object spreads, and added parsing all can. Decide whether the new behavior is right, not merely whether it is different.
- Does a removed or reordered object key change what gets serialized to an API, persisted to disk, or merged over defaults? A key that used to be absent and is now present as `undefined` is a real change.
- Was dead code removed that is actually referenced somewhere the batch task did not search, including dynamic imports, string-keyed lookups, generated assets, and other packages?
- Did a type contract change without every call site being updated? Search for each changed symbol across the workspace.
- Did an extracted component lose state, memoization, ref forwarding, or a stable identity that the original had?
Honesty of the change:
- Is any `// SAFETY:` comment vague, generic, or untrue? A comment must name the check that already ran. If it does not, either delete the assertion by fixing the contract, or write the truthful comment.
- Was a type laundered rather than fixed? Look for invented primitive unions, `any`, new assertions, hand-written type predicates that merely relocate a rejected `typeof` check, or deleted fields and tests.
- Was a lint rule disabled, downgraded, ignored, or suppressed inline anywhere in the diff? Revert that and fix the underlying code.
- Was a new dependency, schema library, utility module, or architectural pattern introduced under the cover of cleanup? Remove it and solve the problem within existing precedent.
Quality of the result:
- Does the new code read like the code around it, in naming, structure, and comment density?
- Are the new names accurate, or do they describe the refactor instead of the domain?
- Is the change complete, or did the batch task fix eight of eleven findings in a file and leave three arbitrary ones behind?
## Fixing
Fix everything you find, on the PR branch, as additional commits. You are explicitly permitted to go beyond the batch's original file scope when correctness requires it: update call sites, correct an upstream contract, add a missing test, or finish an incomplete refactor.
Two boundaries on that freedom:
1. Do not touch files that another open maintenance PR modifies. Check with `gh pr diff <other-number> --name-only` for the other open PRs in this run. If a correct fix genuinely requires such a file, make the change in whichever PR already owns that file, and note the cross-PR dependency in both descriptions.
2. Do not turn a maintenance PR into a feature or a redesign. If you conclude the batch's approach was wrong at the root, revert that part of the diff rather than building on it, and explain the revert in the PR body. A smaller correct PR beats a larger clever one.
Do not disable, downgrade, or ignore lint rules. Do not add `any`, widen a type, or add an assertion to make a check pass. Do not edit `oxlint.config.ts`, `tools/oxlint/anti-slop/`, `CHANGELOG.md`, package versions, or release metadata.
If a batch left findings unfixed and the PR body called them skipped, evaluate each one yourself. Fix the ones that are fixable within a correct, reviewable change. Keep a skip only when you can articulate why fixing it would be wrong here, not merely hard.
## Validating each PR
Re-run the pipeline's own check for the batch, using the run id from the PR body when it is present:
`bun run deslop -- check-batch --run <run-id>` for anti-slop PRs
`bun run doctor -- check-batch --run <run-id>` for React Doctor PRs
If the run directory no longer exists, skip that command and say so; it is a convenience, not the source of truth.
Then, for every package the final diff touches, run its own checks:
`bun run --cwd packages/<name> type-check`
`bun run --cwd packages/<name> lint`
`bun run --cwd packages/<name> test`
Run `bunx oxlint <changed-paths>` on the files in the diff and confirm you have not increased anti-slop findings anywhere.
For surfaces TypeScript does not cover, such as server JavaScript, CLI JavaScript, or Electron main-process helpers, run the focused tests for that surface. Static checks do not prove those correct.
If a check fails for a reason unrelated to this PR, verify that claim by checking the same command on `main` before dismissing it, and report the result either way.
## Delivering each PR
- Commit your fixes with concise messages describing what was actually wrong.
- Push to the PR branch. Never force-push.
- Update the PR description so it describes the final state, using every heading of `.github/PULL_REQUEST_TEMPLATE.md` in the template's order. `## Intent` covers what the batch did and what you corrected; `## Non-goals` covers what you deliberately left alone; `## Affected surfaces` must reflect the final diff, including files you added beyond the batch scope; `## Repository guidance` must list the rules, skills, and module documentation that applied to your own edits, not only the batch's; `## Validation` must contain the exact commands you re-ran and their results; `## Risks and failure behavior` must carry every behavior change you accepted or introduced. A description that still describes only the batch's original work is incomplete.
- Preserve any content the repository owner added to the description by hand, including screenshots. Read the live description before editing it and merge your changes into it rather than overwriting.
- Add one PR comment summarizing your review pass, so the history shows what was examined and what was changed.
- Do not merge, do not close, do not approve, and do not request review.
- Do not release the batch claim. The batch stays claimed until its PR is merged or closed.
Then move to the next PR.
## Finishing the run
When every PR has been handled, return to `main` and pull:
`git checkout main && git pull`
Report, per PR: number, title, what was wrong, what you fixed, what you deliberately left alone and why, validation results, and your assessment of whether it is now ready to merge. State plainly if any PR is not ready and what blocks it.
If you found nothing wrong in a PR, say that explicitly and describe what you checked to reach that conclusion. That is a valid outcome, but only after real inspection.
+38 -14
View File
@@ -7,12 +7,22 @@ You are working in the OpenChamber repository.
Goal: reduce React Doctor diagnostics in a small, reviewable maintenance PR.
Start by running:
This task can run unattended on a schedule, so it must be safe to start at any moment and must stop cleanly when there is nothing to do.
First, verify the worktree is safe to use:
`git status --porcelain`
If the output is not empty, stop immediately and report that the worktree has uncommitted changes. Do not stash, reset, discard, commit, or switch branches. Local work in progress must never end up in a maintenance PR.
Then run:
`bun run doctor -- next-batch --min-issues 75 --max-issues 120`
Use the command output as the source of truth for this task scope.
If the output contains `NO BATCH AVAILABLE`, stop immediately and report the printed reason. Do not create a branch, do not create a pull request, and do not look for other work. Concurrency is already handled: the command excludes files claimed by other active batches and refuses to exceed the active-batch limit.
Workflow:
- Before generating the batch, switch to `main` and pull the latest remote changes.
- Read the `next-batch` output carefully.
@@ -23,7 +33,10 @@ Workflow:
- Fix as many diagnostics as practical in the selected files. Your default should be to fix selected diagnostics, not to skip them.
- Prefer direct, behavior-preserving fixes: missing effect cleanup, mutable effect dependencies, accessibility issues with semantic fixes, local performance improvements, Tailwind shorthand replacements, component extraction when the boundary is clear, dead-code removal after verifying no references, and reducer or derived-state cleanup when the state relationship is local and clear.
- Handle larger diagnostics deliberately instead of skipping them: for component splits, extract the smallest coherent subcomponent that reduces the diagnostic while preserving props/state flow; for dead code, verify references with search before deleting exports, types, or files; for state architecture issues, prefer the smallest local reducer or derived-state simplification that preserves behavior; for render-function extraction, extract only stable render helpers that do not depend on large implicit closure state, or pass explicit props; for behavior-sensitive diagnostics, read the surrounding code first and preserve existing runtime behavior.
- Skip a diagnostic only when the fix would require broad architectural changes, unclear behavior changes, or changes outside the selected batch scope. If skipped, mention it in the PR body.
- Finish each selected file. A file is finished when it has zero React Doctor diagnostics, or when every remaining diagnostic has an individual, specific reason to stay. A half-fixed file will be selected again later and cost a second pull request, a second review, and a second merge over the same code.
- Before considering a file done, re-run `bun run doctor -- file <path>` and read what is left. Leaving more than roughly a quarter of a file's diagnostics behind means you have not finished.
- A group of diagnostics sharing one root cause counts as one reason, and that root cause is usually worth fixing rather than deferring.
- Skip a diagnostic only when the fix would require unclear behavior changes, or a change so large it would stop the pull request from being reviewable. Difficulty alone is not a reason. If skipped, give the specific reason in the PR body under `## Non-goals`.
- Do not suppress React Doctor diagnostics unless there is a clear false positive.
- If a listed diagnostic requires changes outside the selected files, make only the minimal required supporting change. Do not expand the cleanup scope.
@@ -31,11 +44,15 @@ After edits, run:
`bun run doctor -- check-batch --run <run-id>`
Then run:
Then validate the packages you actually touched, not the whole workspace. For each affected package run its own checks, for example:
`bun run type-check`
`bun run --cwd packages/ui type-check`
`bun run lint`
`bun run --cwd packages/ui lint`
`bun run --cwd packages/ui test`
Workspace-wide `bun run type-check` and `bun run lint` are CI's job. Run them locally only when a change crosses package boundaries or touches shared contracts. For files that TypeScript does not cover, such as server or CLI JavaScript, run the focused tests for that surface instead.
Validation and delivery:
- Confirm selected files have fewer diagnostics than before.
@@ -45,15 +62,20 @@ Validation and delivery:
- Create exactly one PR with `gh pr create` using the exact printed `PR title`.
- After the PR is created, switch back to `main` and pull the latest remote changes again.
PR requirements:
PR requirements. The repository has a mandatory pull request template at `.github/PULL_REQUEST_TEMPLATE.md`, and `AGENTS.md` requires it to be completed with concrete evidence for the final PR HEAD. Read the template and `CONTRIBUTING.md` before writing the description. Use every template heading, in the template's order, and do not invent replacement headings. Fill each section as follows.
- Use the exact printed `PR title`.
- Include the `Run ID`, `Batch name`, and `Branch name`.
- Include selected files.
- Include diagnostics fixed according to `check-batch`.
- Include remaining diagnostics in selected files.
- Include validation results for `bun run type-check` and `bun run lint`.
- Include a `Manual testing recommendations` section with focused checks for the changed behavior. Base it on the selected files and actual edits, for example checking affected dropdowns, keyboard navigation, model/agent selection, settings controls, or mobile/desktop variants.
- Include any skipped diagnostics and why.
- `## Intent`: state that this is an unattended maintenance batch, name the `Run ID`, `Batch name`, and `Branch name`, and say what behavior changes. When nothing observable changes, say so explicitly rather than leaving it implied.
- `## Non-goals`: the diagnostics left unfixed in the selected files, diagnostics elsewhere in the repository, and any refactor you deliberately did not start. Give the reason for each, not just the count.
- `## Affected surfaces`: the packages, runtimes, user-visible states, and persisted or external contracts the diff reaches. Name every runtime the changed code runs in, and explain why an apparently applicable runtime is unaffected.
- `## Repository guidance`: fill the table. List the `AGENTS.md` rules you followed, every project skill that matched the change, required skill references you read, and the nearest `README.md` or `DOCUMENTATION.md` for the touched modules. For each row explain why it applies and how the change complies. Do not list filenames without explanation.
- `## Validation`: fill the table with the exact commands you ran and their results, including `check-batch` and every package-scoped type-check, lint, and test command, naming the packages. Record failures honestly, including pre-existing failures unrelated to this PR, and say which checks you did not run. Do not claim runtime behavior from type-check or lint alone.
- `## Visual evidence`: these PRs usually have no visible change, so explain concretely why the diff cannot affect rendered behavior. If anything user-visible did change, attach before/after evidence for the affected states.
- `## Risks and failure behavior`: cover what breaks if a change is wrong, how to roll it back, and any compatibility, data, performance, or cross-runtime concern. State "None identified" only with a concrete reason.
Add a `## Manual testing recommendations` section after the template sections, with focused checks for the changed behavior. Base it on the selected files and actual edits, for example checking affected dropdowns, keyboard navigation, model or agent selection, settings controls, and mobile or desktop variants.
Also state, inside `## Intent`, the selected files and how many diagnostics `check-batch` reports as fixed and remaining.
Constraints:
- Keep the PR small and reviewable.
@@ -61,4 +83,6 @@ Constraints:
- Do not modify unrelated files except minimal supporting changes required by selected-file fixes.
- Do not run broad formatting.
- Do not fix diagnostics outside the selected files.
- Leave `.tmp/react-doctor/runs/<run-id>/` intact after creating the PR. These files are the handoff for the review follow-up task.
- Do not edit `CHANGELOG.md`, package versions, or release metadata. This is internal maintenance with no user-facing change.
- Leave the batch's run directory intact after creating the PR. `next-batch` prints its location. That directory is both the handoff for the review follow-up task and the claim that stops another batch, including the anti-slop pipeline, from touching the same files. Deleting it early lets a parallel batch collide with this PR. Never delete it by hand; use `bun run doctor -- release --run <run-id>`.
- If you stop before creating a PR for any reason, release the claim with `bun run doctor -- release --run <run-id>` so the files return to the pool.
+26 -15
View File
@@ -7,16 +7,27 @@ You are working in the OpenChamber repository.
Goal: follow up on an existing React Doctor maintenance PR, address Greptile/review bot feedback, and clean up the local batch handoff files when done.
Inspect local React Doctor batch handoff files:
This task can run unattended on a schedule, so it must be safe to start at any moment and must stop cleanly when there is nothing to do.
`find .tmp/react-doctor/runs -maxdepth 2 -name batch.json -print 2>/dev/null || true`
First, verify the worktree is safe to use:
`git status --porcelain`
If the output is not empty, stop immediately and report that the worktree has uncommitted changes. Do not stash, reset, discard, or switch branches.
List the active batches:
`bun run doctor -- active`
The listing may include batches owned by the anti-slop pipeline; those are shown as `[pipeline as]`. Never touch them.
Workflow:
- Read the available `.tmp/react-doctor/runs/*/batch.json` files.
- Find the most recent batch that has `branchName`, `batchName`, and `prTitle`.
- Read its `Run ID`, `Batch name`, `Branch name`, `PR title`, and selected files.
- Use `gh` to find the open PR for that branch or title.
- If no open PR exists for the batch, stop and report that there is no PR to follow up.
- If there are no active batches, stop and report that there is nothing to follow up.
- Each active batch corresponds to one open PR. Read its `batch.json` for `runId`, `branchName`, `batchName`, `prTitle`, and selected files.
- Use `gh` to find the open PR for each batch branch.
- Work on the oldest batch that has an open PR with unaddressed feedback. If several qualify, handle exactly one and leave the rest.
- If a batch's PR was already merged or closed, do not treat it as follow-up work. Release its claim with `bun run doctor -- release --run <run-id>` so its files return to the pool, then continue looking.
- If no batch has an open PR with actionable feedback, stop and report that.
- Switch to the batch branch using the exact `branchName`.
- Pull or update the branch from remote if needed.
- Use `gh` to inspect PR review comments, PR issue comments, review threads if available, and check run summaries if relevant.
@@ -32,9 +43,7 @@ After fixes, run:
`bun run doctor -- check-batch --run <run-id>`
`bun run type-check`
`bun run lint`
Then re-run the package-scoped checks for the packages you touched, for example `bun run --cwd packages/ui type-check`, `bun run --cwd packages/ui lint`, and `bun run --cwd packages/ui test`. Workspace-wide checks are CI's job.
Delivery:
- Commit follow-up fixes with a concise message.
@@ -42,15 +51,17 @@ Delivery:
- Reply to addressed review comments using `gh`.
- For each specific review comment you addressed, reply with what was changed and the follow-up commit hash.
- If the feedback was a general PR comment, add one general PR comment summarizing what was addressed, commit hashes, and validation results.
- Update the PR description so it stays true for the final HEAD: refresh `## Validation` with the checks you re-ran, and move any new behavior change into `## Risks and failure behavior`. Keep every heading of `.github/PULL_REQUEST_TEMPLATE.md` intact, and preserve content the repository owner added by hand, including screenshots. Read the live description before editing and merge into it rather than overwriting.
- If a comment is intentionally not addressed, reply with a concise reason.
- After successful push and replies, delete only the completed batch handoff directory: `.tmp/react-doctor/runs/<run-id>/`.
- Do not release the batch while its PR is still open and awaiting review. The claim is what keeps parallel batches off these files.
- Release the batch only once its PR has been merged or closed: `bun run doctor -- release --run <run-id>`.
- After the follow-up is complete, switch back to `main` and pull the latest remote changes.
Constraints:
- Work on exactly one React Doctor batch PR.
- Prefer the most recent batch with an open PR.
- Prefer the oldest batch with an open PR.
- Do not auto-merge.
- Do not close the PR.
- Do not delete handoff files until comments are addressed, validation passes, and follow-up commits are pushed.
- Do not delete unrelated `.tmp/react-doctor/runs/*` directories.
- If validation fails and cannot be fixed safely within scope, do not delete the handoff directory.
- Do not edit `CHANGELOG.md`, package versions, or release metadata.
- Do not release or delete handoff directories for batches you did not handle.
- If validation fails and cannot be fixed safely within scope, leave the batch claimed and report the blocker.
+25 -3
View File
@@ -81,27 +81,49 @@ process violation.
| Trigger | Required skill |
|---|---|
| Any source, dependency, export, build-config, generated-asset, package-contract, or module-ownership change | `openchamber-change-discipline` |
| Source/dependency changes, exports or package contracts, build/generated assets, or module ownership | `openchamber-change-discipline` |
| CLI commands, prompts, terminal output, non-TTY, `--quiet`, or `--json` behavior | `clack-cli-patterns` |
| Shared UI data access, OpenCode SDK, `RuntimeAPIs`, runtime fetch/auth/URLs, bridges/proxies, runtime switching, or server API routes | `ui-api-decoupling` |
| Shared UI data access, OpenCode SDK or server routes, `RuntimeAPIs`, runtime auth/URLs, bridges, or runtime switching | `ui-api-decoupling` |
| Electron main/preload, IPC, native UI, updater, deep links, SSH/tunnels, packaging, or child processes | `desktop-shell` |
| Session sync, bootstrap/reconnect, reducers, polling, optimistic state, queues, live status, reconciliation, or directory-scoped caches | `sync-state-invariants` |
| Render/store/event hot paths, large lists, caching/indexing, high CPU/memory, lag, jank, freezes, or performance regressions | `performance-engineering` |
| Render/store/event hot paths, large lists, caches/indexes, or reported lag, freezes, CPU/memory, startup, or performance regressions | `performance-engineering` |
| WebSocket, SSE, streaming transport, runtime transport internals, or private relay | `relay-transport` |
| UI components, styling, colors, buttons, or icons | `theme-system` |
| User-facing or accessible UI text, labels, aria, toasts, dialogs, or navigation copy | `locale-ui-patterns` |
| Settings UI, settings dialogs, configuration surfaces, or settings search | `settings-ui-patterns` |
| Sortable or drag-to-reorder behavior, especially `@dnd-kit` and touch/wrapping layouts | `drag-to-reorder` |
| iOS Simulator build, launch, preview, gestures, or `serve-sim` control | `serve-sim` |
| Drafting or updating user-facing CHANGELOG entries for the `[Unreleased]` section (main app or VS Code extension) | `changelog-authoring` |
| Creating or editing skills, `AGENTS.md`, or docs reached through agent instructions/context pointers | `writing-for-agents` |
Pure code-reading or explanation does not require implementation skills unless needed to interpret a specialized subsystem.
### Skill Ownership
Keep each cross-cutting rule with one canonical owner; companion skills add only domain-specific consequences and a pointer to that owner.
| Concern | Canonical skill |
|---|---|
| Change scope, abstraction discipline, and validation risk | `openchamber-change-discipline` |
| State authority, reconciliation, optimistic state, and lifecycle correctness | `sync-state-invariants` |
| Measurement, hot-path cost, caching performance, and optimization evidence | `performance-engineering` |
| Shared UI API and runtime boundaries | `ui-api-decoupling` |
| WebSocket/SSE and private relay mechanics | `relay-transport` |
| Electron native ownership and privilege boundary | `desktop-shell` |
| UI tokens, primitives, icons, and animation styling | `theme-system` |
| Settings composition and search behavior | `settings-ui-patterns` |
| User-facing text and localization | `locale-ui-patterns` |
| Agent-facing document structure and context pointers | `writing-for-agents` |
Before adding guidance to a skill, identify its canonical owner. If another skill owns the rule, add a precise companion pointer and only the local consequence; do not copy the rule.
## Validation
- Use `package.json` scripts as the command source of truth.
- Prefer focused tests and package-scoped type-check/lint for executable source changes.
- Use workspace-wide checks for cross-workspace contracts, root tooling, dependencies, or shared generated assets.
- Run `bun run dead-code` when source files are added/deleted/renamed or exports, types, entrypoints, or import shape change; inspect its report because it is non-blocking.
- Run `bunx oxlint <changed-paths>` on TypeScript/JavaScript files you created or substantially rewrote. This runs the vendored `anti-slop` plugin, which rejects low-evidence typing: unjustified type assertions, `unknown`/`object`/`Record<string, unknown>` contracts, ad hoc `typeof` narrowing, and module mocking. Fix findings in code you authored. Pre-existing findings elsewhere are a known backlog: do not mass-fix them, and never silence a rule, weaken severity, or launder types to make the check pass.
- Do not assume TypeScript/lint covers server JS, CLI JS, Electron helpers, or native behavior; run focused tests, syntax checks, builds, or runtime validation for the touched surface.
- For docs-only or isolated config changes, run the narrowest relevant validation.
- Report exactly what was and was not validated. Static checks alone do not prove runtime, relay, performance, or platform correctness.
+43
View File
@@ -4,6 +4,49 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
- **Settings/Integrations:** a new Integrations settings page lists Claude Code, Command Code, and Cursor plugins with install, update, setup, and remove actions, plus Discord and Telegram Coming soon placeholders.
- Usage/Claude: Claude plan limits now work when you are signed in through Claude Code, without also signing into Anthropic in OpenCode; the account is read from Claude Code's own login on macOS, Linux, and WSL. The page shows your session and weekly limits again, adds per-model weekly limits and extra usage spending, and names your plan. Limits are kept on screen instead of disappearing when Anthropic temporarily blocks refreshes.
- Git: the pull request panel now follows the branch's current open PR, and an open PR always wins over an older merged or closed one. After a PR is merged or closed the panel keeps showing it as the branch's last PR and offers creating the next one right below it (thanks to @makeittech).
- Chat: new chats no longer start against a deleted last worktree directory; they fall back to the active project instead of saving the first message and never starting.
- Chat: opening a busy subagent in the context panel now shows its history instead of only the working-status line (thanks to @makeittech).
- Chat: saved chats in the context panel open again instead of staying blank.
- Projects: project names now match the folder name exactly, so `.ssh` and `opencode-claude` are no longer shown as `.Ssh` and `Opencode Claude` in the sidebar, window title, settings and notifications; names you renamed yourself are kept.
- Settings: the session retention action you pick is now saved instead of being dropped (thanks to @Gautam0507).
- Browser: typing a comment on a page no longer triggers app shortcuts.
- Skills Catalog: the source is now named ClawHub instead of "ClawdHub" (thanks to @makeittech).
## [1.18.4] - 2026-08-14
- **Chat:** new messages now remain at the end of the conversation instead of jumping before older messages after the message ID sequence rolls over; history loading, revert, and redo follow the same chronological order.
- **Stability:** a single internal error no longer shuts down the local server, which made the instance unreachable until it was restarted; the error is logged and the server keeps running.
- Mobile: connecting to a server that has authentication disabled now survives closing and reopening the app — auto-reconnect and the return-to-app check no longer treat the missing password token as a lost connection and kick back to the connect screen.
- Browser: restoring or opening a dev server preview while connected to an instance over a relay or other non-standard address no longer crashes the app; the preview reports the tunnel as unavailable instead.
## [1.18.3] - 2026-08-14
- **Browser panel:** the preview and browser panels are now one panel, backed by a real browser view on the desktop app. Pages that previously refused to load because they were being rewritten now open normally, logins persist, and developer tools are available. Point at an element or drag a region, write a comment, and it goes to chat with a screenshot of what you marked.
- **Agent browser control:** agents can now open a page and work with it — read what is on screen, click, type, scroll, look at how an element renders, switch between mobile, tablet and desktop layouts, and save a screenshot into the project — so they can check their own work instead of describing what they expect. It is a separate OpenChamber Web tool, turned on or off in the new Settings → General → OpenChamber Tools section.
- **Chat images:** completed assistant replies now collect Markdown images into a compact gallery with thumbnails and full-screen previews, including workspace-local images and a horizontally scrollable mobile layout (thanks to @ChangeHow).
- Sessions: switching projects now selects a session owned by the new project, and a message already being prepared stays with the session where it was submitted instead of being rerouted by a later project switch (thanks to @makeittech).
- Browser: dev servers are listed from what is actually listening, so one is offered no matter how it was started, and a server that is still starting is waited for instead of showing an error to retry by hand. The panel holds several pages at once, shows each page's own icon, suggests addresses already visited in this project, and adds a hard reload, page zoom, device sizes, a light/dark switch for the page, and clearing cookies or cached data for the panel alone.
- Browser: when OpenChamber runs on another machine, the desktop app opens its dev servers through a local port, so pages load with working hot reload and developer tools; links and redirects to another local port stay on that machine. In a web browser tab, only dev servers on your own machine can be opened.
- Remote access: pairing QR codes created while the app is open through a public domain (for example behind a reverse proxy) now include that domain as a connection address, so paired phones can reach the server over it instead of relying only on the local network address or the relay.
- Remote access: messages sent through the private relay no longer fail with a 400 error when request-body frames are lost during a connection drop; incomplete requests are retried instead (thanks to @claymor333).
- Mobile: a brief network hiccup when opening or returning to the app no longer bounces a working connection to the connect screen — the app retries in the background and reconnects on its own, while an unreachable server shows the connect screen within a few seconds.
- Mobile: long-pressing the logo on the connect screen (or the instances list) opens a connection log with a copy button, for reporting connection problems.
- Usage: quota limits enabled for display now refresh every three minutes on desktop, mobile, and VS Code, with a manual refresh action available at any time.
- Usage: OpenCode Go quota tracking now uses the existing OpenCode API key instead of requiring separate browser cookies and a workspace ID.
- Scheduled Tasks: when two OpenChamber servers use the same project configuration, a scheduled occurrence now runs only once instead of both servers starting duplicate sessions (thanks to @makeittech).
- Desktop/Windows/Linux: minimizing the window now always keeps it in the taskbar; the tray background setting, renamed "Close to the system tray", applies when you close the window.
- Performance: closed context panels no longer keep embedded chats running, and an open panel mounts only its active chat instead of every saved chat tab (thanks to @karimodm).
- Chat: opening subagent and code-review sessions in the context panel no longer steals focus from the main composer; subagent prompting is available immediately when enabled, and code-review sessions are no longer mistaken for read-only subagent sessions.
- Chat: typing `!` to enter shell mode no longer inserts the trigger into the command or moves the caret to the wrong side of it (thanks to @RyderAsKing).
- Chat: line numbers with three or more digits no longer wrap in code blocks (thanks to @ChangeHow).
- Work status: new-session drafts now show project, MCP, and usage details before a session exists, long subagent lists stay within the panel, and hiding every section leaves controls available to restore them (thanks to @alohaninja).
- Desktop/Linux: frameless main and Mini Chat windows now use native rounded corners (thanks to @kydorn).
## [1.18.2] - 2026-08-10
- **Observability panel:** a new panel near to the chat brings the active goal, tasks, subagents, pinned context, MCP servers, and context usage into one live view. The session list also shows how long an agent has been working.
- **Scheduled Tasks:** projects can now define recurring tasks as Markdown files in `.agents/loops`; opening the task list discovers file changes without a restart, and loop tasks can be edited, enabled, disabled, deleted, or run from the app (thanks to @makeittech).
- **Settings:** OpenCode configuration changes now accumulate behind a single Apply & Restart action instead of restarting OpenCode after every edit; the confirmation warns when active chats will be stopped (thanks to @makeittech).
+7
View File
@@ -113,9 +113,16 @@ The final AppImage verifier checks desktop identity and the architecture of Elec
```bash
bun run type-check # Must pass
bun run lint # Must pass
bun run test # Must pass
bun run build # Must succeed
```
`bun run test` runs every suite in the repository: shared UI, VS Code, Electron,
web/server, and the root scripts. The UI, VS Code, and Electron suites keep
module-level singletons, so `scripts/run-isolated-tests.mjs` gives each test file
its own process instead of letting load order decide the result. Run a single
file directly while iterating (`bun test <file>`).
For docs-only changes, validation may be enough:
```bash
+123 -197
View File
@@ -30,7 +30,7 @@
"@heroui/theme": "^2.4.23",
"@lezer/highlight": "^1.2.3",
"@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "1.18.15",
"@opencode-ai/sdk": "1.18.18",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -65,6 +65,7 @@
"devDependencies": {
"@clack/prompts": "^1.1.0",
"@eslint/js": "^9.33.0",
"@oxlint/plugins": "1.78.0",
"@remixicon/react": "^4.7.0",
"@tailwindcss/postcss": "^4.0.0",
"@types/dom-speech-recognition": "^0.0.12",
@@ -83,6 +84,7 @@
"globals": "^16.3.0",
"node-addon-api": "7.1.1",
"nodemon": "^3.1.7",
"oxlint": "1.78.0",
"patch-package": "^8.0.0",
"sharp": "^0.35.0",
"tailwindcss": "^4.0.0",
@@ -95,7 +97,7 @@
},
"packages/electron": {
"name": "@openchamber/electron",
"version": "1.18.1",
"version": "1.18.4",
"dependencies": {
"@openchamber/web": "workspace:*",
"electron-context-menu": "^4.1.2",
@@ -103,9 +105,10 @@
"electron-updater": "^6.8.3",
},
"devDependencies": {
"@electron/rebuild": "^3.7.0",
"electron": "^41.2.1",
"@electron/rebuild": "^4.2.0",
"electron": "^43.3.0",
"electron-builder": "^26.0.0",
"node-abi": "^4.33.0",
},
},
"packages/mobile": {
@@ -131,7 +134,7 @@
},
"packages/ui": {
"name": "@openchamber/ui",
"version": "1.18.1",
"version": "1.18.4",
"dependencies": {
"@aparajita/capacitor-secure-storage": "^8.0.0",
"@base-ui/react": "^1.4.0",
@@ -165,13 +168,12 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@lezer/highlight": "^1.2.3",
"@opencode-ai/sdk": "1.18.15",
"@opencode-ai/sdk": "1.18.18",
"@pierre/diffs": "1.3.0-beta.6",
"@replit/codemirror-vim": "^6.3.0",
"@simplewebauthn/browser": "13.3.0",
"@tanstack/react-virtual": "3.14.5",
"@xenova/transformers": "^2.17.2",
"@zumer/snapdom": "^2.12.0",
"beautiful-mermaid": "^1.1.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -236,10 +238,10 @@
},
"packages/vscode": {
"name": "openchamber",
"version": "1.18.1",
"version": "1.18.4",
"dependencies": {
"@openchamber/ui": "workspace:*",
"@opencode-ai/sdk": "1.18.15",
"@opencode-ai/sdk": "1.18.18",
"adm-zip": "^0.6.0",
"jsonc-parser": "^3.3.1",
"react": "^19.1.1",
@@ -259,14 +261,14 @@
},
"packages/web": {
"name": "@openchamber/web",
"version": "1.18.1",
"version": "1.18.4",
"bin": {
"openchamber": "./bin/cli.js",
},
"dependencies": {
"@clack/prompts": "^1.1.0",
"@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "1.18.15",
"@opencode-ai/sdk": "1.18.18",
"@simplewebauthn/server": "13.3.1",
"adm-zip": "^0.6.0",
"bun-pty": "^0.4.5",
@@ -673,19 +675,19 @@
"@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="],
"@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.5", "", {}, "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA=="],
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
"@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
"@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="],
"@electron/node-gyp": ["@electron/node-gyp@github:electron/node-gyp#06b29aa", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": "./bin/node-gyp.js" }, "electron-node-gyp-06b29aa", "sha512-UJwi6aXMAiUaOvqPHVlMtCOLRa1QAU2SqYD9H07KHpN+I2mBoFuxP1HnUOkt86+j+/o/XyHpM7D33JFFQi/jfA=="],
"@electron/get": ["@electron/get@5.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^3.0.0", "graceful-fs": "^4.2.11", "progress": "^2.0.3", "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "optionalDependencies": { "undici": "^7.24.4" } }, "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA=="],
"@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="],
"@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="],
"@electron/rebuild": ["@electron/rebuild@3.7.2", "", { "dependencies": { "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", "@malept/cross-spawn-promise": "^2.0.0", "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "fs-extra": "^10.0.0", "got": "^11.7.0", "node-abi": "^3.45.0", "node-api-version": "^0.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^6.0.5", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg=="],
"@electron/rebuild": ["@electron/rebuild@4.2.0", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^12.2.0", "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ=="],
"@electron/universal": ["@electron/universal@2.0.3", "", { "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="],
@@ -781,8 +783,6 @@
"@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="],
"@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
"@heroui/react-rsc-utils": ["@heroui/react-rsc-utils@2.1.9", "", { "peerDependencies": { "react": ">=18 || >=19.0.0-rc.0" } }, "sha512-e77OEjNCmQxE9/pnLDDb93qWkX58/CcgIqdNAczT/zUP+a48NxGq2A2WRimvc1uviwaNL2StriE2DmyZPyYW7Q=="],
"@heroui/react-utils": ["@heroui/react-utils@2.1.14", "", { "dependencies": { "@heroui/react-rsc-utils": "2.1.9", "@heroui/shared-utils": "2.1.12" }, "peerDependencies": { "react": ">=18 || >=19.0.0-rc.0" } }, "sha512-hhKklYKy9sRH52C9A8P0jWQ79W4MkIvOnKBIuxEMHhigjfracy0o0lMnAUdEsJni4oZKVJYqNGdQl+UVgcmeDA=="],
@@ -961,9 +961,7 @@
"@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q=="],
"@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="],
"@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="],
"@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="],
"@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
@@ -997,7 +995,47 @@
"@openchamber/web": ["@openchamber/web@workspace:packages/web"],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.15", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-8sfo9nGiVwesAZW9Wqkvynyn7w4wYaHx1O9qOHpYL65+Bs2XUpHP3kBbZe58gjQydFgk6I74kUF7sqCiPu2arQ=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.18", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-zJlwXskIR47V1dkPJqeKBgq7nejG1uU8lJaGIGqbX3MWRCT8vKn0fEotbxuPCKnTdmWsDyNGNg9q1qIliDSMDA=="],
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.78.0", "", { "os": "android", "cpu": "arm" }, "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw=="],
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.78.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ=="],
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.78.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA=="],
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.78.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g=="],
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.78.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg=="],
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.78.0", "", { "os": "linux", "cpu": "arm" }, "sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ=="],
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.78.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ=="],
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.78.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg=="],
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.78.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg=="],
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.78.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ=="],
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.78.0", "", { "os": "linux", "cpu": "none" }, "sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA=="],
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.78.0", "", { "os": "linux", "cpu": "none" }, "sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg=="],
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.78.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw=="],
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.78.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ=="],
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.78.0", "", { "os": "linux", "cpu": "x64" }, "sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg=="],
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.78.0", "", { "os": "none", "cpu": "arm64" }, "sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg=="],
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.78.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA=="],
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.78.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA=="],
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.78.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA=="],
"@oxlint/plugins": ["@oxlint/plugins@1.78.0", "", {}, "sha512-Ypt8KeRYw+4jUtlPirfcHWMrn5ms12VrrFPD+Mds477/7tJxG1Kcz2Yrg2nVcTQEUx/GdlhS+BUg1kmxNm04Ug=="],
"@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.1", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw=="],
@@ -1321,8 +1359,6 @@
"@textlint/types": ["@textlint/types@15.5.2", "", { "dependencies": { "@textlint/ast-node-types": "15.5.2" } }, "sha512-sJOrlVLLXp4/EZtiWKWq9y2fWyZlI8GP+24rnU5avtPWBIMm/1w97yzKrAqYF8czx2MqR391z5akhnfhj2f/AQ=="],
"@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="],
"@types/adm-zip": ["@types/adm-zip@0.5.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
@@ -1405,8 +1441,6 @@
"@types/vscode": ["@types/vscode@1.109.0", "", {}, "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw=="],
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="],
@@ -1477,7 +1511,7 @@
"@zumer/snapdom": ["@zumer/snapdom@2.12.8", "", {}, "sha512-dLX6ZMNjLveasn9yhcruOOfd8GBZBDp59F7iJoLlGf7BnGp0vfVsjxIZDIjN2UTZJN1KoJR/BXsxEnAyY7LuXA=="],
"abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="],
"abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
@@ -1493,8 +1527,6 @@
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
"ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="],
@@ -1625,7 +1657,7 @@
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="],
"cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="],
"cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="],
@@ -1663,7 +1695,7 @@
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="],
@@ -1671,8 +1703,6 @@
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
"cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
@@ -1851,7 +1881,7 @@
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
"electron": ["electron@41.2.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-teeRThiYGTPKf/2yOW7zZA1bhb91KEQ4yLBPOg7GxpmnkLFLugKgQaAKOrCgdzwsXh/5mFIfmkm+4+wACJKwaA=="],
"electron": ["electron@43.3.0", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-nLlvu0WFjftWsSaTkV2B/c4NDuJBspTyXu8vKSQ6vLvFt8uG3NgN49LLKcXddwX0GqVvAQDhciWp+4xOdTdhew=="],
"electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="],
@@ -1969,8 +1999,6 @@
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
"extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="],
"fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
@@ -2041,7 +2069,7 @@
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
"fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
"fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
@@ -2183,12 +2211,8 @@
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
"index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="],
"infer-owner": ["infer-owner@1.0.4", "", {}, "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
@@ -2251,8 +2275,6 @@
"is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
"is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="],
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
@@ -2439,7 +2461,7 @@
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="],
"make-fetch-happen": ["make-fetch-happen@14.0.3", "", { "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" } }, "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ=="],
"markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="],
@@ -2569,11 +2591,11 @@
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="],
"minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="],
"minipass-fetch": ["minipass-fetch@2.1.2", "", { "dependencies": { "minipass": "^3.1.6", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA=="],
"minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="],
"minipass-flush": ["minipass-flush@1.0.7", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA=="],
@@ -2581,9 +2603,9 @@
"minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="],
"minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
"mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
@@ -2611,7 +2633,7 @@
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
"node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="],
"node-abi": ["node-abi@4.33.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA=="],
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
@@ -2621,7 +2643,7 @@
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="],
"node-gyp": ["node-gyp@12.4.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw=="],
"node-pty": ["node-pty@1.2.0-beta.12", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-uExTCG/4VmSJa4+TjxFwPXv8BfacmfFEBL6JpxCMDghcwqzvD0yTcGmZ1fKOK6HY33tp0CelLblqTECJizc+Yw=="],
@@ -2631,7 +2653,7 @@
"nodemon": ["nodemon@3.1.14", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^10.2.1", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": { "nodemon": "bin/nodemon.js" } }, "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw=="],
"nopt": ["nopt@6.0.0", "", { "dependencies": { "abbrev": "^1.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g=="],
"nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="],
"normalize-package-data": ["normalize-package-data@6.0.2", "", { "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g=="],
@@ -2683,6 +2705,8 @@
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
"oxlint": ["oxlint@1.78.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.78.0", "@oxlint/binding-android-arm64": "1.78.0", "@oxlint/binding-darwin-arm64": "1.78.0", "@oxlint/binding-darwin-x64": "1.78.0", "@oxlint/binding-freebsd-x64": "1.78.0", "@oxlint/binding-linux-arm-gnueabihf": "1.78.0", "@oxlint/binding-linux-arm-musleabihf": "1.78.0", "@oxlint/binding-linux-arm64-gnu": "1.78.0", "@oxlint/binding-linux-arm64-musl": "1.78.0", "@oxlint/binding-linux-ppc64-gnu": "1.78.0", "@oxlint/binding-linux-riscv64-gnu": "1.78.0", "@oxlint/binding-linux-riscv64-musl": "1.78.0", "@oxlint/binding-linux-s390x-gnu": "1.78.0", "@oxlint/binding-linux-x64-gnu": "1.78.0", "@oxlint/binding-linux-x64-musl": "1.78.0", "@oxlint/binding-openharmony-arm64": "1.78.0", "@oxlint/binding-win32-arm64-msvc": "1.78.0", "@oxlint/binding-win32-ia32-msvc": "1.78.0", "@oxlint/binding-win32-x64-msvc": "1.78.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA=="],
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
@@ -2759,12 +2783,10 @@
"pretty-bytes": ["pretty-bytes@6.1.1", "", {}, "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="],
"proc-log": ["proc-log@2.0.1", "", {}, "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw=="],
"proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
"promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
@@ -3015,7 +3037,7 @@
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
"socks-proxy-agent": ["socks-proxy-agent@7.0.0", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww=="],
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
@@ -3045,7 +3067,7 @@
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="],
"ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
@@ -3115,7 +3137,7 @@
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="],
"tar-fs": ["tar-fs@3.1.2", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw=="],
@@ -3217,7 +3239,7 @@
"underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="],
"undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="],
"undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
@@ -3233,9 +3255,9 @@
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unique-filename": ["unique-filename@2.0.1", "", { "dependencies": { "unique-slug": "^3.0.0" } }, "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A=="],
"unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="],
"unique-slug": ["unique-slug@3.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w=="],
"unique-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="],
"unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="],
@@ -3387,7 +3409,7 @@
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
@@ -3429,19 +3451,17 @@
"@capacitor/cli/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@capacitor/cli/tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="],
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
"@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"@electron/get/env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
"@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@electron/get/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@electron/node-gyp/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
"@electron/get/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
@@ -3467,16 +3487,10 @@
"@ionic/utils-terminal/slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="],
"@isaacs/fs-minipass/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"@npmcli/agent/socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"@openchamber/web/cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="],
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
@@ -3553,24 +3567,18 @@
"app-builder-lib/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
"app-builder-lib/tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="],
"app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
"babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="],
"cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"cacache/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
"cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"cacache/p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
"cacache/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"cheerio/undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="],
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
@@ -3605,12 +3613,8 @@
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
"glob/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"globby/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="],
@@ -3625,13 +3629,11 @@
"keytar/node-addon-api": ["node-addon-api@4.3.0", "", {}, "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="],
"make-fetch-happen/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="],
"lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"make-fetch-happen/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"make-fetch-happen/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
"make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"make-fetch-happen/proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="],
"markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
@@ -3641,27 +3643,17 @@
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-fetch/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"node-abi/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"node-gyp/make-fetch-happen": ["make-fetch-happen@14.0.3", "", { "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" } }, "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ=="],
"node-gyp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"node-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
"node-gyp/proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="],
"node-gyp/tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="],
"node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
"node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
"node-sarif-builder/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="],
@@ -3683,10 +3675,10 @@
"path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
"path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
"prebuild-install/node-abi": ["node-abi@3.94.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g=="],
"prebuild-install/tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
@@ -3715,16 +3707,12 @@
"slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
"socks-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"sort-keys/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="],
"source-map/whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="],
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"superagent/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
"supports-hyperlinks/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -3733,8 +3721,6 @@
"table/slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="],
"temp/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
"temp/rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="],
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
@@ -3773,24 +3759,8 @@
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@capacitor/cli/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"@capacitor/cli/tar/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"@capacitor/cli/tar/minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
"@capacitor/cli/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
"@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"@electron/node-gyp/glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
"@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"@npmcli/move-file/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"@rollup/plugin-node-resolve/@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@secretlint/config-loader/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
@@ -3807,23 +3777,17 @@
"app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"app-builder-lib/@electron/rebuild/node-abi": ["node-abi@4.28.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g=="],
"app-builder-lib/@electron/rebuild/node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="],
"app-builder-lib/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
"app-builder-lib/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"app-builder-lib/tar/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"app-builder-lib/tar/minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
"app-builder-lib/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"app-builder-lib/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
"cacache/glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
"cacache/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"cacache/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"cacache/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
"cacache/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"cli-truncate/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
@@ -3843,33 +3807,15 @@
"iconv-corefoundation/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="],
"make-fetch-happen/http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"make-fetch-happen/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"micromark-extension-math/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"node-gyp/make-fetch-happen/cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="],
"minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"node-gyp/make-fetch-happen/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"node-gyp/make-fetch-happen/minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="],
"minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"node-gyp/make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"node-gyp/make-fetch-happen/ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="],
"node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
"node-gyp/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"node-gyp/tar/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"node-gyp/tar/minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
"node-gyp/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
"node-gyp/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
"nodemon/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
@@ -3879,6 +3825,8 @@
"openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"prebuild-install/node-abi/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"prebuild-install/tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
"prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
@@ -3895,8 +3843,6 @@
"rimraf/glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
"rimraf/glob/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"source-map/whatwg-url/tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="],
"source-map/whatwg-url/webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
@@ -4011,36 +3957,26 @@
"workbox-build/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@electron/node-gyp/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"app-builder-lib/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"app-builder-lib/@electron/rebuild/node-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
"app-builder-lib/@electron/rebuild/node-gyp/proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="],
"app-builder-lib/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"cacache/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"node-gyp/make-fetch-happen/cacache/@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="],
"node-gyp/make-fetch-happen/cacache/fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
"node-gyp/make-fetch-happen/cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"node-gyp/make-fetch-happen/cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"node-gyp/make-fetch-happen/cacache/minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="],
"node-gyp/make-fetch-happen/cacache/unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="],
"node-gyp/make-fetch-happen/minipass-fetch/minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
"nodemon/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
@@ -4049,34 +3985,24 @@
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
"node-gyp/make-fetch-happen/cacache/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"app-builder-lib/@electron/rebuild/node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
"node-gyp/make-fetch-happen/cacache/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
"cacache/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"node-gyp/make-fetch-happen/cacache/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"cacache/glob/jackspeak/@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"node-gyp/make-fetch-happen/cacache/unique-filename/unique-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="],
"cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"qrcode/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"cacache/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"cacache/glob/jackspeak/@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"qrcode/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
}
}
-948
View File
@@ -1,948 +0,0 @@
# Pairing v2 Trusted-Device Issuance Backend Plan
## Scope
Implement the Pairing v2 mechanism without UI.
Included:
- Backend pairing session runtime.
- Pairing create/redeem/cancel routes.
- Trusted-device token issuance through the existing remote client auth runtime.
- Backward-compatible remote client metadata extension.
- Password/passkey issuance metadata alignment.
- Shared v2 `openchamber://connect` payload helpers.
Not included:
- Settings page.
- QR modal.
- Pair Device button.
- Device list UI.
- Translations/copy.
- Relay implementation.
- LAN discovery.
- End-user polished mobile/desktop screens.
## Naming
Use the existing `client-auth` domain.
New module:
```text
packages/web/server/lib/client-auth/pairing.js
```
Existing durable token module remains:
```text
packages/web/server/lib/client-auth/remote-clients.js
```
Conceptual names:
```text
Remote client
Trusted-device client token
Pairing session
Pairing secret
Pairing redeem
```
Deep link stays:
```text
openchamber://connect
```
Versions:
```text
v=1 => legacy server + long-lived token import
v=2 => one-time pairing handshake
```
## New Files
### 1. `packages/web/server/lib/client-auth/pairing.js`
Create a new backend runtime module for short-lived pairing sessions.
Responsibilities:
```text
createPairingSession
getPairingSession
cancelPairingSession
redeemPairingSession
sweepExpiredSessions
```
Store file:
```text
OPENCHAMBER_DATA_DIR/client-pairing-sessions.json
```
Suggested store shape:
```json
{
"version": 1,
"sessions": [
{
"id": "pair_...",
"secretHash": "...",
"createdAt": "...",
"expiresAt": "...",
"usedAt": null,
"cancelledAt": null,
"clientId": null,
"label": "Pair new device",
"fingerprint": "ABCD-1234",
"allowedClientKinds": ["mobile", "desktop"],
"createdByClientId": null
}
]
}
```
Security requirements:
```text
Persist only secretHash.
Return plaintext secret only from createPairingSession.
Redeem is one-time.
Redeem is expiry-aware.
Redeem is cancellation-aware.
Redeem must be mutation-serialized to avoid double issuance.
No raw token/secret logging.
```
Public methods should accept injected dependencies, following `remote-clients.js` style:
```js
createClientPairingRuntime({
fsPromises,
path,
crypto,
storePath,
remoteClientAuthRuntime,
})
```
## Existing Files To Update
### 2. `packages/web/server/lib/client-auth/remote-clients.js`
Extend trusted-device metadata backward-compatibly.
Current `createClient` input:
```js
{
label,
expiresAt,
clientKind,
dedupeKey,
}
```
Extend to:
```js
{
label,
expiresAt,
clientKind,
dedupeKey,
authMethod,
pairingId,
deviceName,
devicePlatform,
deviceModel,
appVersion,
}
```
Add normalized public fields:
```text
authMethod
pairingId
deviceName
devicePlatform
deviceModel
appVersion
```
Backward compatibility rules:
```text
Existing remote-clients.json remains valid.
Missing new fields normalize to null.
Existing tokens continue authenticating.
Public client output never exposes tokenHash.
Raw token is returned only from createClient.
```
Recommended `authMethod` values:
```text
pairing
password
passkey
desktop-local
manual
legacy
```
Do not force migration for old records. Treat missing `authMethod` as legacy/null.
### 3. `packages/web/server/index.js`
Instantiate the new pairing runtime next to `remoteClientAuthRuntime`.
Existing:
```js
const remoteClientAuthRuntime = createRemoteClientAuthRuntime({
fsPromises,
path,
crypto,
storePath: REMOTE_CLIENTS_FILE_PATH,
});
```
Add:
```js
const CLIENT_PAIRING_SESSIONS_FILE_PATH = path.join(
OPENCHAMBER_DATA_DIR,
'client-pairing-sessions.json',
);
```
Then:
```js
const clientPairingRuntime = createClientPairingRuntime({
fsPromises,
path,
crypto,
storePath: CLIENT_PAIRING_SESSIONS_FILE_PATH,
remoteClientAuthRuntime,
});
```
Pass `clientPairingRuntime` into `registerAuthAndAccessRoutes` dependencies.
### 4. `packages/web/server/lib/opencode/core-routes.js`
Add pairing routes near existing client-auth routes:
```text
/api/client-auth/clients
```
Add:
```http
POST /api/client-auth/pairing/sessions
DELETE /api/client-auth/pairing/sessions/:id
POST /api/client-auth/pairing/redeem
```
Optional, can be deferred:
```http
GET /api/client-auth/pairing/sessions/:id
```
Since UI polling is out of scope, `GET` is not required for this phase.
#### Route: `POST /api/client-auth/pairing/sessions`
Purpose:
```text
Create one short-lived pairing session and return data needed to build QR/deep link.
```
Auth:
```text
Require UI session auth.
Allow desktop-local client only if consistent with existing client-create exception.
Reject arbitrary remote client tokens.
Reject url-token auth.
```
Request:
```json
{
"label": "Pair new device",
"allowedClientKinds": ["mobile", "desktop"]
}
```
Response:
```json
{
"pairing": {
"id": "pair_...",
"secret": "one_time_secret",
"expiresAt": "...",
"fingerprint": "ABCD-1234",
"label": "Pair new device"
},
"server": {
"label": "OpenChamber",
"candidates": [
{
"type": "lan",
"url": "http://192.168.1.20:4096",
"priority": 10
},
{
"type": "tunnel",
"url": "https://abc.ngrok.app",
"priority": 20
}
]
}
}
```
Headers:
```http
Cache-Control: no-store
```
Note:
```text
This route does not render QR.
UI can later encode the returned data into openchamber://connect?v=2&p=...
```
#### Route: `DELETE /api/client-auth/pairing/sessions/:id`
Purpose:
```text
Cancel an unused pairing session.
```
Auth:
```text
Require owner/session auth.
```
Behavior:
```text
Set cancelledAt.
Do not delete immediately.
If already used, cancellation should not revoke the issued client.
```
Response:
```json
{
"cancelled": true
}
```
#### Route: `POST /api/client-auth/pairing/redeem`
Purpose:
```text
Exchange pairingId + one-time secret for a trusted-device client token.
```
Auth:
```text
No existing auth required.
The one-time pairing secret is the authentication factor.
```
Request:
```json
{
"pairingId": "pair_...",
"secret": "one_time_secret",
"clientLabel": "Iryna iPhone",
"clientKind": "mobile",
"deviceName": "Iryna iPhone",
"devicePlatform": "ios",
"deviceModel": "iPhone",
"appVersion": "1.12.0",
"dedupeKey": "optional-stable-device-key"
}
```
Server behavior:
```text
Validate pairing exists.
Validate secret using constant-time comparison.
Validate not expired.
Validate not cancelled.
Validate not used.
Validate clientKind is allowed.
Mark pairing used.
Create remote client through remoteClientAuthRuntime.createClient.
Return clientToken once.
```
Create client with:
```js
{
label: clientLabel || deviceName || 'Remote client',
clientKind,
dedupeKey,
authMethod: 'pairing',
pairingId,
deviceName,
devicePlatform,
deviceModel,
appVersion,
}
```
Response:
```json
{
"ok": true,
"server": {
"label": "OpenChamber",
"url": "https://selected-or-current-url",
"fingerprint": "ABCD-1234"
},
"client": {
"id": "device_...",
"label": "Iryna iPhone",
"clientKind": "mobile",
"authMethod": "pairing",
"createdAt": "..."
},
"clientToken": "oc_client_..."
}
```
Headers:
```http
Cache-Control: no-store
```
Failure response should be generic:
```json
{
"error": "Invalid or expired pairing session"
}
```
Do not reveal whether id, secret, expiry, used, or cancellation caused failure.
### 5. `packages/web/server/lib/ui-auth/ui-auth.js`
Preserve existing password/passkey behavior.
Only add metadata to client token issuance when `issueClientToken === true`.
Password issuance should pass:
```js
authMethod: 'password'
clientKind: req.body?.clientKind
dedupeKey: req.body?.dedupeKey
deviceName: req.body?.deviceName
devicePlatform: req.body?.devicePlatform
deviceModel: req.body?.deviceModel
appVersion: req.body?.appVersion
```
Passkey issuance should pass:
```js
authMethod: 'passkey'
clientKind: req.body?.clientKind
dedupeKey: req.body?.dedupeKey
deviceName: req.body?.deviceName
devicePlatform: req.body?.devicePlatform
deviceModel: req.body?.deviceModel
appVersion: req.body?.appVersion
```
Backward compatibility:
```text
Existing POST /auth/session payload still works.
Existing response shape still works.
Existing clientToken issuance still works.
Password login remains disabled for tunnel/public scope.
```
### 6. `packages/ui/src/lib/connectionPayload.ts`
Extend existing connect payload helpers.
Keep current v1 behavior:
```text
openchamber://connect?v=1&server=...&token=...&label=...
```
Add v2 payload types and helpers.
Suggested types:
```ts
export type ClientConnectionPayloadV1 = {
v: 1;
serverUrl: string;
token: string;
label?: string;
};
export type PairingEndpointCandidate = {
type: 'lan' | 'tunnel' | 'relay';
url: string;
priority?: number;
};
export type PairingConnectionPayloadV2 = {
v: 2;
pairingId: string;
secret: string;
label?: string;
fingerprint?: string;
expiresAt?: string;
candidates: PairingEndpointCandidate[];
};
```
Suggested helpers:
```ts
encodePairingConnectionPayload(payload: PairingConnectionPayloadV2): string
parsePairingConnectionPayload(value: string): PairingConnectionPayloadV2 | null
```
Use deep link format:
```text
openchamber://connect?v=2&p=<base64url-json>
```
Validation:
```text
Require v=2.
Require pairingId.
Require secret.
Require at least one valid http/https candidate.
Reject malformed URL.
Reject oversized payload.
Reject expired payload locally if expiresAt is clearly in the past.
```
Do not break current exports used by mobile QR/manual connect.
### 7. `packages/ui/src/apps/mobileQrScan.ts`
Update parser only.
Current scan parser recognizes legacy fields like:
```text
server
label
```
Add support for v2 connect links.
Output should be able to distinguish:
```text
legacy v1 token import
pairing v2 payload
plain URL
```
Do not implement full mobile UI flow in this scope unless there is already a non-UI callable path.
### 8. `packages/ui/src/apps/mobileConnections.ts`
Add non-visual callable mechanism for redeeming pairing payload.
Add a function conceptually like:
```ts
redeemPairingConnection(payload: PairingConnectionPayloadV2): Promise<void>
```
Responsibilities:
```text
Try endpoint candidates.
POST /api/client-auth/pairing/redeem.
Persist issued token securely.
Persist connection metadata.
Switch runtime only after token write succeeds.
```
No new screens/buttons.
Existing password flow remains unchanged.
Candidate selection:
```text
Normalize candidates.
Probe /health with timeout.
Try candidates by priority.
Prefer HTTPS when priority ties.
If network failure, try next candidate.
If server says invalid/expired/used, stop.
```
Mobile native should reuse existing native HTTP fallback path for LAN HTTP.
### 9. `packages/electron/main.mjs`
Extend existing connect deep-link handling.
Current v1 behavior:
```text
openchamber://connect?v=1&server=...&token=...
```
Keep it.
Add v2 branch:
```text
openchamber://connect?v=2&p=...
```
Behavior:
```text
Parse v2 payload.
Show confirmation before redeem/write/switch.
Probe candidates.
Redeem pairing secret.
Store returned clientToken in desktop hosts config.
Ask/switch according to existing remote host behavior.
Never show token.
Never write config before confirmation.
```
If this phase is strictly backend-only, this file can be deferred. But if desktop app as client must be functionally supported by deep link in this phase, include this change.
### 10. `packages/electron/preload.mjs`
No change expected unless a renderer-side desktop API is needed for pairing redeem.
Prefer keeping pairing redeem in main process only for deep-link handling if desktop v2 is implemented there.
### 11. `packages/web/server/lib/ui-auth/DOCUMENTATION.md`
Update module documentation to reflect the unified issuance model:
```text
Password, passkey, and pairing are issuance methods.
Trusted-device client token is the durable credential.
Pairing v2 uses one-time secrets and issues remote client tokens.
```
Optionally add:
```text
packages/web/server/lib/client-auth/DOCUMENTATION.md
```
if the client-auth module needs ownership docs.
## Route Registration Summary
Add to `registerAuthAndAccessRoutes`:
```http
POST /api/client-auth/pairing/sessions
DELETE /api/client-auth/pairing/sessions/:id
POST /api/client-auth/pairing/redeem
```
Optional later:
```http
GET /api/client-auth/pairing/sessions/:id
```
Route placement:
```text
Register before generic OpenCode proxy.
Place near existing /api/client-auth/clients routes.
```
## Execution Sequence
### Step 1: Extend Remote Client Metadata
Files:
```text
packages/web/server/lib/client-auth/remote-clients.js
```
Do:
```text
Add metadata normalization.
Extend createClient input.
Extend publicClient output.
Keep old records valid.
Do not change token generation/authentication behavior.
```
### Step 2: Add Password/Passkey Metadata Issuance
Files:
```text
packages/web/server/lib/ui-auth/ui-auth.js
```
Do:
```text
When issueClientToken is true, pass authMethod='password' from password login.
When issueClientToken is true, pass authMethod='passkey' from passkey auth.
Pass optional device metadata through.
Preserve response shape.
```
### Step 3: Create Pairing Runtime Module
Files:
```text
packages/web/server/lib/client-auth/pairing.js
```
Do:
```text
Implement session creation.
Implement hashed secret storage.
Implement cancel.
Implement redeem.
Implement expiry/used/cancelled checks.
Integrate remoteClientAuthRuntime.createClient in redeem.
```
### Step 4: Instantiate Pairing Runtime
Files:
```text
packages/web/server/index.js
```
Do:
```text
Define CLIENT_PAIRING_SESSIONS_FILE_PATH.
Instantiate createClientPairingRuntime.
Pass clientPairingRuntime to registerAuthAndAccessRoutes.
```
### Step 5: Add Pairing Routes
Files:
```text
packages/web/server/lib/opencode/core-routes.js
```
Do:
```text
Destructure clientPairingRuntime from dependencies.
Add POST /api/client-auth/pairing/sessions.
Add DELETE /api/client-auth/pairing/sessions/:id.
Add POST /api/client-auth/pairing/redeem.
Use correct auth gates.
Set Cache-Control: no-store where secrets/tokens are returned.
Keep error responses generic for redeem.
```
### Step 6: Add v2 Payload Helpers
Files:
```text
packages/ui/src/lib/connectionPayload.ts
```
Do:
```text
Keep v1 helpers unchanged.
Add v2 payload type.
Add encode v2 helper.
Add parse v2 helper.
Use openchamber://connect?v=2&p=<base64url-json>.
Validate candidates.
Reject malformed/expired/oversized payloads.
```
### Step 7: Update QR Scan Parser Shape
Files:
```text
packages/ui/src/apps/mobileQrScan.ts
```
Do:
```text
Recognize v2 connect payload.
Return structured v2 result.
Do not add new UI.
Do not break v1/manual URL behavior.
```
### Step 8: Add Non-UI Mobile Redeem Plumbing
Files:
```text
packages/ui/src/apps/mobileConnections.ts
```
Do:
```text
Add callable redeem pairing function.
Try endpoint candidates.
Redeem via /api/client-auth/pairing/redeem.
Persist token before runtime switch.
Reuse existing storage model.
Keep password/manual connect unchanged.
```
### Step 9: Add Desktop Deep-Link v2 Handling If In Scope
Files:
```text
packages/electron/main.mjs
```
Do:
```text
Extend connect deep-link parser to recognize v2.
Confirm before redeem.
Redeem against candidate endpoint.
Store remote host config with returned token.
Switch only after confirmation and successful storage.
Keep v1 behavior unchanged.
```
If desktop client deep-link support is deferred, skip this step and document that v2 backend/shared payload exists but desktop consumer is not wired yet.
### Step 10: Update Documentation
Files:
```text
packages/web/server/lib/ui-auth/DOCUMENTATION.md
```
Optionally add:
```text
packages/web/server/lib/client-auth/DOCUMENTATION.md
```
Document:
```text
Unified trusted-device token issuance.
Pairing v2 flow.
Password/passkey/pairing authMethod values.
Security rules.
Backward compatibility guarantees.
```
## Important Non-Goals
Do not implement:
```text
Settings page
Pair Device button
QR modal
Device list UI
Translations
Visual design
Relay transport
LAN discovery
Account/cloud sync
Token migration to OS keychain on desktop
```
## Backward Compatibility Requirements
Must remain true:
```text
Existing v1 openchamber://connect links keep working.
Existing password login with issueClientToken keeps working.
Existing passkey issueClientToken keeps working.
Existing remote-clients.json keeps loading.
Existing client tokens keep authenticating.
Existing mobile saved connections keep working.
Existing desktop remote hosts keep working.
```
## Security Requirements
Must hold:
```text
No long-lived token in v2 link.
Pairing secret persisted only as hash.
Pairing secret returned only once.
Client token returned only once.
Token hash persisted server-side.
Redeem is one-time.
Redeem is expiry-aware.
Redeem is cancellation-aware.
Redeem errors are generic.
Password login remains disabled for tunnel/public scope.
Pairing session creation requires owner/session auth.
Pairing redeem requires no prior auth but requires valid one-time secret.
Desktop v2 connect confirms before writing host config or switching runtime.
```
+47
View File
@@ -0,0 +1,47 @@
import { defineConfig } from "oxlint";
// Oxlint here runs only the vendored anti-slop plugin; ESLint remains the
// general-purpose linter for this repository.
export default defineConfig({
categories: {
correctness: "off",
},
ignorePatterns: [
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/out/**",
"**/.next/**",
"**/ios/**",
"**/android/**",
".agents/**",
".claude/**",
".conductor/**",
".opencode/**",
".openchamber/**",
".tmp/**",
"patches/**",
"bun-patches/**",
"tools/oxlint/anti-slop/**",
],
jsPlugins: [
{ name: "anti-slop", specifier: "./tools/oxlint/anti-slop/index.ts" },
],
rules: {
"anti-slop/no-chained-type-assertions": "error",
"anti-slop/no-conditional-empty-object-spread": "error",
"anti-slop/no-known-value-widening": "error",
"anti-slop/no-module-mocking": "error",
"anti-slop/no-object-parameters": "error",
"anti-slop/no-reflect-apply": "error",
"anti-slop/no-reflect-get": "error",
"anti-slop/no-runtime-typeof": "error",
"anti-slop/no-shape-in-symbol-names": "error",
"anti-slop/no-unknown-parameters": "error",
"anti-slop/no-unknown-returns": "error",
"anti-slop/no-unknown-type-aliases": "error",
"anti-slop/no-unsafe-dictionary-type": "error",
"anti-slop/no-widen-then-assert": "error",
"anti-slop/require-safety-comment-for-type-assertion": "error",
},
});
+8 -3
View File
@@ -1,6 +1,6 @@
{
"name": "openchamber-monorepo",
"version": "1.18.1",
"version": "1.18.4",
"description": "OpenChamber monorepo workspace for web, ui, and desktop runtimes",
"private": true,
"type": "module",
@@ -38,6 +38,8 @@
"lint:ui": "bun run --cwd packages/ui lint",
"lint:electron": "bun run --cwd packages/electron lint",
"lint:mobile": "bun run --cwd packages/mobile lint",
"lint:anti-slop": "oxlint",
"test": "node scripts/run-isolated-tests.mjs scripts && bun run --cwd packages/ui test && bun run --cwd packages/vscode test && bun run --cwd packages/electron test && bun run --cwd packages/web test",
"clean": "bun run --filter '*' clean",
"changelog-card": "node scripts/changelog-card/generate.mjs",
"postinstall": "node ./fix-deprecation.js && patch-package && node ./packages/electron/scripts/ensure-electron.mjs --best-effort",
@@ -73,6 +75,7 @@
"docs:validate": "node scripts/docs/validate-docs.mjs",
"dead-code": "bunx knip@5.80.0 --no-exit-code --include files,exports,nsExports,types,nsTypes,enumMembers,duplicates",
"doctor": "node scripts/react-doctor.mjs",
"deslop": "node scripts/anti-slop.mjs",
"profile:browser": "node scripts/profile-browser.mjs",
"icons:sprite": "node scripts/generate-file-type-sprite.mjs",
"icons:generate": "bun run scripts/generate-icon-sprite.mjs",
@@ -112,7 +115,7 @@
"@heroui/theme": "^2.4.23",
"@lezer/highlight": "^1.2.3",
"@octokit/rest": "^22.0.1",
"@opencode-ai/sdk": "1.18.15",
"@opencode-ai/sdk": "1.18.18",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -151,6 +154,8 @@
"devDependencies": {
"@clack/prompts": "^1.1.0",
"@eslint/js": "^9.33.0",
"@oxlint/plugins": "1.78.0",
"@remixicon/react": "^4.7.0",
"@tailwindcss/postcss": "^4.0.0",
"@types/dom-speech-recognition": "^0.0.12",
"@types/node": "^24.3.1",
@@ -168,8 +173,8 @@
"globals": "^16.3.0",
"node-addon-api": "7.1.1",
"nodemon": "^3.1.7",
"oxlint": "1.78.0",
"patch-package": "^8.0.0",
"@remixicon/react": "^4.7.0",
"sharp": "^0.35.0",
"tailwindcss": "^4.0.0",
"tsx": "^4.20.6",
@@ -28,7 +28,7 @@ The tool can list projects and model preferences, create and follow up on sessio
## Turn the tool on or off
Open **Settings → General → OpenCode CLI**, change **Agent control tool**, then select **Save + Reload**. The setting applies after the managed OpenCode server restarts.
Open **Settings → General → OpenChamber Tools** and change **Agent control tool**. The setting applies once the managed OpenCode server restarts, which OpenChamber offers as **Apply & Restart**.
The tool is not available when OpenChamber connects to an external OpenCode server through `OPENCODE_HOST` or skip-start, or inside the VS Code extension. Desktop and web installations that use OpenChamber's managed OpenCode server support it automatically.
@@ -37,3 +37,4 @@ The tool is not available when OpenChamber connects to an external OpenCode serv
- [Scheduled Tasks](/scheduled-tasks/)
- [Worktree Sessions](/worktrees/)
- [Session Goals](/session-goals/)
- [Browser Panel](/desktop-browser/) — the OpenChamber Web tool, for looking at and driving a page
@@ -28,7 +28,7 @@ Das Werkzeug kann Projekte und Modelleinstellungen auflisten, Sitzungen erstelle
## Werkzeug ein- oder ausschalten
Öffne **Einstellungen → Allgemein → OpenCode CLI**, ändere **Agent control tool** und wähle dann **Save + Reload**. Die Einstellung gilt, nachdem der verwaltete OpenCode-Server neu gestartet wurde.
Öffne **Einstellungen → Allgemein → OpenChamber-Werkzeuge** und ändere **Agent control tool**. Die Einstellung gilt, sobald der verwaltete OpenCode-Server neu startet — OpenChamber bietet das als **Apply & Restart** an.
Das Werkzeug ist nicht verfügbar, wenn OpenChamber über `OPENCODE_HOST` oder skip-start mit einem externen OpenCode-Server verbunden ist oder innerhalb der VS-Code-Erweiterung läuft. Desktop- und Web-Installationen, die den verwalteten OpenCode-Server von OpenChamber verwenden, unterstützen es automatisch.
@@ -37,3 +37,4 @@ Das Werkzeug ist nicht verfügbar, wenn OpenChamber über `OPENCODE_HOST` oder s
- [Geplante Aufgaben](/scheduled-tasks/)
- [Worktree-Sitzungen](/worktrees/)
- [Sitzungsziele](/session-goals/)
- [Browser-Panel](/desktop-browser/) — das OpenChamber-Web-Werkzeug, um eine Seite anzusehen und zu bedienen
@@ -1,22 +1,53 @@
---
title: Desktop-Browser
description: Durchsuche jede Seite in der Desktop-App mit Inspektion und Konsolenaufzeichnung.
title: Browser-Panel
description: Öffne jede Seite in der App, annotiere sie und lass den Agenten sie bedienen.
---
# Desktop-Browser
# Browser-Panel
Die Desktop-App hat einen eingebauten Browser, damit du jede Seite direkt neben deinem Chat öffnen, auf Elemente zeigen und danach fragen sowie die Konsole der Seite aufzeichnen kannst. Öffne ihn über die Globus-Schaltfläche im App-Kopfbereich.
Das Browser-Panel öffnet jede Seite direkt neben deinem Chat. Öffne es über die Globus-Schaltfläche in der Kopfzeile.
> Der Desktop-Browser ist eine Funktion **nur für den Desktop**. Im Web bietet das [Preview](/preview/)-Panel dieselben Inspektions- und Konsolentools für deinen lokalen Dev-Server.
In der Desktop-App ist es ein echter Browser: Deine Logins bleiben erhalten, Hot Reload funktioniert, und die Entwicklerwerkzeuge sind einen Klick entfernt. In einem Browser-Tab zeigt das Panel eine Seite zwar an, kann aber nicht in sie hineinsehen — die Annotationswerkzeuge unten gibt es nur auf dem Desktop.
## Inspizieren und annotieren
Seiten, die hier geöffnet werden, bekommen keinen Zugriff auf Kamera, Mikrofon oder Standort: solche Anfragen werden abgelehnt.
Aktiviere **inspect** und klicke auf ein beliebiges Element auf der Seite. OpenChamber erstellt dazu eine Notiz — was es ist, welche Stile es hat, wo es sich befindet und einen Screenshot — und hängt sie an deine Chatnachricht an. Das ist der schnellste Weg, dem Agenten zu sagen: „dieses Element, genau hier“.
## Die Werkzeugleiste
## Konsolenaufzeichnung
Die Adressleiste merkt sich Seiten, die du in diesem Projekt geöffnet hast, und schlägt sie beim Tippen vor — passend zu einem Teil der Adresse oder des Seitentitels. Mit den Pfeiltasten gehst du durch die Liste, Enter öffnet den markierten Eintrag, und die Schaltfläche in einer Zeile entfernt ihn.
Der Browser sammelt die Konsolenausgabe der Seite — Fehler, Warnungen und Logs — damit du sie filtern und lesen kannst, ohne die Entwicklertools zu öffnen.
Daneben liegt **Neu laden**, dazu ein **hartes Neuladen**, das den Cache übergeht, wenn eine Änderung partout nicht erscheint, sowie eine Zoomsteuerung, die nur die Seite skaliert.
**Cookies löschen** und **Zwischengespeicherte Daten löschen** gelten allein für dieses Panel. Deine OpenChamber-Sitzung und andere Fenster bleiben unberührt.
## Eine Seite annotieren
Drücke **Annotieren**, und über der Seite erscheint eine Leiste mit drei Werkzeugen:
- **Element** — klicke ein Element an. Ein Klick auf ein anderes verschiebt die Auswahl, ein erneuter Klick auf dasselbe hebt sie auf.
- **Bereich** — ziehe einen Rahmen um einen Ausschnitt, wenn es um mehr als ein Element geht.
- **Zeichnen** — skizziere frei über die Seite.
Schreib dein Anliegen in das Feld neben deiner Markierung und drücke **Anhängen** — oder einfach Enter. Deine Chat-Nachricht bekommt eine Karte mit allem Markierten, deiner Notiz und einem Screenshot der sichtbaren Seite mit deinen Markierungen darauf — du kannst also „dieser Button, etwas runder" sagen, statt zu beschreiben, wo er steht.
Die Seite selbst wird nie verändert — Annotieren markiert nur, was da ist. `Esc` bricht ab und schließt die Leiste.
## Den Agenten steuern lassen
Der Agent kann das Browser-Panel selbst benutzen — eine Seite öffnen, lesen, was darauf steht, klicken, tippen, scrollen und zwischen mobiler, Tablet- und Desktop-Ansicht wechseln — um seine eigene Arbeit zu prüfen, statt dich darum zu bitten. Du siehst es im Panel passieren.
Beliebigen Code kann der Agent auf der Seite nicht ausführen. Der Browser behält deine echten Logins, deshalb bleibt er auf die genannten Aktionen beschränkt.
Er kann außerdem ein Bild dessen, was er sieht, in `.openchamber/screenshots/` in deinem Projekt speichern und es dir in seiner Antwort zeigen. Genau das macht ein Vorher-Nachher möglich, und die Datei bleibt danach liegen, um sie an einen Pull Request zu hängen.
Die Browser-Aktionen sind das **OpenChamber-Web-Werkzeug**, das sich unter **Einstellungen → Allgemein → OpenChamber-Werkzeuge** einzeln ein- und ausschalten lässt.
Dafür braucht es die Desktop-App: eine Seite in einem Browser-Tab lässt sich nicht steuern.
## Entwicklerwerkzeuge
Drücke die Terminal-Schaltfläche in der Leiste, um Chromiums eigene Entwicklerwerkzeuge für die Seite zu öffnen — Konsole, Netzwerk, Elemente, alles Gewohnte.
## Verwandt
- [Preview & Dev Servers](/preview/) — dieselben Tools für deinen lokalen Dev-Server
- [Vorschau & Dev-Server](/preview/) — deine laufende App öffnen, auch auf einem entfernten Rechner
- [Agenten-Steuerungswerkzeug](/agent-control-tool/) — Sitzungen, Worktrees und geplante Aufgaben aus dem Chat
+20 -17
View File
@@ -1,32 +1,35 @@
---
title: Vorschau & Entwicklungsserver
description: Öffne einen laufenden Entwicklungsserver direkt in OpenChamber.
title: Vorschau & Dev-Server
description: Öffne einen laufenden Dev-Server direkt in OpenChamber.
---
# Vorschau & Entwicklungsserver
# Vorschau & Dev-Server
Wenn du einen Entwicklungsserver startest, kann OpenChamber ihn direkt in der App öffnen statt in einem separaten Browser-Tab — so kannst du deine Seite neben dem Chat sehen, ihre Konsole aufzeichnen und Elemente anstupsen, um Fragen dazu zu stellen.
Wenn du einen Dev-Server startest, kann OpenChamber ihn direkt in der App öffnen statt in einem separaten Browser-Tab — so siehst du deine Seite neben dem Chat und kannst auf Elemente zeigen, um danach zu fragen.
## Eine Vorschau öffnen
## Einen Dev-Server öffnen
OpenChamber überwacht die Terminalausgabe auf eine lokale Adresse (die `Local:`-Zeile, die Werkzeuge wie Vite, Next.js oder Astro ausgeben). Sobald es eine findet:
Öffne das Browser-Panel über die Globus-Schaltfläche in der Kopfzeile. Läuft bereits ein Dev-Server, steht er dort in der Liste und ein Klick öffnet ihn — OpenChamber findet ihn daran, was auf deinem Rechner tatsächlich lauscht, also unabhängig davon, wie du ihn gestartet hast.
- erscheint im Terminal eine Schaltfläche **Open preview**
- öffnet eine [Projektaktion](/project-actions/) mit aktiviertem Auto-Open die Vorschau für dich
- kann auch ein lokaler Link in einer Chatnachricht sie öffnen
Ein Dev-Server öffnet sich außerdem automatisch, wenn:
Die Seite lädt im Seitenbereich. Nur lokale Adressen (auf deinem eigenen Rechner) können als Vorschau geöffnet werden.
- du im Terminal bei einer lokalen Adresse auf **Vorschau öffnen** drückst
- eine [Projektaktion](/project-actions/) mit aktiviertem Auto-Öffnen einen startet
- du einem lokalen Link in einer Chat-Nachricht folgst
## Konsole und Inspektion
Du kannst die Adresse jederzeit selbst eintippen. Ein bloßes `localhost:5173` wird als `http://` verstanden, das Schema musst du also nicht mitschreiben.
Im Vorschau-Bereich kannst du:
## Mit einem entfernten OpenChamber arbeiten
- die **Konsole** der Seite beobachten — Fehler, Warnungen und Protokolle, so gefiltert, wie du möchtest
- **inspect** einschalten, auf ein beliebiges Element klicken und eine Notiz dazu senden — Selektor, Stile, Position und ein Screenshot — direkt in den Chat
Läuft OpenChamber auf einem anderen Rechner, liegt sein Dev-Server ebenfalls auf *jenem* Rechner — `localhost` auf deinem Laptop führt ganz woandershin. Die Desktop-App erledigt das für dich: Sie öffnet einen lokalen Port, der die Verbindung zum entfernten Dev-Server durchreicht. Die Seite lädt ganz normal, mit funktionierendem Hot Reload und Entwicklerwerkzeugen. Du tippst weiterhin die Adresse, die du erwartest; die Technik dahinter bleibt dir aus dem Weg.
Das ist der schnellste Weg, dem Agenten "diese Schaltfläche hier" zu sagen, ohne sie beschreiben zu müssen.
Dafür brauchst du die Desktop-App. In einem Browser-Tab lassen sich nur Dev-Server auf deinem eigenen Rechner öffnen.
## Die Seite annotieren
Wie du auf Elemente zeigst, auf der Seite zeichnest und alles in den Chat schickst, steht unter [Browser-Panel](/desktop-browser/).
## Verwandt
- [Projektaktionen](/project-actions/) — einen Server automatisch öffnen, wenn du ihn startest
- [Desktop-Browser](/desktop-browser/) — dieselben Werkzeuge für jede Seite, auf dem Desktop
- [Projektaktionen](/project-actions/) — einen Server beim Start automatisch öffnen
- [Browser-Panel](/desktop-browser/) — Seiten annotieren und den Agenten steuern lassen
@@ -12,7 +12,7 @@ Zum Schreiben eigener Skills siehe [Skills](/skills/).
## Einen Skill installieren
1. Öffne den Katalog.
2. Durchsuche die eingebauten Quellen — das Anthropic-Skills-Repo und die ClawdHub-Community-Registry — oder nutze die Suche.
2. Durchsuche die eingebauten Quellen — das Anthropic-Skills-Repo und die ClawHub-Community-Registry — oder nutze die Suche.
3. Wähle einen Skill aus und installiere ihn.
4. Entscheide, wo er installiert werden soll: für alles, was du tust, oder nur für das aktuelle Projekt.
+51 -10
View File
@@ -1,22 +1,63 @@
---
title: Desktop Browser
description: Browse any page inside the desktop app, with inspect and console capture.
title: Browser Panel
description: Browse any page inside the app, annotate it, and let the agent drive it.
---
# Desktop Browser
# Browser Panel
The desktop app has a built-in browser so you can open any page right next to your chat, point at elements to ask about them, and capture the page's console. Open it from the globe button in the app header.
The browser panel opens any page right next to your chat. Open it from the globe button in the app header.
> The desktop browser is a **desktop-only** feature. On the web, the [preview](/preview/) panel offers the same inspect-and-console tools for your local dev server.
On the desktop app it is a real browser: your logins persist, hot reload works, and developer tools are one click away. In a web browser tab the panel can still display a page, but it cannot look inside one — the annotation tools below are desktop-only. The VS Code extension has no browser panel at all: VS Code is already an editor with a browser beside it, and everything that makes this panel worth having needs the desktop app.
## Inspect and annotate
Pages opened here cannot use your camera, microphone, or location: those requests are refused.
Turn on **inspect** and click any element on the page. OpenChamber captures a note about it — what it is, its styles, where it sits, and a screenshot — and attaches it to your chat message. It's the quickest way to tell the agent "this element, right here."
## The toolbar
## Console capture
The address bar remembers pages you have opened in this project and offers them as you type, matching part of an address or a page title. Arrow keys move through the list, Enter opens the highlighted entry, and the button on a row removes it.
The browser collects the page's console output — errors, warnings, and logs — so you can filter and read it without opening developer tools.
**Reload** is next to it, along with a **hard reload** that ignores the cache when a change refuses to show up, and zoom controls that scale the page only.
**Clear cookies** and **Clear cached data** apply to this panel alone. Your OpenChamber session and any other window are untouched.
## Annotate a page
Press **Annotate** and a toolbar appears over the page with three tools:
- **Element** — click an element. Clicking another moves the selection; clicking the same one again clears it.
- **Region** — drag a box around an area, when what you mean covers more than one element.
- **Draw** — sketch freehand over the page.
Write what you want in the box that appears beside your mark, and press **Attach** — or just press Enter. Your chat message gets a card with everything you marked, your note, and a screenshot of the visible page with your marks drawn on it — so you can say "this button, a bit rounder" instead of describing where it is.
The page itself is never modified — annotating marks what is there. `Esc` cancels and closes the toolbar.
## Let the agent drive
The agent can use the browser panel itself — opening a page, reading what is on it, clicking, typing, scrolling, and switching between mobile, tablet and desktop layouts — so it can check its own work instead of asking you to. You will see it happening in the panel.
The agent cannot run arbitrary code in the page. The browser keeps your real logins, so it is limited to the specific actions above.
It can also save a picture of what it is looking at into `.openchamber/screenshots/` in your project and show it to you in its reply. That is what makes a before-and-after possible, and the file stays there afterwards to attach to a pull request.
The browser actions are the **OpenChamber Web tool**, which can be turned on and off on its own in **Settings → General → OpenChamber Tools**.
This needs the desktop app: a page shown in a web browser tab cannot be driven.
## Size and appearance
Press the phone button to open the device bar. Pick a preset or type a width and
height, and the page is laid out at that size — scaled down to fit the panel
when it is bigger, but still measuring itself at the size you asked for.
The same bar forces the page to light or dark, so a theme can be checked without
changing anything on your machine. It leaves DevTools alone; a page can only
have one debugger attached, so close DevTools first if it is open.
## Developer tools
Press the terminal button in the toolbar to open Chromium's own developer tools for the page — console, network, elements, everything you would expect.
## Related
- [Preview & Dev Servers](/preview/) — the same tools for your local dev server
- [Preview & Dev Servers](/preview/) — opening your running app, including on a remote machine
- [Agent Control Tool](/agent-control-tool/) — sessions, worktrees and scheduled tasks from chat
@@ -28,7 +28,7 @@ La herramienta puede listar proyectos y preferencias de modelos, crear y continu
## Activar o desactivar la herramienta
Abre **Ajustes → General → OpenCode CLI**, cambia **Herramienta de control para agentes** y selecciona **Save + Reload**. El ajuste se aplica cuando se reinicia el servidor OpenCode gestionado.
Abre **Ajustes → General → Herramientas de OpenChamber** y cambia **Herramienta de control para agentes**. El ajuste se aplica cuando se reinicia el servidor OpenCode gestionado, que OpenChamber ofrece como **Apply & Restart**.
La herramienta no está disponible cuando OpenChamber se conecta a un servidor OpenCode externo mediante `OPENCODE_HOST` o skip-start, ni dentro de la extensión de VS Code. Las instalaciones web y de escritorio que usan el servidor OpenCode gestionado por OpenChamber la admiten automáticamente.
@@ -37,3 +37,4 @@ La herramienta no está disponible cuando OpenChamber se conecta a un servidor O
- [Tareas programadas](/es/scheduled-tasks/)
- [Sesiones de worktree](/es/worktrees/)
- [Objetivos de sesión](/es/session-goals/)
- [Panel del navegador](/es/desktop-browser/) — la herramienta OpenChamber Web, para ver una página y manejarla
@@ -1,22 +1,53 @@
---
title: Navegador de escritorio
description: Navega cualquier página dentro de la app de escritorio, con inspección y captura de consola.
title: Panel del navegador
description: Navega cualquier página dentro de la aplicación, anótala y deja que el agente la maneje.
---
# Navegador de escritorio
# Panel del navegador
La app de escritorio tiene un navegador integrado para que abras cualquier página justo al lado de tu chat, señales elementos para preguntar sobre ellos y captures la consola de la página. Ábrelo desde el botón del globo en el encabezado de la app.
El panel del navegador abre cualquier página justo al lado del chat. Ábrelo con el botón del globo de la cabecera.
> El navegador de escritorio es una función **solo de escritorio**. En la web, el panel de [vista previa](/es/preview/) ofrece las mismas herramientas de inspección y consola para tu servidor de desarrollo local.
En la aplicación de escritorio es un navegador de verdad: tus sesiones se mantienen, la recarga en caliente funciona y las herramientas de desarrollo están a un clic. En una pestaña del navegador el panel puede mostrar una página, pero no mirar dentro de ella: las herramientas de anotación de abajo son solo de escritorio.
## Inspecciona y anota
Las páginas que abras aquí no pueden usar tu cámara, tu micrófono ni tu ubicación: esas peticiones se rechazan.
Activa **inspect** y haz clic en cualquier elemento de la página. OpenChamber captura una nota sobre él —qué es, sus estilos, dónde se sitúa y una captura de pantalla— y la adjunta a tu mensaje del chat. Es la forma más rápida de decirle al agente "este elemento, justo aquí".
## La barra de herramientas
## Captura de consola
La barra de direcciones recuerda las páginas que has abierto en este proyecto y las ofrece mientras escribes, buscando en parte de la dirección o del título de la página. Las flechas recorren la lista, Enter abre la entrada resaltada y el botón de una fila la quita.
El navegador recopila la salida de la consola de la página —errores, advertencias y registros— para que puedas filtrarla y leerla sin abrir las herramientas de desarrollo.
Al lado está **Recargar**, junto con una **recarga forzada** que ignora la caché cuando un cambio se niega a aparecer, y los controles de zoom, que escalan solo la página.
**Borrar cookies** y **Borrar datos en caché** afectan únicamente a este panel. Tu sesión de OpenChamber y cualquier otra ventana quedan intactas.
## Anotar una página
Pulsa **Anotar** y aparecerá una barra sobre la página con tres herramientas:
- **Elemento** — haz clic en un elemento. Hacer clic en otro mueve la selección; volver a hacer clic en el mismo la quita.
- **Región** — arrastra un recuadro alrededor de una zona cuando te refieras a más de un elemento.
- **Dibujar** — traza a mano alzada sobre la página.
Escribe lo que quieres en el cuadro que aparece junto a tu marca y pulsa **Adjuntar**, o simplemente Enter. Tu mensaje recibe una tarjeta con todo lo que has marcado, tu nota y una captura de la página visible con tus marcas dibujadas encima — así puedes decir "este botón, un poco más redondeado" en vez de describir dónde está.
La página en sí nunca se modifica: anotar solo marca lo que ya está ahí. `Esc` cancela y cierra la barra.
## Dejar que el agente maneje
El agente puede usar el panel del navegador por su cuenta — abrir una página, leer lo que hay en ella, hacer clic, escribir, desplazarse y alternar entre diseño móvil, de tableta y de escritorio — para comprobar su propio trabajo en lugar de pedírtelo a ti. Lo verás ocurrir en el panel.
El agente no puede ejecutar código arbitrario en la página. El navegador conserva tus sesiones reales, así que se limita a las acciones anteriores.
También puede guardar una imagen de lo que está viendo en `.openchamber/screenshots/` de tu proyecto y mostrártela en su respuesta. Eso es lo que hace posible un antes y después, y el archivo se queda ahí para adjuntarlo a un pull request.
Las acciones del navegador son la **herramienta OpenChamber Web**, que se activa y desactiva por separado en **Ajustes → General → Herramientas de OpenChamber**.
Esto necesita la aplicación de escritorio: una página mostrada en una pestaña del navegador no se puede controlar.
## Herramientas de desarrollo
Pulsa el botón de terminal de la barra para abrir las herramientas de desarrollo propias de Chromium — consola, red, elementos, todo lo habitual.
## Relacionado
- [Vista previa y servidores de desarrollo](/es/preview/) — las mismas herramientas para tu servidor de desarrollo local
- [Vista previa y servidores de desarrollo](/preview/) — abrir tu aplicación en marcha, también en una máquina remota
- [Herramienta de control para agentes](/es/agent-control-tool/) — sesiones, worktrees y tareas programadas desde el chat
+17 -14
View File
@@ -5,28 +5,31 @@ description: Abre un servidor de desarrollo en marcha dentro de OpenChamber.
# Vista previa y servidores de desarrollo
Cuando inicias un servidor de desarrollo, OpenChamber puede abrirlo dentro de la propia app en lugar de en una pestaña de navegador aparte, para que veas tu sitio junto al chat, captures su consola y señales elementos para preguntar sobre ellos.
Cuando arrancas un servidor de desarrollo, OpenChamber puede abrirlo dentro de la propia aplicación en lugar de en una pestaña aparte — así ves tu sitio junto al chat y puedes señalar elementos para preguntar por ellos.
## Abre una vista previa
## Abrir un servidor de desarrollo
OpenChamber observa la salida de la terminal en busca de una dirección local (la línea `Local:` que imprimen herramientas como Vite, Next.js o Astro). Cuando detecta una:
Abre el panel del navegador con el botón del globo de la cabecera. Si ya hay un servidor en marcha, aparece en la lista y se abre con un clic: OpenChamber lo encuentra mirando qué está escuchando de verdad en tu máquina, así que funciona sin importar cómo lo hayas arrancado.
- en la terminal aparece un botón **Open preview**
- una [acción de proyecto](/es/project-actions/) con la apertura automática activada la abre por ti
- un enlace local en un mensaje del chat también puede abrirla
Un servidor de desarrollo también se abre solo cuando:
El sitio se carga en el panel lateral. Solo se pueden previsualizar direcciones locales (en tu propia máquina).
- pulsas **Abrir vista previa** sobre una dirección local en la terminal
- una [acción de proyecto](/project-actions/) con apertura automática arranca uno
- sigues un enlace local en un mensaje del chat
## Consola e inspección
Siempre puedes escribir la dirección a mano. Un simple `localhost:5173` se entiende como `http://`, así que no hace falta escribir el esquema.
En el panel de vista previa puedes:
## Trabajar con un OpenChamber remoto
- ver la **consola** de la página —errores, advertencias y registros— filtrada como prefieras
- activar **inspect**, hacer clic en cualquier elemento y enviar una nota sobre él —selector, estilos, posición y una captura de pantalla— directamente al chat
Cuando OpenChamber corre en otra máquina, su servidor de desarrollo está en *esa* máquina — `localhost` en tu portátil apunta a otro sitio completamente distinto. La aplicación de escritorio se encarga: abre un puerto local que lleva la conexión hasta el servidor remoto, de modo que la página carga con normalidad, con recarga en caliente y herramientas de desarrollo funcionando. Tú sigues escribiendo la dirección que esperas; la fontanería no te estorba.
Esta es la forma más rápida de decirle al agente "este botón, aquí" sin describirlo.
Esto requiere la aplicación de escritorio. En una pestaña del navegador solo se pueden abrir servidores de tu propia máquina.
## Anotar la página
Consulta [Panel del navegador](/desktop-browser/) para señalar elementos, dibujar sobre la página y enviarlo todo al chat.
## Relacionado
- [Acciones de proyecto](/es/project-actions/) — abre automáticamente un servidor al iniciarlo
- [Navegador de escritorio](/es/desktop-browser/) — las mismas herramientas para cualquier página, en el escritorio
- [Acciones de proyecto](/project-actions/) — abrir un servidor automáticamente al arrancarlo
- [Panel del navegador](/desktop-browser/) — anotar páginas y dejar que el agente las maneje
@@ -12,7 +12,7 @@ Para escribir tus propias skills, consulta [Skills](/es/skills/).
## Instala una skill
1. Abre el catálogo.
2. Explora las fuentes integradas —el repositorio de skills de Anthropic y el registro comunitario de ClawdHub— o busca.
2. Explora las fuentes integradas —el repositorio de skills de Anthropic y el registro comunitario de ClawHub— o busca.
3. Elige una skill e instálala.
4. Elige dónde instalarla: para todo lo que hagas, o solo en el proyecto actual.
@@ -28,7 +28,7 @@ Loutil peut répertorier les projets et les préférences de modèles, créer
## Activer ou désactiver loutil
Ouvrez **Paramètres → Général → OpenCode CLI**, modifiez **Outil de contrôle pour les agents**, puis sélectionnez **Save + Reload**. Le réglage sapplique après le redémarrage du serveur OpenCode géré.
Ouvrez **Paramètres → Général → Outils OpenChamber** et modifiez **Outil de contrôle pour les agents**. Le réglage sapplique au redémarrage du serveur OpenCode géré, que OpenChamber propose sous **Apply & Restart**.
Loutil nest pas disponible quand OpenChamber se connecte à un serveur OpenCode externe avec `OPENCODE_HOST` ou skip-start, ni dans lextension VS Code. Les installations desktop et web utilisant le serveur OpenCode géré par OpenChamber le prennent automatiquement en charge.
@@ -37,3 +37,4 @@ Loutil nest pas disponible quand OpenChamber se connecte à un serveur Ope
- [Tâches planifiées](/fr/scheduled-tasks/)
- [Sessions worktree](/fr/worktrees/)
- [Objectifs de session](/fr/session-goals/)
- [Panneau navigateur](/fr/desktop-browser/) — l'outil OpenChamber Web, pour consulter une page et la piloter
@@ -1,22 +1,53 @@
---
title: Navigateur desktop
description: Parcourez nimporte quelle page dans lapplication desktop, avec inspection et capture de console.
title: Panneau navigateur
description: Parcourez n'importe quelle page dans l'application, annotez-la et laissez l'agent la piloter.
---
# Navigateur desktop
# Panneau navigateur
Lapplication desktop possède un navigateur intégré pour ouvrir nimporte quelle page juste à côté de votre chat, pointer des éléments pour poser des questions à leur sujet et capturer la console de la page. Ouvrez-le avec le bouton globe dans len-tête de lapplication.
Le panneau navigateur ouvre n'importe quelle page juste à côté de votre discussion. Ouvrez-le depuis le bouton globe de l'en-tête.
> Le navigateur desktop est une fonctionnalité **desktop uniquement**. Sur le web, le panneau [aperçu](/preview/) offre les mêmes outils dinspection et de console pour votre serveur de dev local.
Dans l'application de bureau, c'est un vrai navigateur : vos connexions persistent, le rechargement à chaud fonctionne et les outils de développement sont à un clic. Dans un onglet de navigateur, le panneau affiche bien une page mais ne peut pas regarder à l'intérieur — les outils d'annotation ci-dessous sont réservés au bureau.
## Inspecter et annoter
Les pages ouvertes ici ne peuvent pas utiliser votre caméra, votre micro ni votre position : ces demandes sont refusées.
Activez **inspect** et cliquez sur nimporte quel élément de la page. OpenChamber capture une note à son sujet — ce que cest, ses styles, sa position et une capture d’écran — puis lattache à votre message de chat. Cest le moyen le plus rapide de dire à lagent « cet élément, juste ici ».
## La barre d'outils
## Capture de console
La barre d'adresse retient les pages que vous avez ouvertes dans ce projet et les propose pendant la saisie, en cherchant dans une partie de l'adresse ou du titre de la page. Les flèches parcourent la liste, Entrée ouvre l'entrée surlignée, et le bouton d'une ligne la retire.
Le navigateur collecte la sortie console de la page — erreurs, avertissements et logs — pour que vous puissiez la filtrer et la lire sans ouvrir les outils développeur.
À côté se trouve **Recharger**, ainsi qu'un **rechargement forcé** qui ignore le cache quand un changement refuse d'apparaître, et des commandes de zoom qui agrandissent la page seule.
## Pages liées
**Effacer les cookies** et **Effacer les données en cache** ne concernent que ce panneau. Votre session OpenChamber et les autres fenêtres n'y touchent pas.
- [Aperçu et serveurs de dev](/preview/) — les mêmes outils pour votre serveur de dev local
## Annoter une page
Appuyez sur **Annoter** : une barre apparaît au-dessus de la page avec trois outils.
- **Élément** — cliquez sur un élément. Cliquer sur un autre déplace la sélection ; recliquer sur le même la retire.
- **Zone** — tracez un cadre autour d'une portion lorsque votre remarque porte sur plusieurs éléments.
- **Dessin** — croquez à main levée par-dessus la page.
Écrivez votre demande dans le champ qui apparaît à côté de votre marque, puis appuyez sur **Joindre** — ou simplement sur Entrée. Votre message reçoit une carte avec tout ce que vous avez marqué, votre note et une capture de la page visible avec vos marques dessinées dessus — vous pouvez donc dire « ce bouton, un peu plus arrondi » au lieu de décrire où il se trouve.
La page elle-même n'est jamais modifiée : annoter ne fait que marquer ce qui s'y trouve. `Échap` annule et ferme la barre.
## Laisser l'agent piloter
L'agent peut se servir lui-même du panneau navigateur — ouvrir une page, lire ce qu'elle contient, cliquer, saisir du texte, faire défiler et basculer entre les mises en page mobile, tablette et bureau — afin de vérifier son propre travail plutôt que de vous le demander. Vous le voyez faire dans le panneau.
L'agent ne peut pas exécuter de code arbitraire dans la page. Le navigateur conserve vos vraies connexions ; il s'en tient donc aux actions ci-dessus.
Il peut aussi enregistrer une image de ce qu'il regarde dans `.openchamber/screenshots/` de votre projet et vous la montrer dans sa réponse. C'est ce qui rend un avant-après possible, et le fichier reste ensuite disponible pour l'attacher à une pull request.
Les actions du navigateur constituent l'**outil OpenChamber Web**, qui s'active et se désactive séparément dans **Réglages → Général → Outils OpenChamber**.
Cela nécessite l'application de bureau : une page affichée dans un onglet de navigateur ne peut pas être pilotée.
## Outils de développement
Appuyez sur le bouton terminal de la barre pour ouvrir les outils de développement de Chromium pour la page — console, réseau, éléments, tout ce à quoi vous vous attendez.
## Voir aussi
- [Aperçu et serveurs de développement](/preview/) — ouvrir votre application en cours, y compris sur une machine distante
- [Outil de contrôle pour les agents](/fr/agent-control-tool/) — sessions, worktrees et tâches planifiées depuis la discussion
+21 -18
View File
@@ -1,32 +1,35 @@
---
title: Aperçu et serveurs de dev
description: Ouvrez un serveur de dev en cours dexécution dans OpenChamber.
title: Aperçu et serveurs de développement
description: Ouvrez un serveur de développement en cours d'exécution dans OpenChamber.
---
# Aperçu et serveurs de dev
# Aperçu et serveurs de développement
Quand vous démarrez un serveur de dev, OpenChamber peut louvrir directement dans lapplication au lieu dun onglet de navigateur séparé — vous voyez ainsi votre site à côté du chat, vous capturez sa console et vous pouvez pointer des éléments pour poser des questions à leur sujet.
Quand vous lancez un serveur de développement, OpenChamber peut l'ouvrir directement dans l'application plutôt que dans un onglet séparé — vous voyez ainsi votre site à côté de la discussion et pouvez désigner des éléments pour poser des questions à leur sujet.
## Ouvrir un aperçu
## Ouvrir un serveur de développement
OpenChamber surveille la sortie du terminal pour détecter une adresse locale (la ligne `Local:` affichée par des outils comme Vite, Next.js ou Astro). Quand il en trouve une :
Ouvrez le panneau navigateur depuis le bouton globe de l'en-tête. Si un serveur tourne déjà, il apparaît dans la liste et un clic suffit à l'ouvrir : OpenChamber le repère à partir de ce qui écoute réellement sur votre machine, quelle que soit la façon dont vous l'avez lancé.
- dans le terminal, un bouton **Ouvrir laperçu** apparaît
- une [action de projet](/project-actions/) avec louverture automatique activée louvre pour vous
- un lien local dans un message de chat peut aussi louvrir
Un serveur s'ouvre également tout seul quand :
Le site se charge dans le panneau latéral. Seules les adresses locales (sur votre propre machine) peuvent être prévisualisées.
- vous appuyez sur **Ouvrir l'aperçu** sur une adresse locale dans le terminal
- une [action de projet](/project-actions/) avec ouverture automatique en démarre un
- vous suivez un lien local dans un message de la discussion
## Console et inspection
Vous pouvez toujours saisir l'adresse vous-même. Un simple `localhost:5173` est compris comme `http://`, inutile donc d'écrire le schéma.
Dans le panneau daperçu, vous pouvez :
## Travailler avec un OpenChamber distant
- regarder la **console** de la page — erreurs, avertissements et logs, filtrés comme vous le voulez
- activer **inspect**, cliquer sur nimporte quel élément et envoyer une note à son sujet — sélecteur, styles, position et capture d’écran — directement dans le chat
Quand OpenChamber tourne sur une autre machine, son serveur de développement s'y trouve aussi — `localhost` sur votre portable désigne tout autre chose. L'application de bureau s'en charge : elle ouvre un port local qui achemine la connexion jusqu'au serveur distant, si bien que la page se charge normalement, avec rechargement à chaud et outils de développement fonctionnels. Vous continuez à saisir l'adresse attendue ; la tuyauterie reste hors de votre chemin.
Cest le moyen le plus rapide de dire à lagent « ce bouton, ici » sans devoir le décrire.
Cela nécessite l'application de bureau. Dans un onglet de navigateur, seuls les serveurs de votre propre machine peuvent être ouverts.
## Pages liées
## Annoter la page
- [Actions de projet](/project-actions/) — ouvrir automatiquement un serveur quand vous le démarrez
- [Navigateur desktop](/desktop-browser/) — les mêmes outils pour nimporte quelle page, sur desktop
Voyez [Panneau navigateur](/desktop-browser/) pour désigner des éléments, dessiner sur la page et envoyer le tout dans la discussion.
## Voir aussi
- [Actions de projet](/project-actions/) — ouvrir automatiquement un serveur à son démarrage
- [Panneau navigateur](/desktop-browser/) — annoter les pages et laisser l'agent les piloter
@@ -12,7 +12,7 @@ Pour écrire vos propres skills, voir [Skills](/skills/).
## Installer un skill
1. Ouvrez le catalogue.
2. Parcourez les sources intégrées — le dépôt de skills Anthropic et le registre communautaire ClawdHub — ou lancez une recherche.
2. Parcourez les sources intégrées — le dépôt de skills Anthropic et le registre communautaire ClawHub — ou lancez une recherche.
3. Choisissez un skill et installez-le.
4. Choisissez où linstaller : pour tout ce que vous faites, ou seulement pour le projet actuel.
@@ -28,7 +28,7 @@ description: エージェントがチャットから OpenChamber のセッショ
## ツールを有効または無効にする
**設定 → 一般 → OpenCode CLI** を開き、**エージェント制御ツール**を変更して、**Save + Reload** を選択します。この設定は、管理対象の OpenCode サーバーが再起動した後に反映されます。
**設定 → 一般 → OpenChamber ツール** を開き、**エージェント制御ツール**を変更します。この設定は、管理対象の OpenCode サーバーが再起動すると反映されます。再起動は OpenChamber が **Apply & Restart** として案内します。
OpenChamber が `OPENCODE_HOST` または skip-start で外部 OpenCode サーバーに接続している場合や、VS Code 拡張機能内では、このツールを利用できません。OpenChamber が管理する OpenCode サーバーを使用するデスクトップ版と Web 版では自動的に利用できます。
@@ -37,3 +37,4 @@ OpenChamber が `OPENCODE_HOST` または skip-start で外部 OpenCode サー
- [スケジュールタスク](/ja/scheduled-tasks/)
- [Worktree セッション](/ja/worktrees/)
- [セッションゴール](/ja/session-goals/)
- [ブラウザパネル](/ja/desktop-browser/) — ページを見て操作するための OpenChamber Web ツール
@@ -1,22 +1,53 @@
---
title: デスクトップブラウザ
description: デスクトップアプリ内で任意のページを開き、検査とコンソール取得を使います。
title: ブラウザパネル
description: アプリ内で任意のページを開き、注釈を付け、エージェントに操作させます。
---
# デスクトップブラウザ
# ブラウザパネル
デスクトップアプリには組み込みブラウザがあります。チャットのすぐ横で任意のページを開き、要素を指して質問したり、ページのコンソールを取得したりできます。アプリヘッダーの地球儀ボタンから開きます
ブラウザパネルはチャットのすぐ隣に任意のページを開きます。ヘッダーの地球儀ボタンから開いてください
> デスクトップブラウザは**デスクトップ専用**機能です。Web では、[プレビュー](/preview/) パネルがローカル開発サーバー向けに同じ検査・コンソールツールを提供します。
デスクトップアプリでは本物のブラウザです。ログイン状態は保持され、ホットリロードが動き、開発者ツールはワンクリックで開けます。ブラウザのタブでもページの表示はできますが、中を覗くことはできません。以下の注釈ツールはデスクトップ専用です。
## 検査して注釈を付ける
ここで開いたページは、カメラ・マイク・位置情報を使えません。これらの要求は拒否されます。
**inspect** をオンにして、ページ上の任意の要素をクリックします。OpenChamber はその要素について、何であるか、スタイル、位置、スクリーンショットを含むメモを取得し、チャットメッセージに添付します。エージェントに「この要素、ここ」と伝える最短の方法です。
## ツールバー
## コンソール取得
アドレスバーはこのプロジェクトで開いたページを覚えていて、入力中に候補として出します。アドレスの一部でもページタイトルの一部でも一致します。矢印キーで候補を移動し、Enter で選択中の候補を開き、行のボタンでその候補を消せます。
ブラウザはページのコンソール出力(エラー、警告、ログ)を集めるので、開発者ツールを開かずにフィルターして読めます。
隣には **再読み込み** があり、変更がどうしても反映されないときのためにキャッシュを無視する **強制再読み込み**、そしてページだけを拡大縮小するズーム操作も並びます。
**Cookie を消去** と **キャッシュを消去** はこのパネルにだけ効きます。OpenChamber のセッションや他のウィンドウには影響しません。
## ページに注釈を付ける
**注釈** を押すと、ページの上に3つのツールを備えたバーが表示されます。
- **要素** — 要素をクリックします。別の要素をクリックすると選択が移り、同じ要素をもう一度クリックすると解除されます。
- **範囲** — 複数の要素にまたがる話をしたいときは、領域をドラッグで囲みます。
- **描画** — ページの上にフリーハンドで描きます。
印の隣に現れる入力欄に希望を書き、**添付** を押します。Enter でも送れます。チャットメッセージには、印を付けた内容、あなたのメモ、表示中のページに印を描き込んだスクリーンショットを含むカードが付きます。「このボタン、もう少し角を丸く」と言えば済み、場所を説明する必要はありません。
ページ自体は変更されません。注釈はそこにあるものに印を付けるだけです。`Esc` で取り消してツールバーを閉じます。
## エージェントに操作させる
エージェントはブラウザパネルを自分で使えます。ページを開き、内容を読み、クリックし、文字を入力し、スクロールし、モバイル・タブレット・デスクトップのレイアウトを切り替えて、自分の作業をあなたに頼まず自分で確認します。その様子はパネルで見えます。
エージェントがページ内で任意のコードを実行することはできません。ブラウザは実際のログイン状態を保持しているため、上記の操作に限定されています。
見ている内容をプロジェクト内の `.openchamber/screenshots/` に画像として保存し、返答の中で見せることもできます。ビフォー・アフターができるのはこのためで、ファイルはその後もプルリクエストに添付できる形で残ります。
ブラウザ操作は **OpenChamber Web ツール** で、**設定 → 一般 → OpenChamber ツール** から単独でオン・オフできます。
これにはデスクトップアプリが必要です。ブラウザのタブに表示したページは操作できません。
## 開発者ツール
バーのターミナルボタンを押すと、そのページに対する Chromium 本来の開発者ツールが開きます。コンソール、ネットワーク、要素など、期待どおりのものがすべて使えます。
## 関連
- [プレビューと開発サーバー](/preview/) — ローカル開発サーバー向けの同じツール
- [プレビューと開発サーバー](/preview/) — リモートマシン上のものも含め、実行中のアプリを開く
- [エージェント制御ツール](/ja/agent-control-tool/) — チャットからセッション・worktree・スケジュールタスクを扱う
+18 -15
View File
@@ -1,32 +1,35 @@
---
title: プレビューと開発サーバー
description: 実行中の開発サーバーを OpenChamber で開きます。
description: 実行中の開発サーバーを OpenChamber の中で開きます。
---
# プレビューと開発サーバー
開発サーバーを起動すると、OpenChamber は別のブラウザタブではなくアプリ内で直接開けます。サイトをチャットの横で見ながら、コンソールを取得し、要素を指して質問できます。
開発サーバーを起動すると、OpenChamber は別のブラウザタブではなくアプリ内でそれを開けます。チャットの隣にサイトを表示したまま、要素を指し示して質問できます。
## プレビューを開く
## 開発サーバーを開く
OpenChamber はターミナル出力からローカルアドレスを監視します(Vite、Next.js、Astro などが表示する `Local:` 行)。見つけると次のことができます。
ヘッダーの地球儀ボタンからブラウザパネルを開きます。開発サーバーがすでに動いていれば一覧に表示され、クリックひとつで開けます。OpenChamber はマシン上で実際に待ち受けているものから見つけるので、どうやって起動したかに関係なく機能します。
- ターミナルに **Open preview** ボタンが表示されます
- 自動オープンを有効にした [プロジェクトアクション](/project-actions/) が開きます
- チャットメッセージ内のローカルリンクからも開けます
次の場合にも開発サーバーは自動で開きます
サイトはサイドパネルに読み込まれます。プレビューできるのはローカルアドレス(あなたのマシン上)のみです。
- ターミナルのローカルアドレスで **プレビューを開く** を押したとき
- 自動オープンを有効にした[プロジェクトアクション](/project-actions/)が起動したとき
- チャットメッセージ内のローカルリンクをたどったとき
## コンソールと検査
アドレスはいつでも自分で入力できます。`localhost:5173` のようにスキームを省いた入力は `http://` として扱われます。
プレビューパネルでは次のことができます。
## リモートの OpenChamber を使う場合
- ページの **console** を見る — エラー、警告、ログを好きなようにフィルターできます
- **inspect** をオンにし、任意の要素をクリックして、そのメモ(セレクター、スタイル、位置、スクリーンショット)をそのままチャットへ送る
OpenChamber が別のマシンで動いているとき、開発サーバーも*そちら*のマシンにあります。手元のノートPCの `localhost` はまったく別の場所を指します。デスクトップアプリはこれを引き受けます。ローカルポートを開いてリモートの開発サーバーまで接続を通すので、ページは普通に読み込まれ、ホットリロードも開発者ツールも動きます。入力するアドレスは期待どおりのままで、裏側の仕組みが邪魔をすることはありません。
これは「このボタン、ここ」とエージェントに伝える最速の方法です。
これにはデスクトップアプリが必要です。ブラウザのタブでは、自分のマシン上の開発サーバーだけを開けます。
## ページに注釈を付ける
要素を指し示す、ページに描き込む、それらをまとめてチャットへ送る方法は[ブラウザパネル](/desktop-browser/)を参照してください。
## 関連
- [プロジェクトアクション](/project-actions/) — サーバー起動時に自動で開く
- [デスクトップブラウザ](/desktop-browser/) — デスクトップで任意のページに同じツールを使う
- [プロジェクトアクション](/project-actions/) — 起動時にサーバーを自動で開く
- [ブラウザパネル](/desktop-browser/) — ページへの注釈とエージェントによる操作
@@ -12,7 +12,7 @@ Skills Catalog では、自分で書く代わりに、他の人が公開した
## スキルをインストールする
1. カタログを開きます。
2. 組み込みソース(Anthropic skills repo と ClawdHub community registry)を閲覧するか、検索します。
2. 組み込みソース(Anthropic skills repo と ClawHub community registry)を閲覧するか、検索します。
3. スキルを選び、インストールします。
4. インストール先を選びます。すべての作業で使うか、現在のプロジェクトだけで使うかです。
@@ -28,7 +28,7 @@ description: 에이전트가 채팅에서 OpenChamber 세션, worktree, 예약
## 도구 켜기 또는 끄기
**설정 → 일반 → OpenCode CLI**를 열고 **에이전트 제어 도구**를 변경한 다음 **Save + Reload**를 선택하세요. 관리형 OpenCode 서버가 다시 시작된 후 설정이 적용됩니다.
**설정 → 일반 → OpenChamber 도구**를 열고 **에이전트 제어 도구**를 변경하세요. 설정은 관리형 OpenCode 서버가 다시 시작되면 적용되며, OpenChamber가 **Apply & Restart** 로 안내합니다.
OpenChamber가 `OPENCODE_HOST` 또는 skip-start를 통해 외부 OpenCode 서버에 연결된 경우와 VS Code 확장에서는 이 도구를 사용할 수 없습니다. OpenChamber의 관리형 OpenCode 서버를 사용하는 데스크톱 및 웹 설치에서는 자동으로 지원됩니다.
@@ -37,3 +37,4 @@ OpenChamber가 `OPENCODE_HOST` 또는 skip-start를 통해 외부 OpenCode 서
- [예약 작업](/ko/scheduled-tasks/)
- [Worktree 세션](/ko/worktrees/)
- [세션 목표](/ko/session-goals/)
- [브라우저 패널](/ko/desktop-browser/) — 페이지를 보고 조작하는 OpenChamber Web 도구
@@ -1,22 +1,53 @@
---
title: 데스크톱 브라우저
description: 검사 및 콘솔 캡처 기능과 함께 데스크톱 앱 안에서 임의의 페이지를 탐색하세요.
title: 브라우저 패널
description: 앱 안에서 아무 페이지나 열고 주석을 달며 에이전트가 조작하게 합니다.
---
# 데스크톱 브라우저
# 브라우저 패널
데스크톱 앱에는 내장 브라우저가 있어 채팅 바로 옆에서 임의의 페이지를 열고, 요소를 가리켜 질문하고, 페이지의 콘솔을 캡처할 수 있습니다. 앱 헤더의 지구본 버튼에서 엽니다.
브라우저 패널은 채팅 바로 옆에 아무 페이지나 엽니다. 앱 헤더의 지구본 버튼으로 여세요.
> 데스크톱 브라우저는 **데스크톱 전용** 기능입니다. 웹에서는 [미리보기](/ko/preview/) 패널이 로컬 개발 서버에 대해 동일한 검사 및 콘솔 도구를 제공합니다.
데스크톱 앱에서는 진짜 브라우저입니다. 로그인 상태가 유지되고 핫 리로드가 동작하며 개발자 도구도 클릭 한 번이면 열립니다. 브라우저 탭에서도 페이지를 보여줄 수는 있지만 내부를 들여다볼 수는 없습니다. 아래 주석 도구는 데스크톱 전용입니다.
## 검사 및 주석
여기서 연 페이지는 카메라, 마이크, 위치를 사용할 수 없습니다. 그런 요청은 거부됩니다.
**inspect**를 켜고 페이지의 임의 요소를 클릭합니다. OpenChamber가 그것이 무엇인지, 스타일, 위치, 스크린샷을 담은 메모를 캡처해 채팅 메시지에 첨부합니다. 에이전트에게 "바로 여기 이 요소"라고 알리는 가장 빠른 방법입니다.
## 도구 모음
## 콘솔 캡처
주소창은 이 프로젝트에서 열었던 페이지를 기억해 두었다가 입력하는 동안 제안합니다. 주소의 일부나 페이지 제목의 일부와 맞춰 봅니다. 화살표 키로 목록을 이동하고, Enter로 선택한 항목을 열고, 행의 버튼으로 목록에서 지웁니다.
브라우저는 페이지의 콘솔 출력(오류, 경고, 로그)을 수집하므로 개발자 도구를 열지 않고도 필터링하여 읽을 수 있습니다.
그 옆에는 **새로 고침**이 있고, 변경이 도무지 반영되지 않을 때 캐시를 무시하는 **강력 새로 고침**, 그리고 페이지만 확대·축소하는 확대 조절이 있습니다.
## 관련 항목
**쿠키 지우기**와 **캐시 데이터 지우기**는 이 패널에만 적용됩니다. OpenChamber 세션이나 다른 창은 그대로입니다.
- [Preview & Dev Servers](/ko/preview/) — 로컬 개발 서버에 대한 동일한 도구
## 페이지에 주석 달기
**주석** 을 누르면 페이지 위에 세 가지 도구가 있는 막대가 나타납니다.
- **요소** — 요소를 클릭합니다. 다른 요소를 클릭하면 선택이 옮겨가고, 같은 요소를 다시 클릭하면 해제됩니다.
- **영역** — 여러 요소에 걸친 이야기를 할 때는 해당 부분을 드래그해 감쌉니다.
- **그리기** — 페이지 위에 자유롭게 스케치합니다.
표시 옆에 나타나는 입력란에 원하는 내용을 적고 **첨부** 를 누르세요. Enter 로도 됩니다. 채팅 메시지에 표시한 모든 것, 남긴 메모, 표시 중인 페이지에 표시를 그려 넣은 스크린샷이 담긴 카드가 붙습니다. 위치를 설명하는 대신 "이 버튼, 조금 더 둥글게"라고 말하면 됩니다.
페이지 자체는 변경되지 않습니다. 주석은 있는 것을 표시할 뿐입니다. `Esc` 로 취소하고 도구 막대를 닫습니다.
## 에이전트에게 조작 맡기기
에이전트는 브라우저 패널을 직접 쓸 수 있습니다. 페이지를 열고, 내용을 읽고, 클릭하고, 입력하고, 스크롤하고, 모바일·태블릿·데스크톱 레이아웃을 바꿔 가며 자기 작업을 여러분에게 부탁하지 않고 스스로 확인합니다. 그 과정은 패널에서 보입니다.
에이전트가 페이지에서 임의의 코드를 실행할 수는 없습니다. 브라우저가 실제 로그인 상태를 유지하므로 위에 적힌 동작으로만 제한됩니다.
보고 있는 화면을 프로젝트의 `.openchamber/screenshots/` 에 이미지로 저장하고 답변에서 보여 줄 수도 있습니다. 전후 비교가 가능한 이유가 이것이며, 파일은 그대로 남아 풀 리퀘스트에 첨부할 수 있습니다.
브라우저 동작은 **OpenChamber Web 도구**이며, **설정 → 일반 → OpenChamber 도구** 에서 따로 켜고 끌 수 있습니다.
이 기능에는 데스크톱 앱이 필요합니다. 브라우저 탭에 표시된 페이지는 조작할 수 없습니다.
## 개발자 도구
막대의 터미널 버튼을 누르면 해당 페이지에 대한 Chromium 자체 개발자 도구가 열립니다. 콘솔, 네트워크, 요소 등 기대하는 모든 기능을 쓸 수 있습니다.
## 관련 문서
- [미리보기와 개발 서버](/preview/) — 원격 컴퓨터의 것을 포함해 실행 중인 앱 열기
- [에이전트 제어 도구](/ko/agent-control-tool/) — 채팅에서 세션, worktree, 예약 작업 다루기
+21 -18
View File
@@ -1,32 +1,35 @@
---
title: 미리보기 개발 서버
description: 실행 중인 개발 서버를 OpenChamber 안에서 여세요.
title: 미리보기 개발 서버
description: 실행 중인 개발 서버를 OpenChamber 안에서 엽니다.
---
# 미리보기 개발 서버
# 미리보기 개발 서버
개발 서버를 시작하면 OpenChamber 별도의 브라우저 탭이 아니라 앱 안에서 바로 열 수 있습니다. 그래서 채팅 옆에 사이트를 보고, 콘솔을 캡처하고, 요소를 가리켜 질문할 수 있습니다.
개발 서버를 띄우면 OpenChamber 별도의 브라우저 탭 대신 앱 안에서 바로 열어 줍니다. 채팅 옆에 사이트를 두고 요소를 가리키며 물어볼 수 있습니다.
## 미리보기 열기
## 개발 서버 열기
OpenChamber는 터미널 출력에서 로컬 주소(Vite, Next.js, Astro 같은 도구가 출력하는 `Local:` 줄)를 감시합니다. 발견하면 다음과 같이 동작합니다.
앱 헤더의 지구본 버튼으로 브라우저 패널을 엽니다. 개발 서버가 이미 실행 중이면 목록에 나타나고 클릭 한 번으로 열립니다. OpenChamber는 컴퓨터에서 실제로 수신 대기 중인 것을 보고 찾아내므로, 어떻게 실행했든 상관없이 동작합니다.
- 터미널에 **Open preview** 버튼이 나타납니다
- auto-open이 켜진 [프로젝트 액션](/ko/project-actions/)이 대신 열어줍니다
- 채팅 메시지의 로컬 링크로도 열 수 있습니다
다음 경우에도 개발 서버가 자동으로 열립니다.
사이트는 사이드 패널에 로드됩니다. 로컬 주소(사용자 자신의 컴퓨터)만 미리볼 수 있습니다.
- 터미널의 로컬 주소에서 **미리보기 열기** 를 누를 때
- 자동 열기를 켠 [프로젝트 작업](/project-actions/)이 서버를 시작할 때
- 채팅 메시지의 로컬 링크를 따라갈 때
## 콘솔과 검사
주소는 언제든 직접 입력할 수 있습니다. `localhost:5173` 처럼 스킴을 생략하면 `http://` 로 처리됩니다.
미리보기 패널에서 다음을 할 수 있습니다.
## 원격 OpenChamber와 함께 쓰기
- 페이지의 **console**(오류, 경고, 로그)을 원하는 대로 필터링하여 확인
- **inspect**를 켜고 임의의 요소를 클릭한 뒤 그에 대한 메모(선택자, 스타일, 위치, 스크린샷)를 바로 채팅으로 전송
OpenChamber가 다른 컴퓨터에서 실행 중이면 개발 서버도 *그* 컴퓨터에 있습니다. 노트북의 `localhost`는 전혀 다른 곳을 가리키죠. 데스크톱 앱이 이를 대신 처리합니다. 로컬 포트를 열어 원격 개발 서버까지 연결을 이어 주므로 페이지가 평소처럼 로드되고 핫 리로드와 개발자 도구도 동작합니다. 여러분은 기대한 주소를 그대로 입력하면 되고, 내부 배관은 눈에 띄지 않습니다.
것은 설명 없이 에이전트에게 "여기 이 버튼"이라고 알리는 가장 빠른 방법입니다.
기능에는 데스크톱 앱이 필요합니다. 브라우저 탭에서는 자신의 컴퓨터에 있는 개발 서버만 열 수 있습니다.
## 관련 항목
## 페이지에 주석 달기
- [Project Actions](/ko/project-actions/) — 서버를 시작할 때 자동으로 열기
- [Desktop Browser](/ko/desktop-browser/) — 데스크톱에서 임의의 페이지에 동일한 도구 사용
요소를 가리키고, 페이지에 그리고, 스타일 변경을 시험해 보고, 이 모두를 채팅으로 보내는 방법은 [브라우저 패널](/desktop-browser/)을 참고하세요.
## 관련 문서
- [프로젝트 작업](/project-actions/) — 서버 시작 시 자동으로 열기
- [브라우저 패널](/desktop-browser/) — 페이지 주석과 에이전트 조작
@@ -12,7 +12,7 @@ Skills Catalog를 사용하면 직접 작성하는 대신 다른 사람이 게
## 스킬 설치하기
1. 카탈로그를 엽니다.
2. 내장된 소스(Anthropic 스킬 저장소와 ClawdHub 커뮤니티 레지스트리)를 둘러보거나 검색합니다.
2. 내장된 소스(Anthropic 스킬 저장소와 ClawHub 커뮤니티 레지스트리)를 둘러보거나 검색합니다.
3. 스킬을 선택하고 설치합니다.
4. 설치 위치를 선택합니다. 모든 작업에 적용할지, 현재 프로젝트에만 적용할지 선택합니다.
@@ -28,7 +28,7 @@ Narzędzie może wyświetlać projekty i preferencje modeli, tworzyć i kontynuo
## Włączanie i wyłączanie narzędzia
Otwórz **Ustawienia → Ogólne → OpenCode CLI**, zmień **Narzędzie sterowania dla agentów**, a następnie wybierz **Save + Reload**. Ustawienie zacznie działać po ponownym uruchomieniu zarządzanego serwera OpenCode.
Otwórz **Ustawienia → Ogólne → Narzędzia OpenChamber** i zmień **Narzędzie sterowania dla agentów**. Ustawienie zacznie działać po ponownym uruchomieniu zarządzanego serwera OpenCode, które OpenChamber proponuje jako **Apply & Restart**.
Narzędzie nie jest dostępne, gdy OpenChamber łączy się z zewnętrznym serwerem OpenCode przez `OPENCODE_HOST` lub skip-start, ani w rozszerzeniu VS Code. Instalacje desktopowe i webowe korzystające z serwera OpenCode zarządzanego przez OpenChamber obsługują je automatycznie.
@@ -37,3 +37,4 @@ Narzędzie nie jest dostępne, gdy OpenChamber łączy się z zewnętrznym serwe
- [Zaplanowane zadania](/pl/scheduled-tasks/)
- [Sesje worktree](/pl/worktrees/)
- [Cele sesji](/pl/session-goals/)
- [Panel przeglądarki](/pl/desktop-browser/) — narzędzie OpenChamber Web — oglądanie strony i sterowanie nią
@@ -1,22 +1,53 @@
---
title: Przeglądarka na komputerze
description: Przeglądaj dowolną stronę wewnątrz aplikacji na komputerze, z inspekcją i przechwytywaniem konsoli.
title: Panel przeglądarki
description: Przeglądaj dowolną stronę w aplikacji, dodawaj do niej adnotacje i pozwól agentowi nią sterować.
---
# Przeglądarka na komputerze
# Panel przeglądarki
Aplikacja na komputerze ma wbudowaną przeglądarkę, dzięki czemu możesz otworzyć dowolną stronę tuż obok czatu, wskazywać elementy, aby o nie zapytać, oraz przechwytywać konsolę strony. Otwórz ją z przycisku globusa w nagłówku aplikacji.
Panel przeglądarki otwiera dowolną stronę tuż obok czatu. Otwórz go przyciskiem globusa w nagłówku aplikacji.
> Przeglądarka na komputerze to funkcja **tylko na komputerze**. W wersji webowej panel [podglądu](/pl/preview/) oferuje te same narzędzia inspekcji i konsoli dla Twojego lokalnego serwera deweloperskiego.
W aplikacji desktopowej to prawdziwa przeglądarka: twoje logowania są zachowywane, przeładowanie na gorąco działa, a narzędzia deweloperskie są o jedno kliknięcie. W karcie przeglądarki panel wyświetli stronę, ale nie zajrzy do jej wnętrza — poniższe narzędzia adnotacji są dostępne tylko na desktopie.
## Inspekcja i adnotacje
Strony otwarte tutaj nie mogą użyć twojej kamery, mikrofonu ani lokalizacji: takie prośby są odrzucane.
Włącz **inspect** i kliknij dowolny element na stronie. OpenChamber przechwytuje o nim notatkę — czym jest, jakie ma style, gdzie się znajduje, oraz zrzut ekranu — i dołącza ją do Twojej wiadomości czatu. To najszybszy sposób, by powiedzieć agentowi „ten element, dokładnie tutaj”.
## Pasek narzędzi
## Przechwytywanie konsoli
Pasek adresu pamięta strony otwierane w tym projekcie i podpowiada je podczas pisania, dopasowując fragment adresu albo tytułu strony. Strzałki przechodzą po liście, Enter otwiera podświetloną pozycję, a przycisk w wierszu usuwa ją z listy.
Przeglądarka zbiera wynik konsoli strony — błędy, ostrzeżenia i logi — dzięki czemu możesz go filtrować i czytać bez otwierania narzędzi deweloperskich.
Obok jest **odświeżenie**, a także **twarde odświeżenie**, które pomija pamięć podręczną, gdy zmiana uparcie się nie pokazuje, oraz sterowanie powiększeniem działające tylko na stronę.
**Wyczyść ciasteczka** i **Wyczyść dane w pamięci podręcznej** dotyczą wyłącznie tego panelu. Twoja sesja OpenChamber i pozostałe okna zostają nietknięte.
## Dodawanie adnotacji
Naciśnij **Adnotuj** — nad stroną pojawi się pasek z trzema narzędziami:
- **Element** — kliknij element. Kliknięcie innego przenosi zaznaczenie, ponowne kliknięcie tego samego je zdejmuje.
- **Obszar** — obrysuj fragment ramką, gdy chodzi o więcej niż jeden element.
- **Rysuj** — szkicuj odręcznie po stronie.
Napisz, czego oczekujesz, w polu obok swojego oznaczenia i naciśnij **Dołącz** — albo po prostu Enter. Do wiadomości trafi karta ze wszystkim, co zaznaczyłeś, z twoją notatką i ze zrzutem widocznej strony z naniesionymi oznaczeniami — możesz więc powiedzieć „ten przycisk, trochę bardziej zaokrąglony" zamiast opisywać, gdzie jest.
Sama strona nie jest zmieniana — adnotacja tylko oznacza to, co już tam jest. `Esc` anuluje i zamyka pasek.
## Sterowanie przez agenta
Agent może sam korzystać z panelu przeglądarki — otworzyć stronę, odczytać jej zawartość, klikać, wpisywać tekst, przewijać i przełączać układ mobilny, tabletowy i desktopowy — żeby sprawdzić własną pracę zamiast prosić o to ciebie. Zobaczysz to w panelu.
Agent nie może uruchamiać dowolnego kodu na stronie. Przeglądarka zachowuje twoje prawdziwe logowania, więc ogranicza się do powyższych działań.
Może też zapisać obraz tego, co widzi, w `.openchamber/screenshots/` w twoim projekcie i pokazać go w odpowiedzi. To właśnie umożliwia porównanie przed i po, a plik zostaje, by dołączyć go do pull requesta.
Działania w przeglądarce to **narzędzie OpenChamber Web**, które włącza się i wyłącza osobno w **Ustawienia → Ogólne → Narzędzia OpenChamber**.
Wymaga to aplikacji desktopowej: stroną pokazaną w karcie przeglądarki nie da się sterować.
## Narzędzia deweloperskie
Naciśnij przycisk terminala na pasku, aby otworzyć własne narzędzia deweloperskie Chromium dla strony — konsolę, sieć, elementy, wszystko czego oczekujesz.
## Powiązane
- [Podgląd i serwery deweloperskie](/pl/preview/) — te same narzędzia dla Twojego lokalnego serwera deweloperskiego
- [Podgląd i serwery deweloperskie](/preview/) — otwieranie działającej aplikacji, także na zdalnym komputerze
- [Narzędzie sterowania dla agentów](/pl/agent-control-tool/) — sesje, worktree i zaplanowane zadania prosto z czatu
+17 -14
View File
@@ -5,28 +5,31 @@ description: Otwórz działający serwer deweloperski wewnątrz OpenChamber.
# Podgląd i serwery deweloperskie
Gdy uruchamiasz serwer deweloperski, OpenChamber może otworzyć go bezpośrednio w aplikacji zamiast w osobnej karcie przeglądarki — dzięki czemu widzisz swoją witrynę obok czatu, przechwytujesz jej konsolę i wskazujesz elementy, aby o nie zapytać.
Gdy uruchamiasz serwer deweloperski, OpenChamber może otworzyć go od razu w aplikacji zamiast w osobnej karcie przeglądarki — widzisz swoją stronę obok czatu i możesz wskazywać elementy, żeby o nie zapytać.
## Otwórz podgląd
## Otwieranie serwera deweloperskiego
OpenChamber obserwuje wynik terminala pod kątem adresu lokalnego (wiersz `Local:`, który drukują narzędzia takie jak Vite, Next.js czy Astro). Gdy go wykryje:
Otwórz panel przeglądarki przyciskiem globusa w nagłówku aplikacji. Jeśli serwer już działa, znajdziesz go na liście i otworzysz jednym kliknięciem — OpenChamber rozpoznaje go po tym, co faktycznie nasłuchuje na twoim komputerze, więc działa niezależnie od sposobu uruchomienia.
- w terminalu pojawia się przycisk **Open preview**
- [akcja projektu](/pl/project-actions/) z włączonym auto-open otwiera go za Ciebie
- lokalny link w wiadomości czatu również może go otworzyć
Serwer otwiera się też sam, gdy:
Witryna ładuje się w panelu bocznym. Podglądać można wyłącznie adresy lokalne (na Twojej własnej maszynie).
- naciśniesz **Otwórz podgląd** przy lokalnym adresie w terminalu
- [akcja projektu](/project-actions/) z włączonym automatycznym otwieraniem go uruchomi
- klikniesz lokalny odnośnik w wiadomości na czacie
## Konsola i inspekcja
Adres zawsze możesz wpisać ręcznie. Samo `localhost:5173` jest traktowane jako `http://`, więc schematu nie musisz podawać.
W panelu podglądu możesz:
## Praca ze zdalnym OpenChamber
- obserwować **konsolę** strony — błędy, ostrzeżenia i logi, filtrowane wedle uznania
- włączyć **inspect**, kliknąć dowolny element i wysłać o nim notatkę — selektor, style, pozycję i zrzut ekranu — prosto do czatu
Gdy OpenChamber działa na innym komputerze, jego serwer deweloperski też jest na *tamtym* komputerze — `localhost` na twoim laptopie prowadzi zupełnie gdzie indziej. Aplikacja desktopowa załatwia to za ciebie: otwiera lokalny port, którym prowadzi połączenie do zdalnego serwera, dzięki czemu strona ładuje się normalnie, z działającym przeładowaniem na gorąco i narzędziami deweloperskimi. Nadal wpisujesz adres, którego się spodziewasz; technikalia nie wchodzą ci w drogę.
To najszybszy sposób, by powiedzieć agentowi „ten przycisk, tutaj”, bez opisywania go.
Wymaga to aplikacji desktopowej. W karcie przeglądarki otworzysz tylko serwery na własnym komputerze.
## Adnotacje na stronie
O wskazywaniu elementów, rysowaniu po stronie, przymierzaniu zmian stylów i wysyłaniu tego wszystkiego na czat przeczytasz w [Panelu przeglądarki](/desktop-browser/).
## Powiązane
- [Akcje projektu](/pl/project-actions/) — automatycznie otwórz serwer przy uruchomieniu
- [Przeglądarka na komputerze](/pl/desktop-browser/) — te same narzędzia dla dowolnej strony, na komputerze
- [Akcje projektu](/project-actions/) — automatyczne otwarcie serwera po uruchomieniu
- [Panel przeglądarki](/desktop-browser/) — adnotacje stron i sterowanie przez agenta
@@ -12,7 +12,7 @@ Aby pisać własne skille, zobacz [Skille](/pl/skills/).
## Zainstaluj skill
1. Otwórz katalog.
2. Przeglądaj wbudowane źródła — repozytorium skilli Anthropic oraz rejestr społeczności ClawdHub — albo wyszukaj.
2. Przeglądaj wbudowane źródła — repozytorium skilli Anthropic oraz rejestr społeczności ClawHub — albo wyszukaj.
3. Wybierz skill i zainstaluj go.
4. Wybierz, gdzie go zainstalować: dla wszystkiego, co robisz, albo tylko dla bieżącego projektu.
+16 -13
View File
@@ -5,28 +5,31 @@ description: Open a running dev server inside OpenChamber.
# Preview & Dev Servers
When you start a dev server, OpenChamber can open it right inside the app instead of a separate browser tab — so you can see your site next to the chat, capture its console, and point at elements to ask about them.
When you start a dev server, OpenChamber can open it right inside the app instead of a separate browser tab — so you can see your site next to the chat and point at elements to ask about them.
## Open a preview
## Open a dev server
OpenChamber watches terminal output for a local address (the `Local:` line that tools like Vite, Next.js, or Astro print). When it spots one:
Open the browser panel from the globe button in the app header. If a dev server is already running, it is listed there and one click opens it — OpenChamber finds it by looking at what is actually listening on your machine, so it works no matter how you started it.
- in the terminal, an **Open preview** button appears
- a [project action](/project-actions/) with auto-open turned on opens it for you
- a local link in a chat message can open it too
A dev server also opens automatically when:
The site loads in the side panel. Only local addresses (on your own machine) can be previewed.
- you press **Open preview** on a local address in the terminal
- a [project action](/project-actions/) with auto-open turned on starts one
- you follow a local link in a chat message
## Console and inspect
You can always type an address yourself. A bare `localhost:5173` is treated as `http://`, so you do not have to type the scheme.
In the preview panel you can:
## Working with a remote OpenChamber
- watch the page's **console** — errors, warnings, and logs, filtered however you like
- turn on **inspect**, click any element, and send a note about it — selector, styles, position, and a screenshot — straight into chat
When OpenChamber runs on another machine, its dev server is on *that* machine — `localhost` on your laptop is somewhere else entirely. The desktop app handles this for you: it opens a local port that carries the connection through to the remote dev server, so the page loads normally, with working hot reload and developer tools. You keep typing the address you expect; the plumbing stays out of your way.
This is the fastest way to tell the agent "this button, here" without describing it.
This needs the desktop app. In a web browser tab, only dev servers on your own machine can be opened.
## Annotate the page
See [Browser Panel](/desktop-browser/) for pointing at elements, drawing on the page, and sending it all to chat.
## Related
- [Project Actions](/project-actions/) — auto-open a server when you start it
- [Desktop Browser](/desktop-browser/) — the same tools for any page, on desktop
- [Browser Panel](/desktop-browser/) — annotating pages and letting the agent drive
@@ -28,7 +28,7 @@ A ferramenta pode listar projetos e preferências de modelos, criar e continuar
## Ativar ou desativar a ferramenta
Abra **Configurações → Geral → OpenCode CLI**, altere **Ferramenta de controle para agentes** e selecione **Save + Reload**. A configuração entra em vigor depois que o servidor OpenCode gerenciado reinicia.
Abra **Configurações → Geral → Ferramentas do OpenChamber** e altere **Ferramenta de controle para agentes**. A configuração entra em vigor quando o servidor OpenCode gerenciado reinicia, o que o OpenChamber oferece como **Apply & Restart**.
A ferramenta não está disponível quando o OpenChamber se conecta a um servidor OpenCode externo por `OPENCODE_HOST` ou skip-start, nem na extensão do VS Code. As instalações desktop e web que usam o servidor OpenCode gerenciado pelo OpenChamber têm suporte automático.
@@ -37,3 +37,4 @@ A ferramenta não está disponível quando o OpenChamber se conecta a um servido
- [Tarefas agendadas](/pt-br/scheduled-tasks/)
- [Sessões de worktree](/pt-br/worktrees/)
- [Objetivos de sessão](/pt-br/session-goals/)
- [Painel do navegador](/pt-br/desktop-browser/) — a ferramenta OpenChamber Web, para ver uma página e conduzi-la
@@ -1,22 +1,53 @@
---
title: Navegador no Desktop
description: Navegue por qualquer página dentro do app de desktop, com inspeção e captura de console.
title: Painel do navegador
description: Navegue em qualquer página dentro do aplicativo, anote-a e deixe o agente conduzi-la.
---
# Navegador no Desktop
# Painel do navegador
O app de desktop tem um navegador integrado para você abrir qualquer página logo ao lado do seu chat, apontar para elementos para perguntar sobre eles e capturar o console da página. Abra-o pelo botão de globo no cabeçalho do app.
O painel do navegador abre qualquer página bem ao lado do seu chat. Abra-o pelo botão do globo no cabeçalho.
> O navegador no desktop é um recurso **apenas para desktop**. Na web, o painel de [preview](/pt-br/preview/) oferece as mesmas ferramentas de inspeção e console para o seu servidor de desenvolvimento local.
No aplicativo desktop é um navegador de verdade: seus logins persistem, a recarga a quente funciona e as ferramentas de desenvolvedor estão a um clique. Em uma aba do navegador o painel exibe a página, mas não consegue olhar dentro dela — as ferramentas de anotação abaixo são exclusivas do desktop.
## Inspecionar e anotar
Páginas abertas aqui não podem usar sua câmera, seu microfone nem sua localização: esses pedidos são recusados.
Ative o **inspect** e clique em qualquer elemento da página. O OpenChamber captura uma nota sobre ele — o que é, seus estilos, onde fica e uma captura de tela — e a anexa à sua mensagem no chat. É a forma mais rápida de dizer ao agente "este elemento, bem aqui".
## A barra de ferramentas
## Captura de console
A barra de endereço lembra as páginas que você abriu neste projeto e as oferece enquanto você digita, procurando por parte do endereço ou do título da página. As setas percorrem a lista, Enter abre o item destacado e o botão da linha o remove.
O navegador coleta a saída do console da página — erros, avisos e logs — para que você possa filtrá-la e lê-la sem abrir as ferramentas de desenvolvedor.
Ao lado fica **Recarregar**, junto de uma **recarga forçada** que ignora o cache quando uma mudança teima em não aparecer, e os controles de zoom, que ampliam apenas a página.
**Limpar cookies** e **Limpar dados em cache** valem só para este painel. Sua sessão do OpenChamber e as outras janelas ficam intactas.
## Anotar uma página
Pressione **Anotar** e uma barra aparece sobre a página com três ferramentas:
- **Elemento** — clique em um elemento. Clicar em outro move a seleção; clicar no mesmo de novo a remove.
- **Região** — arraste um retângulo em volta de uma área quando o assunto envolver mais de um elemento.
- **Desenhar** — rabisque à mão livre sobre a página.
Escreva o que quer no campo que aparece ao lado da sua marcação e pressione **Anexar** — ou apenas Enter. Sua mensagem ganha um cartão com tudo o que você marcou, sua nota e uma captura da página visível com suas marcações desenhadas nela — assim você pode dizer "este botão, um pouco mais arredondado" em vez de descrever onde ele está.
A página em si nunca é modificada — anotar apenas marca o que já está lá. `Esc` cancela e fecha a barra.
## Deixar o agente conduzir
O agente pode usar o painel do navegador sozinho — abrir uma página, ler o que há nela, clicar, digitar, rolar e alternar entre layout móvel, de tablet e de desktop — para conferir o próprio trabalho em vez de pedir isso a você. Você vê acontecendo no painel.
O agente não pode executar código arbitrário na página. O navegador guarda seus logins reais, então ele fica limitado às ações acima.
Ele também pode salvar uma imagem do que está vendo em `.openchamber/screenshots/` no seu projeto e mostrá-la na resposta. É isso que torna possível um antes e depois, e o arquivo continua lá para anexar a um pull request.
As ações do navegador são a **ferramenta OpenChamber Web**, que pode ser ligada e desligada por conta própria em **Configurações → Geral → Ferramentas do OpenChamber**.
Isso exige o aplicativo de desktop: uma página exibida numa aba do navegador não pode ser controlada.
## Ferramentas de desenvolvedor
Pressione o botão de terminal na barra para abrir as ferramentas de desenvolvedor do próprio Chromium para a página — console, rede, elementos, tudo o que você espera.
## Relacionado
- [Preview e Servidores de Desenvolvimento](/pt-br/preview/) — as mesmas ferramentas para o seu servidor de desenvolvimento local
- [Pré-visualização e servidores de desenvolvimento](/preview/) — abrir seu aplicativo em execução, inclusive em uma máquina remota
- [Ferramenta de controle para agentes](/pt-br/agent-control-tool/) — sessões, worktrees e tarefas agendadas pelo chat
+19 -16
View File
@@ -1,32 +1,35 @@
---
title: Preview e Servidores de Desenvolvimento
title: Pré-visualização e servidores de desenvolvimento
description: Abra um servidor de desenvolvimento em execução dentro do OpenChamber.
---
# Preview e Servidores de Desenvolvimento
# Pré-visualização e servidores de desenvolvimento
Quando você inicia um servidor de desenvolvimento, o OpenChamber pode abri-lo logo dentro do app em vez de uma aba separada do navegador — para que você veja seu site ao lado do chat, capture seu console e aponte para elementos para perguntar sobre eles.
Quando você sobe um servidor de desenvolvimento, o OpenChamber pode abri-lo dentro do próprio aplicativo em vez de uma aba separada — assim você vê seu site ao lado do chat e pode apontar para elementos para perguntar sobre eles.
## Abrir um preview
## Abrir um servidor de desenvolvimento
O OpenChamber observa a saída do terminal em busca de um endereço local (a linha `Local:` que ferramentas como Vite, Next.js ou Astro imprimem). Quando ele detecta um:
Abra o painel do navegador pelo botão do globo no cabeçalho. Se já houver um servidor rodando, ele aparece na lista e um clique o abre: o OpenChamber o encontra olhando o que está de fato escutando na sua máquina, então funciona independentemente de como você o iniciou.
- no terminal, aparece um botão **Open preview**
- uma [ação de projeto](/pt-br/project-actions/) com auto-open ativado o abre para você
- um link local em uma mensagem do chat também pode abri-lo
Um servidor também abre sozinho quando:
O site carrega no painel lateral. Apenas endereços locais (na sua própria máquina) podem ter preview.
- você pressiona **Abrir pré-visualização** em um endereço local no terminal
- uma [ação de projeto](/project-actions/) com abertura automática inicia um
- você segue um link local em uma mensagem do chat
## Console e inspeção
Você sempre pode digitar o endereço à mão. Um simples `localhost:5173` é entendido como `http://`, então não precisa escrever o esquema.
No painel de preview você pode:
## Trabalhando com um OpenChamber remoto
- acompanhar o **console** da página — erros, avisos e logs, filtrados como você quiser
- ativar o **inspect**, clicar em qualquer elemento e enviar uma nota sobre ele — seletor, estilos, posição e uma captura de tela — direto no chat
Quando o OpenChamber roda em outra máquina, o servidor de desenvolvimento está *nessa* máquina — `localhost` no seu notebook aponta para outro lugar completamente. O aplicativo desktop resolve isso: ele abre uma porta local que leva a conexão até o servidor remoto, de modo que a página carrega normalmente, com recarga a quente e ferramentas de desenvolvedor funcionando. Você continua digitando o endereço que espera; o encanamento não atrapalha.
Esta é a forma mais rápida de dizer ao agente "este botão, aqui" sem descrevê-lo.
Isso exige o aplicativo desktop. Em uma aba do navegador, só é possível abrir servidores da sua própria máquina.
## Anotar a página
Veja [Painel do navegador](/desktop-browser/) para apontar elementos, desenhar sobre a página e mandar tudo para o chat.
## Relacionado
- [Ações de Projeto](/pt-br/project-actions/) — abra um servidor automaticamente ao iniciá-lo
- [Navegador no Desktop](/pt-br/desktop-browser/) — as mesmas ferramentas para qualquer página, no desktop
- [Ações de projeto](/project-actions/) — abrir um servidor automaticamente ao iniciá-lo
- [Painel do navegador](/desktop-browser/) — anotar páginas e deixar o agente conduzir
@@ -12,7 +12,7 @@ Para escrever suas próprias skills, veja [Skills](/pt-br/skills/).
## Instalar uma skill
1. Abra o catálogo.
2. Navegue pelas fontes integradas — o repositório de skills da Anthropic e o registro comunitário ClawdHub — ou pesquise.
2. Navegue pelas fontes integradas — o repositório de skills da Anthropic e o registro comunitário ClawHub — ou pesquise.
3. Escolha uma skill e instale-a.
4. Escolha onde instalá-la: para tudo o que você faz, ou apenas no projeto atual.
@@ -12,7 +12,7 @@ For writing your own skills, see [Skills](/skills/).
## Install a skill
1. Open the catalog.
2. Browse the built-in sources — the Anthropic skills repo and the ClawdHub community registry — or search.
2. Browse the built-in sources — the Anthropic skills repo and the ClawHub community registry — or search.
3. Pick a skill and install it.
4. Choose where to install it: for everything you do, or just the current project.
@@ -28,7 +28,7 @@ description: Дозвольте агенту керувати сесіями, wo
## Увімкнення та вимкнення
Відкрийте **Налаштування → Загальні → OpenCode CLI**, змініть **Інструмент керування для агентів**, потім виберіть **Save + Reload**. Налаштування застосовується після перезапуску керованого сервера OpenCode.
Відкрийте **Налаштування → Загальні → Інструменти OpenChamber** і змініть **Інструмент керування для агентів**. Налаштування застосовується після перезапуску керованого сервера OpenCode, який OpenChamber запропонує як **Apply & Restart**.
Інструмент недоступний, коли OpenChamber підключається до зовнішнього сервера OpenCode через `OPENCODE_HOST` чи skip-start, а також у розширенні VS Code. Десктопні та вебінсталяції з керованим OpenChamber сервером OpenCode підтримують його автоматично.
@@ -37,3 +37,4 @@ description: Дозвольте агенту керувати сесіями, wo
- [Заплановані задачі](/uk/scheduled-tasks/)
- [Сесії worktree](/uk/worktrees/)
- [Цілі сесії](/uk/session-goals/)
- [Панель браузера](/uk/desktop-browser/) — інструмент OpenChamber Web — дивитися на сторінку й керувати нею
@@ -1,22 +1,64 @@
---
title: Десктопний браузер
description: Переглядайте будь-яку сторінку всередині десктопного застосунку, з інспекцією та перехопленням консолі.
title: Панель браузера
description: Переглядайте будь-яку сторінку в застосунку, анотуйте її та дозвольте агенту нею керувати.
---
# Десктопний браузер
# Панель браузера
Десктопний застосунок має вбудований браузер, тож ви можете відкрити будь-яку сторінку просто поруч із чатом, вказувати на елементи, щоб запитати про них, і перехоплювати консоль сторінки. Відкрийте його з кнопки глобуса в заголовку застосунку.
Панель браузера відкриває будь-яку сторінку просто поруч із чатом. Відкрийте її кнопкою глобуса в заголовку застосунку.
> Десктопний браузер — це функція **лише для десктопа**. У вебі панель [перегляду](/uk/preview/) пропонує ті самі інструменти інспекції та консолі для вашого локального dev-сервера.
У десктопному застосунку це справжній браузер: ваші входи зберігаються, гаряче перезавантаження працює, а інструменти розробника — за один клік. У вкладці веббраузера панель теж покаже сторінку, але не зможе зазирнути всередину — інструменти анотацій нижче доступні лише на десктопі.
## Інспекція та анотування
Сторінки, відкриті тут, не можуть скористатися вашою камерою, мікрофоном чи місцем перебування: такі запити відхиляються.
Увімкніть **inspect** і клікніть будь-який елемент на сторінці. OpenChamber перехоплює нотатку про нього — що це, його стилі, де він розташований, і знімок екрана — і прикріплює її до вашого повідомлення в чаті. Це найшвидший спосіб сказати агентові «ось цей елемент, прямо тут».
## Панель інструментів
## Перехоплення консолі
Адресний рядок памʼятає сторінки, які ви відкривали в цьому проєкті, і пропонує їх під час набору, звіряючись із частиною адреси або назвою сторінки. Стрілки рухають списком, Enter відкриває підсвічений запис, а кнопка в рядку прибирає його зі списку.
Браузер збирає вивід консолі сторінки — помилки, попередження та логи — щоб ви могли фільтрувати й читати його, не відкриваючи інструменти розробника.
Поруч — **перезавантаження**, а також **жорстке перезавантаження**, яке ігнорує кеш, коли зміна вперто не показується, і керування масштабом, що змінює лише сторінку.
## Пов'язане
**Очистити куки** та **Очистити кеш** стосуються тільки цієї панелі. Вашої сесії OpenChamber та інших вікон це не торкається.
- [Перегляд і dev-сервери](/uk/preview/) — ті самі інструменти для вашого локального dev-сервера
## Анотувати сторінку
Натисніть **Анотувати** — над сторінкою зʼявиться панель із трьома інструментами:
- **Елемент** — клікніть на елемент. Клік по іншому переносить вибір, повторний клік по тому самому — знімає його.
- **Область** — обведіть ділянку рамкою, коли йдеться більш ніж про один елемент.
- **Малювання** — малюйте від руки поверх сторінки.
Опишіть бажане в полі, що зʼявляється поруч із позначкою, і натисніть **Додати** — або просто Enter. У повідомленні чату зʼявиться картка з усім, що ви позначили, з вашою нотаткою і зі знімком видимої сторінки, на якому намальовано твої позначки — тож можна сказати «оця кнопка, трохи круглішу», а не описувати, де вона.
Сама сторінка при цьому не змінюється — анотація лише позначає те, що є. `Esc` скасовує й закриває панель.
## Дозволити агенту керувати
Агент може сам користуватися панеллю браузера — відкривати сторінку, читати, що на ній, клікати, вводити текст, прокручувати й перемикатися між мобільним, планшетним і десктопним розкладами — щоб перевіряти власну роботу, а не просити про це вас. Ви бачитимете це в панелі.
Агент не може виконувати довільний код на сторінці. Браузер зберігає ваші справжні входи, тож агент обмежений переліченими діями.
Він також може зберегти знімок того, що бачить, у `.openchamber/screenshots/` вашого проєкту й показати його у відповіді. Саме це робить можливим «до і після», а файл лишається на місці, щоб потім прикріпити його до pull request.
Дії з браузером — це **інструмент OpenChamber Web**, який вмикається й вимикається окремо в **Налаштування → Загальні → Інструменти OpenChamber**.
Для цього потрібен десктопний застосунок: сторінкою у вкладці веббраузера керувати не вийде.
## Розмір і оформлення
Натисніть кнопку телефона, щоб відкрити панель пристроїв. Оберіть пресет або
введіть ширину й висоту — сторінка буде викладена саме в цьому розмірі, а якщо
не вміщається в панель, її буде зменшено візуально. Сама сторінка при цьому
вимірює себе в тому розмірі, який ви задали.
Там же можна змусити сторінку показатися світлою чи темною, не змінюючи нічого
на своїй машині. З DevTools вони не поєднуються: у сторінки може бути лише один
приєднаний зневаджувач, тож DevTools доведеться спершу закрити.
## Інструменти розробника
Натисніть кнопку термінала на панелі, щоб відкрити власні інструменти розробника Chromium для сторінки — консоль, мережу, елементи, усе як зазвичай.
## Повʼязане
- [Перегляд і dev-сервери](/preview/) — як відкрити запущений застосунок, зокрема на віддаленій машині
- [Інструмент керування для агентів](/uk/agent-control-tool/) — сесії, worktree й заплановані задачі просто з чату
+18 -15
View File
@@ -5,28 +5,31 @@ description: Відкрийте запущений dev-сервер усеред
# Перегляд і dev-сервери
Коли ви запускаєте dev-сервер, OpenChamber може відкрити його просто всередині застосунку замість окремої вкладки браузера — щоб ви могли бачити свій сайт поруч із чатом, перехоплювати його консоль і вказувати на елементи, щоб запитати про них.
Коли ви запускаєте dev-сервер, OpenChamber може відкрити його просто всередині застосунку замість окремої вкладки браузера — щоб ви бачили свій сайт поруч із чатом і могли вказувати на елементи, щоб запитати про них.
## Відкриття перегляду
## Відкрити dev-сервер
OpenChamber слідкує за виводом терміналу на предмет локальної адреси (рядок `Local:`, який друкують інструменти на кшталт Vite, Next.js чи Astro). Коли він її помічає:
Відкрийте панель браузера кнопкою глобуса в заголовку застосунку. Якщо dev-сервер уже запущено, він буде у списку — один клік, і він відкриється. OpenChamber знаходить його за тим, що насправді слухає порти на вашій машині, тож це працює незалежно від того, як саме ви його запустили.
- у терміналі з'являється кнопка **Open preview**
- [дія проєкту](/uk/project-actions/) з увімкненим автовідкриттям відкриває її за вас
- локальне посилання в повідомленні чату теж може її відкрити
Dev-сервер також відкривається автоматично, коли:
Сайт завантажується в бічній панелі. Переглянути можна лише локальні адреси (на вашій власній машині).
- ви натискаєте **Відкрити перегляд** на локальній адресі в терміналі
- [дія проєкту](/project-actions/) з увімкненим автовідкриттям запускає його
- ви переходите за локальним посиланням у повідомленні чату
## Консоль та інспекція
Адресу завжди можна ввести вручну. Простий `localhost:5173` трактується як `http://`, тож схему писати не обовʼязково.
У панелі перегляду ви можете:
## Робота з віддаленим OpenChamber
- спостерігати за **консоллю** сторінки — помилки, попередження та логи, відфільтровані як вам зручно
- увімкнути **inspect**, клікнути будь-який елемент і надіслати нотатку про нього — селектор, стилі, позицію та знімок екрана — прямо в чат
Коли OpenChamber працює на іншій машині, його dev-сервер теж на *тій* машині — `localhost` на вашому ноутбуці веде зовсім не туди. Десктопний застосунок робить це за вас: відкриває локальний порт, який проводить зʼєднання до віддаленого dev-сервера, тож сторінка вантажиться як звичайно, з робочим гарячим перезавантаженням і інструментами розробника. Ви й далі вводите очікувану адресу; технічні деталі вам не заважають.
Це найшвидший спосіб сказати агентові «ось ця кнопка, тут», не описуючи її.
Для цього потрібен десктопний застосунок. У вкладці браузера можна відкрити лише dev-сервери на вашій власній машині.
## Пов'язане
## Анотації на сторінці
- [Дії проєкту](/uk/project-actions/) — автоматично відкривайте сервер при запуску
- [Десктопний браузер](/uk/desktop-browser/) — ті самі інструменти для будь-якої сторінки на десктопі
Про те, як вказувати на елементи, малювати на сторінці й надсилати все це в чат, читайте в розділі [Панель браузера](/desktop-browser/).
## Повʼязане
- [Дії проєкту](/project-actions/) — автовідкриття сервера після запуску
- [Панель браузера](/desktop-browser/) — анотації сторінок і керування агентом
@@ -12,7 +12,7 @@ description: Переглядайте та встановлюйте готові
## Встановлення навички
1. Відкрийте каталог.
2. Перегляньте вбудовані джерела — репозиторій навичок Anthropic та спільнотний реєстр ClawdHub — або скористайтеся пошуком.
2. Перегляньте вбудовані джерела — репозиторій навичок Anthropic та спільнотний реєстр ClawHub — або скористайтеся пошуком.
3. Оберіть навичку й установіть її.
4. Виберіть, куди встановити: для всього, що ви робите, чи лише для поточного проєкту.
@@ -28,7 +28,7 @@ description: 让智能体从聊天中管理 OpenChamber 会话、worktree 和计
## 开启或关闭工具
打开 **设置 → 常规 → OpenCode CLI**,更改 **智能体控制工具**,然后选择 **Save + Reload**。该设置会在托管的 OpenCode 服务器重启后生效。
打开 **设置 → 常规 → OpenChamber 工具**,更改 **智能体控制工具**。该设置会在托管的 OpenCode 服务器重启后生效OpenChamber 会以 **Apply & Restart** 的形式提示重启
当 OpenChamber 通过 `OPENCODE_HOST` 或 skip-start 连接外部 OpenCode 服务器时,或在 VS Code 扩展中,此工具不可用。使用 OpenChamber 托管 OpenCode 服务器的桌面端和 Web 安装会自动支持此工具。
@@ -37,3 +37,4 @@ description: 让智能体从聊天中管理 OpenChamber 会话、worktree 和计
- [计划任务](/zh-cn/scheduled-tasks/)
- [Worktree 会话](/zh-cn/worktrees/)
- [会话目标](/zh-cn/session-goals/)
- [浏览器面板](/zh-cn/desktop-browser/) — 用于查看并操作页面的 OpenChamber Web 工具
@@ -1,22 +1,53 @@
---
title: 桌面浏览器
description: 在桌面应用内浏览任意页面,并提供检查和控制台捕获功能
title: 浏览器面板
description: 在应用内浏览任意页面、为其添加标注,并让智能体操作它
---
# 桌面浏览器
# 浏览器面板
桌面应用内置了浏览器,因此你可以在聊天旁边直接打开任意页面、指向元素来询问相关问题,并捕获页面的控制台。从应用标题栏的地球按钮打开它。
浏览器面板会在聊天旁边打开任意页面。用应用标题栏的地球按钮打开它。
> 桌面浏览器是一项**仅限桌面**的功能。在网页端,[预览](/zh-cn/preview/) 面板为你的本地开发服务器提供相同的检查与控制台工具
在桌面应用中,它是一个真正的浏览器:登录状态会保留,热重载可用,开发者工具一键即达。在浏览器标签页中,面板仍能显示页面,但无法查看其内部——下面的标注工具仅限桌面端
## 检查与标注
在这里打开的页面无法使用你的摄像头、麦克风或位置:这类请求会被拒绝。
开启 **inspect** 并点击页面上的任意元素。OpenChamber 会捕获关于它的说明 — 它是什么、它的样式、它所处的位置,以及一张截图 — 并将其附加到你的聊天消息中。这是告诉智能体“就这个元素,就在这里”的最快方式。
## 工具栏
## 控制台捕获
地址栏会记住你在这个项目里打开过的页面,并在你输入时给出候选,按地址或页面标题的任意片段匹配。方向键在列表中移动,回车打开选中的一项,行上的按钮把它从列表中移除。
浏览器会收集页面的控制台输出 — 错误、警告和日志 — 因此你无需打开开发者工具即可筛选和阅读它
旁边是**重新加载**,还有在改动怎么都不出现时忽略缓存的**强制重新加载**,以及只缩放页面的缩放控件
## 相关内容
**清除 Cookie** 和**清除缓存数据**只作用于这个面板。你的 OpenChamber 会话和其他窗口不受影响。
- [预览与开发服务器](/zh-cn/preview/) — 为你的本地开发服务器提供相同的工具
## 为页面添加标注
按 **标注**,页面上方会出现一个包含三种工具的工具条:
- **元素** — 点击某个元素。点击另一个会移动选择,再次点击同一个则取消。
- **区域** — 当要说的内容不止一个元素时,拖拽出矩形框住那块区域。
- **绘制** — 在页面上自由手绘。
在标记旁出现的输入框里写下你的想法,然后按 **附加**,或直接按回车。你的聊天消息会收到一张卡片,包含你标记的全部内容、你的备注,以及一张画上你的标记的可见页面截图——于是你可以说"这个按钮,再圆一点",而不必描述它在哪里。
页面本身不会被修改——标注只是标记已有的内容。按 `Esc` 取消并关闭工具条。
## 让智能体操作
智能体可以自己使用浏览器面板——打开页面、读取页面内容、点击、输入、滚动,并在移动端、平板和桌面端布局之间切换——从而自行检查工作成果,而不必来问你。你可以在面板中看到这个过程。
智能体无法在页面中执行任意代码。浏览器保留着你真实的登录状态,因此它只能执行上述操作。
它还可以把当前看到的画面保存为图片,放进项目里的 `.openchamber/screenshots/`,并在回复中展示给你。「改动前后」的对比正是靠这一点,而文件会留在那里,方便附到 pull request 上。
浏览器相关的动作属于 **OpenChamber Web 工具**,可在**设置 → 通用 → OpenChamber 工具**中单独开关。
这需要桌面应用:在浏览器标签页中显示的页面无法被操作。
## 开发者工具
按工具条上的终端按钮,即可打开 Chromium 自带的页面开发者工具——控制台、网络、元素,一应俱全。
## 相关
- [预览与开发服务器](/preview/) — 打开正在运行的应用,包括远程机器上的
- [智能体控制工具](/zh-cn/agent-control-tool/) — 在聊天中管理会话、worktree 和计划任务
+19 -16
View File
@@ -1,32 +1,35 @@
---
title: 预览与开发服务器
description: 在 OpenChamber 内打开正在运行的开发服务器。
description: 在 OpenChamber 内打开正在运行的开发服务器。
---
# 预览与开发服务器
当你启动开发服务器OpenChamber 可以直接在应用内打开它,而不是在单独的浏览器标签页中 — 这样你就可以在聊天旁边查看你的站点、捕获它的控制台,并指元素来询问相关问题
启动开发服务器OpenChamber 可以直接在应用内打开它,而不是另开一个浏览器标签页——这样你就在聊天旁边看到自己的站点,并指元素提问
## 打开预览
## 打开开发服务器
OpenChamber 会监视终端输出中的本地地址(像 Vite、Next.js 或 Astro 这类工具打印的 `Local:` 行)。当它发现一个时:
用应用标题栏的地球按钮打开浏览器面板。如果开发服务器已在运行,它会出现在列表里,点一下即可打开:OpenChamber 是根据你机器上实际正在监听的端口找到它的,因此无论你用什么方式启动都能识别。
- 终端中会出现一个 **Open preview** 按钮
- 一个开启了自动打开的 [项目操作](/zh-cn/project-actions/) 会为你打开它
- 聊天消息中的本地链接也可以打开它
以下情况开发服务器也会自动打开:
站点会在侧面板中加载。只有本地地址(在你自己的机器上)才能被预览。
- 你在终端的本地地址上按 **打开预览**
- 开启了自动打开的[项目操作](/project-actions/)启动了一个
- 你点击了聊天消息中的本地链接
## 控制台与检查
你随时可以自己输入地址。直接写 `localhost:5173` 会按 `http://` 处理,不必输入协议。
在预览面板中你可以:
## 配合远程 OpenChamber 使用
- 查看页面的**控制台** — 错误、警告和日志,可按你喜欢的方式筛选
- 开启 **inspect**,点击任意元素,并将关于它的说明 — 选择器、样式、位置和一张截图 — 直接发送到聊天
当 OpenChamber 运行在另一台机器上时,它的开发服务器也在*那台*机器上——你笔记本上的 `localhost` 指向的完全是别处。桌面应用会替你处理:它打开一个本地端口,把连接一路接到远程开发服务器,于是页面照常加载,热重载和开发者工具都能用。你仍然输入你预期的地址,底层的管道不会碍事。
是告诉智能体“就这个按钮,这里”而无需描述它的最快方式
需要桌面应用。在浏览器标签页中,只能打开你自己机器上的开发服务器
## 相关内容
## 为页面添加标注
- [项目操作](/zh-cn/project-actions/) — 启动服务器时自动打开它
- [桌面浏览器](/zh-cn/desktop-browser/) — 在桌面端为任意页面提供相同的工具
指向元素、在页面上绘制并把这些一起发到聊天的方法,见[浏览器面板](/desktop-browser/)。
## 相关
- [项目操作](/project-actions/) — 启动服务器时自动打开
- [浏览器面板](/desktop-browser/) — 标注页面并让智能体操作
@@ -12,7 +12,7 @@ Skills 目录让你能够安装其他人发布的 skill,而不必自己编写
## 安装 skill
1. 打开目录。
2. 浏览内置来源 — Anthropic skills 仓库和 ClawdHub 社区注册表 — 或进行搜索。
2. 浏览内置来源 — Anthropic skills 仓库和 ClawHub 社区注册表 — 或进行搜索。
3. 选择一个 skill 并安装它。
4. 选择安装位置:用于你的所有工作,或仅用于当前项目。
+10 -10
View File
@@ -638,18 +638,18 @@
}
},
{
"label": "Desktop Browser",
"label": "Browser Panel",
"link": "/desktop-browser/",
"translations": {
"uk": "Десктопний браузер",
"zh-CN": "桌面浏览器",
"es": "Navegador de escritorio",
"pt-BR": "Navegador desktop",
"ko": "데스크톱 브라우저",
"pl": "Przeglądarka na pulpicie",
"fr": "Navigateur desktop",
"ja": "デスクトップブラウザ",
"de": "Desktop-Browser"
"uk": "Панель браузера",
"zh-CN": "浏览器面板",
"es": "Panel del navegador",
"pt-BR": "Painel do navegador",
"ko": "브라우저 패널",
"pl": "Panel przeglądarki",
"fr": "Panneau navigateur",
"ja": "ブラウザパネル",
"de": "Browser-Panel"
}
},
{
+10 -1
View File
@@ -44,7 +44,7 @@ bun run electron:dev
The Electron workspace package trusts Electron's install script so `bun install` downloads the platform runtime in fresh checkouts and worktrees.
Electron's postinstall (`node install.js`) is run by `bun install` with the system Node. Under Node 24, the bundled `extract-zip@2.0.1` silently unpacks only the first entry of the Electron zip, leaving `dist/` without the binary and `path.txt` missing. To keep this from blocking desktop work:
Electron's postinstall (`node install.js`) is run by `bun install` with the system Node. Older Electron releases bundled `extract-zip@2.0.1`, which under Node 24 silently unpacked only the first entry of the Electron zip, leaving `dist/` without the binary and `path.txt` missing. Electron 43+ ships its own fixed extractor (`@electron-internal/extract-zip`), but to keep interrupted or wrong-architecture installs from blocking desktop work:
- The root `postinstall` runs `ensure-electron.mjs --best-effort`, which detects an incomplete Electron install (missing binary, stale `dist/version`/`path.txt`, or a binary of the wrong architecture) and repairs it by re-running the postinstall under Bun (which extracts correctly), falling back to Node.
- `electron-dev.mjs` runs the same check (fail-fast, not best-effort) before launching, so `bun run electron:dev` self-heals even when an install was interrupted.
@@ -104,6 +104,8 @@ A loopback-only updater fixture is available for contributor QA of N-to-N+1 AppI
The package supports macOS, Windows, and Linux desktop features. Linux AppImage builds include in-app window controls, auto-update, system tray (right-click Show / Hide / Close), and launch-at-login (XDG autostart). Opening files in installed apps, installed-app discovery, and FreeDesktop icon lookup (including the default file manager) work on macOS, Windows, and Linux.
On Windows and Linux, the General setting persisted as `desktopMinimizeToTrayEnabled` keeps the app running in the tray when the main window is **closed**. Minimize — the in-app control, the native title-bar button, and the taskbar — always performs a normal window minimize, so the taskbar entry stays available.
The macOS menu bar item is enabled by default and can be disabled in General settings. The setting applies after restart; while disabled, Desktop does not create the native tray controller or start the renderer subscriptions, polling, quota refresh, or IPC updates that feed it.
## Bundled OpenCode CLI
@@ -151,6 +153,13 @@ Use an explicit override when testing a different OpenCode CLI build or when a u
- SSH uses OpenSSH ControlMaster on macOS/Linux. Windows uses independent hidden OpenSSH processes for setup commands and each long-lived forward because Win32 OpenSSH does not support ControlMaster reliably.
- Tunnel lifecycle integration through the web server runtime.
- Auto-update checks, downloads, and restart/apply flow.
- The browser panel's own session (`persist:openchamber-browser`): its storage is
cleared only through the scoped clear-data command, and camera, microphone,
location, and device-picker requests from pages shown there are denied. Electron
grants permission requests by default when no handler is set, and the panel
loads whatever address the user types. Tab favicons are fetched in this
session too, so icons behind the page's own login resolve and the app's origin
never requests anything from a third-party host.
## IPC Pattern
+5 -7
View File
@@ -8,7 +8,7 @@ const DEFAULT_XDG_DATA_DIRS = ['/usr/local/share', '/usr/share'];
const TARGET_FIELD_CODES = new Set(['f', 'F', 'u', 'U']);
const TERMINAL_APP_IDS = new Set(['terminal', 'iterm2', 'ghostty']);
export const LINUX_CLI_BY_APP_ID = {
const LINUX_CLI_BY_APP_ID = {
vscode: 'code',
cursor: 'cursor',
vscodium: 'codium',
@@ -43,7 +43,7 @@ const normalizeComparable = (value) => String(value || '')
.trim();
const normalizeCompactComparable = (value) => normalizeComparable(value).replace(/\s+/g, '');
export const stripDesktopExecFieldCodes = (execValue) => String(execValue || '')
const stripDesktopExecFieldCodes = (execValue) => String(execValue || '')
.replace(/%%/g, '\^@')
.replace(/%[fFuUdDnNickvm]/g, '')
.replace(/%./g, '')
@@ -142,9 +142,7 @@ export const readLinuxDesktopEntries = async (options = {}) => {
return entries.sort((left, right) => left.name.localeCompare(right.name));
};
export const discoverLinuxDesktopApps = readLinuxDesktopEntries;
export const desktopEntryMatchesApp = (entry, appName, appId = '') => {
const desktopEntryMatchesApp = (entry, appName, appId = '') => {
const needles = uniqueStrings([appName, appId]).flatMap((value) => [normalizeComparable(value), normalizeCompactComparable(value)]).filter(Boolean);
const haystacks = [entry.name, entry.id, path.basename(entry.filePath || ''), entry.exec]
.flatMap((value) => [normalizeComparable(value), normalizeCompactComparable(value)]);
@@ -331,7 +329,7 @@ const pathExistsSync = (candidate) => {
}
};
export const linuxIconThemeDirs = ({ env = process.env, homeDir = os.homedir() } = {}) => {
const linuxIconThemeDirs = ({ env = process.env, homeDir = os.homedir() } = {}) => {
const dataHome = typeof env.XDG_DATA_HOME === 'string' && env.XDG_DATA_HOME.trim()
? env.XDG_DATA_HOME.trim()
: path.join(homeDir || os.homedir(), '.local', 'share');
@@ -419,7 +417,7 @@ export const resolveLinuxIconFile = (iconName, options = {}) => {
return null;
};
export const resolveDefaultLinuxFileManagerId = ({ env = process.env, execFileSyncImpl = execFileSync } = {}) => {
const resolveDefaultLinuxFileManagerId = ({ env = process.env, execFileSyncImpl = execFileSync } = {}) => {
try {
const output = String(execFileSyncImpl('xdg-mime', ['query', 'default', 'inode/directory'], {
encoding: 'utf8',
+1 -1
View File
@@ -5,7 +5,7 @@ import path from 'node:path';
const AUTOSTART_FILE_NAME = 'openchamber.desktop';
export const resolveLinuxAutostartDirectory = ({
const resolveLinuxAutostartDirectory = ({
env = process.env,
homeDir = os.homedir(),
} = {}) => {
+227 -10
View File
@@ -306,6 +306,9 @@ const readDesktopMinimizeToTrayStatus = () => {
};
};
// Close-to-tray gate. The persisted key is still `desktopMinimizeToTrayEnabled`
// (settings written by earlier versions), but the behavior it controls is the
// window close path only; minimize stays a normal taskbar/dock minimize.
const shouldHideMainWindowToTray = (browserWindow) => {
if (process.platform !== 'win32' && process.platform !== 'linux') return false;
if (!state.trayController) return false;
@@ -1129,6 +1132,76 @@ const injectRuntimeConfigIntoHtml = (html) => {
return `${initScript}${html}`;
};
/**
* The browser panel's own session, kept separate from OpenChamber's.
*
* Every page the user opens in the panel shares this partition, which is what
* lets a dev-server login persist between sessions without touching the app's
* own storage.
*/
const BROWSER_PANEL_PARTITION = 'persist:openchamber-browser';
/**
* Denies device and location access to pages shown in the browser panel.
*
* Electron grants permission requests by default when no handler is set. The
* panel loads whatever address the user types, so that default would hand a
* page the camera, the microphone, or the user's location without anything
* being asked or shown a browser people would not tolerate.
*
* This denies rather than prompts: a prompt is the right end state, but a
* silent grant is the one outcome that must not stay. Denials are logged so a
* page that legitimately needs something is diagnosable rather than mysterious.
*/
const MAX_FAVICON_BYTES = 512 * 1024;
const FAVICON_MIME_TYPES = new Set([
'image/x-icon',
'image/vnd.microsoft.icon',
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'image/svg+xml',
]);
/**
* Resolves a web contents id to a browser-panel view, or refuses.
*
* These commands take an id from the renderer, and an id is guessable. Without
* this a compromised renderer could point capture or the debugger at another
* window's contents. Membership of the panel's own session is the proof: only
* views created with that partition have it, and nothing else in the app does.
*/
const resolveBrowserPanelContents = (rawId) => {
const id = Number.isFinite(rawId) ? Math.trunc(rawId) : null;
if (id === null || id < 0) throw new Error('webContentsId is required');
const target = webContents.fromId(id);
if (!target || target.isDestroyed()) throw new Error('WebContents not found');
if (target.session !== session.fromPartition(BROWSER_PANEL_PARTITION)) {
throw new Error('That view is not a browser panel page');
}
return target;
};
const hardenBrowserPanelSession = () => {
const panelSession = session.fromPartition(BROWSER_PANEL_PARTITION);
panelSession.setPermissionRequestHandler((_contents, permission, callback, details) => {
log.info('[electron] browser panel denied a permission request', {
permission,
origin: details?.requestingUrl || '',
});
callback(false);
});
// Asked before some features even request; answering here keeps a page from
// reporting a capability it would then be denied.
panelSession.setPermissionCheckHandler(() => false);
// Serial, HID and USB device pickers.
panelSession.setDevicePermissionHandler(() => false);
};
const registerPackagedUiProtocol = () => {
if (!shouldUsePackagedUi()) return;
protocol.handle(UI_PROTOCOL, async (request) => {
@@ -3646,6 +3719,28 @@ const runSpecChain = (specs, appName) => {
throw new Error(`Failed to open in ${appName}: ${failures.join('; ')}`);
};
// The tunnel client lives in the web package (it already has a WebSocket
// client) and is loaded only if the user actually previews a remote dev server.
let devTunnelClientPromise = null;
const getDevTunnelClient = async () => {
if (!devTunnelClientPromise) {
devTunnelClientPromise = import('@openchamber/web/server/lib/dev-tunnel/client.js')
.then(({ createDevTunnelClient }) => createDevTunnelClient({ logger: log }))
.catch((error) => {
devTunnelClientPromise = null;
throw error;
});
}
return devTunnelClientPromise;
};
const closeAllDevTunnels = () => {
if (!devTunnelClientPromise) return;
const pending = devTunnelClientPromise;
devTunnelClientPromise = null;
pending.then((client) => client.closeAll()).catch(() => {});
};
const handleInvoke = async (browserWindow, command, args = {}) => {
switch (command) {
case 'desktop_start_window_drag':
@@ -3737,11 +3832,131 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
return { supported: true, enabled, active };
}
// Dev-server tunnels: bind a loopback port here and pipe it to a dev server
// on the remote OpenChamber host, so the browser panel loads a real origin
// instead of a rewritten page. Deliberately absent from
// COMMANDS_SAFE_FOR_REMOTE — a remote page must never open local listeners.
case 'desktop_dev_tunnel_open': {
const baseUrl = typeof args.baseUrl === 'string' ? args.baseUrl.trim() : '';
const port = Number.isFinite(args.port) ? Math.trunc(args.port) : 0;
if (!baseUrl) throw new Error('baseUrl is required');
if (!(port > 0 && port <= 65535)) throw new Error('A valid port is required');
const headers = {};
const requestHeaders = args.requestHeaders && typeof args.requestHeaders === 'object' ? args.requestHeaders : {};
for (const [name, value] of Object.entries(requestHeaders)) {
if (typeof value === 'string' && value) headers[name] = value;
}
if (typeof args.clientToken === 'string' && args.clientToken) {
headers.Authorization = `Bearer ${args.clientToken}`;
}
const client = await getDevTunnelClient();
const result = await client.open({ baseUrl, port, headers });
return { localPort: result.localPort, reused: result.reused, url: `http://127.0.0.1:${result.localPort}/` };
}
case 'desktop_dev_tunnel_close': {
const baseUrl = typeof args.baseUrl === 'string' ? args.baseUrl.trim() : '';
const port = Number.isFinite(args.port) ? Math.trunc(args.port) : 0;
if (!baseUrl || !(port > 0)) return { closed: false };
const client = await getDevTunnelClient();
return { closed: client.close({ baseUrl, port }) };
}
/**
* Forces prefers-color-scheme for one previewed page.
*
* nativeTheme.themeSource is app-wide and would drag OpenChamber's own
* appearance along with it, so this goes through the page's own emulation
* instead. The debugger session has to stay attached: emulation is part of
* that session and resets the moment it detaches.
*/
case 'desktop_browser_set_color_scheme': {
const scheme = args.scheme === 'light' || args.scheme === 'dark' ? args.scheme : 'system';
const target = resolveBrowserPanelContents(args.webContentsId);
if (!target.debugger.isAttached()) {
try {
target.debugger.attach('1.3');
} catch {
// DevTools owns the only debugger session a page can have.
throw new Error('Close DevTools for this page before changing its appearance');
}
}
await target.debugger.sendCommand('Emulation.setEmulatedMedia', scheme === 'system'
? { features: [] }
: { features: [{ name: 'prefers-color-scheme', value: scheme }] });
if (scheme === 'system') {
// Nothing left to emulate; give the session back so DevTools can attach.
try { target.debugger.detach(); } catch { /* already gone */ }
}
return { scheme };
}
/**
* Fetches a page's favicon for the tab strip.
*
* Done here, in the panel's own session, rather than by the renderer: the
* icon often sits behind the same login as the page, and letting the app's
* own origin request it would both fail on those and quietly send traffic
* to third-party hosts from OpenChamber itself. The bytes come back as a
* data URL so nothing else has to fetch anything.
*/
case 'desktop_browser_fetch_favicon': {
const target = typeof args.url === 'string' ? args.url.trim() : '';
let parsed;
try {
parsed = new URL(target);
} catch {
throw new Error('A favicon URL is required');
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error('Unsupported favicon URL');
}
const response = await electronNet.fetch(parsed.toString(), {
session: session.fromPartition(BROWSER_PANEL_PARTITION),
});
if (!response.ok) throw new Error(`Favicon request failed (${response.status})`);
const mime = (response.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
if (!FAVICON_MIME_TYPES.has(mime)) throw new Error('Favicon is not an image');
const buffer = Buffer.from(await response.arrayBuffer());
// A tab icon is a few kilobytes; anything of a different order is not one,
// and is not worth holding in memory for every tab.
if (buffer.length === 0 || buffer.length > MAX_FAVICON_BYTES) {
throw new Error('Favicon is not a usable size');
}
return { dataUrl: `data:${mime};base64,${buffer.toString('base64')}` };
}
// Scoped to the browser panel's own partition, so clearing it can never
// touch OpenChamber's session or any other window's storage.
case 'desktop_browser_clear_data': {
// Exact match, not a prefix: a prefix would also accept a partition that
// merely starts with this name, which is not what the comment above
// promises and would quietly stop being true if one were ever added.
const partition = typeof args.partition === 'string' ? args.partition.trim() : '';
if (partition !== BROWSER_PANEL_PARTITION) {
throw new Error('Unsupported browser partition');
}
const storages = [];
if (args.cookies === true) storages.push('cookies');
if (args.cache === true) storages.push('localstorage', 'indexdb', 'websql', 'serviceworkers', 'cachestorage');
if (storages.length === 0) return { cleared: false };
const browserSession = session.fromPartition(partition);
await browserSession.clearStorageData({ storages });
if (args.cache === true) await browserSession.clearCache();
return { cleared: true };
}
case 'desktop_browser_capture_page': {
const wcId = Number.isFinite(args.webContentsId) ? Math.trunc(args.webContentsId) : null;
if (wcId === null || wcId < 0) throw new Error('webContentsId is required');
const wc = webContents.fromId(wcId);
if (!wc || wc.isDestroyed()) throw new Error('WebContents not found');
const wc = resolveBrowserPanelContents(args.webContentsId);
const image = await wc.capturePage();
const buffer = image.toJPEG(82);
return {
@@ -4398,14 +4613,13 @@ const handleInvoke = async (browserWindow, command, args = {}) => {
}
return null;
// Minimize always goes to the taskbar/dock, even with tray background mode
// on: hiding the window here would drop the taskbar entry and make the
// in-app minimize button behave differently from the native one. Only
// closing hands the window to the tray.
case 'desktop_minimize_current_window':
if (browserWindow && !browserWindow.isDestroyed()) {
if (shouldHideMainWindowToTray(browserWindow)) {
debounceWindowStatePersist(browserWindow, true);
browserWindow.hide();
} else {
browserWindow.minimize();
}
browserWindow.minimize();
}
return null;
@@ -5081,6 +5295,8 @@ app.on('window-all-closed', () => {
app.on('before-quit', (event) => {
state.quitRequested = true;
// Loopback listeners would otherwise outlive the window that needed them.
closeAllDevTunnels();
if (state.installingUpdate) {
return;
@@ -5154,6 +5370,7 @@ app.whenReady().then(async () => {
});
nativeTheme.themeSource = readThemeSource();
registerPackagedUiProtocol();
hardenBrowserPanelSession();
setupAutoUpdater();
if (process.platform === 'darwin') {
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it } from 'bun:test';
import { resolveManagedOpenCodeCwd } from './opencode-cwd.mjs';
+6 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@openchamber/electron",
"version": "1.18.1",
"version": "1.18.4",
"private": true,
"description": "Electron desktop runtime for OpenChamber",
"author": "OpenChamber",
@@ -13,9 +13,10 @@
"electron-updater": "^6.8.3"
},
"devDependencies": {
"@electron/rebuild": "^3.7.0",
"electron": "^41.2.1",
"electron-builder": "^26.0.0"
"@electron/rebuild": "^4.2.0",
"electron": "^43.3.0",
"electron-builder": "^26.0.0",
"node-abi": "^4.33.0"
},
"trustedDependencies": [
"electron"
@@ -39,6 +40,7 @@
"bundle:main": "bun ./scripts/bundle-main.mjs",
"generate:macos-icon": "node ./scripts/generate-macos-icon-assets.cjs",
"rebuild:native": "node ./scripts/rebuild-native.mjs",
"test": "node ../../scripts/run-isolated-tests.mjs .",
"test:architecture": "node --test ./startup-url-selection.test.mjs ./scripts/target-architecture.test.mjs ./scripts/verify-linux-appimage.test.mjs ./scripts/verify-update-manifest.test.mjs ./scripts/ensure-electron.test.mjs",
"test:updater": "node --test ./updater-capability.test.mjs ./updater-channel.test.mjs ./updater-check.test.mjs ./updater-feed.test.mjs ./scripts/finalize-latest-yml.test.mjs ./scripts/updater-e2e-fixture.test.mjs",
"test:linux-desktop": "node --test ./linux-autostart.test.mjs && node ./scripts/smoke-linux-app-discovery.mjs && node ./scripts/smoke-path-open-utils.mjs",
+1 -1
View File
@@ -12,7 +12,7 @@ const accessErrorMessage = (label, targetPath, error) => {
return `${label} could not be checked: ${error?.message || String(error)}`;
};
export const normalizeRequiredPath = (rawPath, label = 'Path') => {
const normalizeRequiredPath = (rawPath, label = 'Path') => {
const targetPath = typeof rawPath === 'string' ? rawPath.trim() : '';
if (!targetPath) {
throw new Error(`${label} is required`);
+1 -1
View File
@@ -1,7 +1,7 @@
const MISSING_UPDATE_FEED_RE =
/404|ENOTFOUND|Cannot find (?:channel|latest)|latest-linux(?:-arm64)?\.yml|HttpError:\s*404|status code 404/i;
export const isMissingUpdateFeedError = (error) => {
const isMissingUpdateFeedError = (error) => {
const message = error instanceof Error ? error.message : String(error ?? '');
return MISSING_UPDATE_FEED_RE.test(message);
};
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@openchamber/ui",
"version": "1.18.1",
"version": "1.18.4",
"private": true,
"type": "module",
"main": "src/main.tsx",
@@ -8,7 +8,8 @@
"dev": "tsc --noEmit --watch",
"build": "tsc --noEmit",
"type-check": "tsc --noEmit",
"lint": "eslint \"./src/**/*.{ts,tsx}\" --config ../../eslint.config.js"
"lint": "eslint \"./src/**/*.{ts,tsx}\" --config ../../eslint.config.js",
"test": "node ../../scripts/run-isolated-tests.mjs src"
},
"dependencies": {
"@aparajita/capacitor-secure-storage": "^8.0.0",
@@ -43,13 +44,12 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@lezer/highlight": "^1.2.3",
"@opencode-ai/sdk": "1.18.15",
"@opencode-ai/sdk": "1.18.18",
"@pierre/diffs": "1.3.0-beta.6",
"@replit/codemirror-vim": "^6.3.0",
"@simplewebauthn/browser": "13.3.0",
"@tanstack/react-virtual": "3.14.5",
"@xenova/transformers": "^2.17.2",
"@zumer/snapdom": "^2.12.0",
"beautiful-mermaid": "^1.1.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+27 -7
View File
@@ -54,7 +54,11 @@ import { MCP_OAUTH_CALLBACK_PATH } from '@/components/sections/mcp/mcpOAuth';
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
import { useI18n } from '@/lib/i18n';
import { applyMobileKeyboardMode } from '@/lib/mobileKeyboardMode';
import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat';
import {
EMBEDDED_VISIBILITY_UPDATE,
isEmbeddedSessionChat,
requestEmbeddedSessionVisibility,
} from '@/components/layout/contextPanelEmbeddedChat';
import { SyncAppEffects } from '@/apps/AppEffects';
import { resetAppForRuntimeEndpointChange } from '@/apps/runtimeEndpointReset';
import { useAppFontEffects } from '@/apps/useAppFontEffects';
@@ -106,6 +110,7 @@ type EmbeddedSessionChatConfig = {
sessionId: string;
directory: string | null;
readOnly: boolean;
allowPromptingSubagentSessions?: boolean;
};
type EmbeddedVisibilityPayload = {
@@ -138,6 +143,9 @@ const readEmbeddedSessionChatConfig = (): EmbeddedSessionChatConfig | null => {
sessionId,
directory,
readOnly: params.get('readOnly') === '1' || params.get('readOnly') === 'true',
allowPromptingSubagentSessions: params.has('allowPromptingSubagentSessions')
? params.get('allowPromptingSubagentSessions') === '1'
: undefined,
};
};
@@ -199,7 +207,16 @@ const EmbeddedSessionChatContent: React.FC<{
<>
<SyncAppEffects embeddedBackgroundWorkEnabled={embeddedBackgroundWorkEnabled} />
<OpenCodeUpdateToast />
<ChatView readOnly={embeddedSessionChat.readOnly} />
<ChatView
active={embeddedBackgroundWorkEnabled}
// Always subscribe to message history in the mounted session-chat
// iframe. Visibility still gates composer focus and background work so
// a boot-inactive / lost-handshake race cannot leave a busy subagent
// showing only its status row (#2903 / #2892).
messagesEnabled={true}
readOnly={embeddedSessionChat.readOnly}
initialAllowPromptingSubagentSessions={embeddedSessionChat.allowPromptingSubagentSessions}
/>
<Toaster />
</>
);
@@ -228,7 +245,10 @@ function App({ apis }: AppProps) {
const [showMemoryDebug, setShowMemoryDebug] = React.useState(false);
const refreshGitHubAuthStatus = useGitHubAuthStore((state) => state.refreshStatus);
const [isVSCodeRuntime, setIsVSCodeRuntime] = React.useState<boolean>(() => apis.runtime.isVSCode);
const [isEmbeddedVisible, setIsEmbeddedVisible] = React.useState(true);
// Embedded chats start inactive until the parent panel identifies the active
// tab. Otherwise a newly loaded background tab can focus its composer first
// and steal keyboard input from the main chat.
const [isEmbeddedVisible, setIsEmbeddedVisible] = React.useState(false);
const [initRetryExhausted, setInitRetryExhausted] = React.useState(false);
const [initRetryEpoch, setInitRetryEpoch] = React.useState(0);
const [runtimeEndpointEpoch, setRuntimeEndpointEpoch] = React.useState(0);
@@ -527,17 +547,16 @@ function App({ apis }: AppProps) {
}
const applyVisibility = (payload?: EmbeddedVisibilityPayload) => {
const nextVisible = payload?.visible === true;
setIsEmbeddedVisible(nextVisible);
setIsEmbeddedVisible(payload?.visible === true);
};
const handleMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) {
if (event.origin !== window.location.origin || event.source !== window.parent) {
return;
}
const data = event.data as { type?: unknown; payload?: EmbeddedVisibilityPayload };
if (data?.type !== 'openchamber:embedded-visibility') {
if (data?.type !== EMBEDDED_VISIBILITY_UPDATE) {
return;
}
@@ -550,6 +569,7 @@ function App({ apis }: AppProps) {
scopedWindow.__openchamberSetEmbeddedVisibility = applyVisibility;
window.addEventListener('message', handleMessage);
requestEmbeddedSessionVisibility();
return () => {
window.removeEventListener('message', handleMessage);
+80 -34
View File
@@ -54,7 +54,7 @@ import { MobileSessionsSheet } from './MobileSessionsSheet';
import { MobileFullscreenSurface } from './MobileFullscreenSurface';
import { MobileWorkspaceDrawer, type MobileWorkspaceTab } from './MobileWorkspaceDrawer';
import { DedicatedMobileAppProvider, type MobileAppActions } from './mobileAppContext';
import { autoConnectLastInstance, getAutoConnectTargetLabel, reprobeActiveConnection, type AutoConnectOutcome } from './mobileConnections';
import { autoConnectLastInstance, getAutoConnectTargetLabel, logMobileConnectEvent, reprobeActiveConnection, type AutoConnectOutcome } from './mobileConnections';
import { isCapacitorMobileApp, useNativeAndroidBackButton, useNativeMobileChrome, useNativeMobileLifecycle } from './mobileNativeChrome';
import { reconnectAppForTransportSwitch, resetAppForRuntimeEndpointChange } from './runtimeEndpointReset';
import { useAppFontEffects } from './useAppFontEffects';
@@ -83,6 +83,7 @@ const MOBILE_SETTINGS_PAGES = [
'providers',
'usage',
'voice',
'integrations',
'about',
] as const;
@@ -660,9 +661,11 @@ export function MobileApp({ apis }: MobileAppProps) {
// saved instance instead of dead-ending on the connect screen until the
// user restarts the app. Success fires runtime-endpoint-changed, which
// re-bootstraps everything.
logMobileConnectEvent('resume:auto-connect', {});
void autoConnectLastInstance();
return;
}
logMobileConnectEvent('resume:reprobe', {});
// Re-probe the active device's transports on resume: the network may have
// changed while the app slept, so hot-switch LAN⇄relay if a better transport
@@ -675,7 +678,8 @@ export function MobileApp({ apis }: MobileAppProps) {
if (providersCount === 0) void loadProviders({ source: 'mobileApp:nativeResume' });
if (agentsCount === 0) void loadAgents({ source: 'mobileApp:nativeResume' });
};
const disconnect = () => {
const disconnect = (reason: string) => {
logMobileConnectEvent('resume:disconnect', { reason });
switchRuntimeEndpoint({ apiBaseUrl: '', clientToken: null, runtimeKey: 'mobile-disconnected' });
setConnectionEpoch((value) => value + 1);
};
@@ -683,36 +687,50 @@ export function MobileApp({ apis }: MobileAppProps) {
void reprobeActiveConnection().then((outcome) => {
if (nativeResumeValidationSeqRef.current !== validationSeq) return;
if (outcome === 'no-connection') {
disconnect();
disconnect('no-connection');
return;
}
if (outcome === 'needs-login') {
// Token explicitly rejected (revoked/expired) — tell the user why they
// land back on the connect screen instead of silently bouncing them.
setAutoConnectNotice({ kind: 'auth-expired', label: getAutoConnectTargetLabel() ?? '' });
disconnect();
disconnect('needs-login');
return;
}
if (outcome === 'unreachable') {
// Right after a resume or Wi-Fi switch the network is often still
// settling (on Android without a SIM there is NO connectivity at all for
// a few seconds), so a single fast probe races the network coming up.
// Retry once after a grace period before tearing the connection down.
window.setTimeout(() => {
if (nativeResumeValidationSeqRef.current !== validationSeq) return;
void reprobeActiveConnection().then((retry) => {
// settling (Android without a SIM has NO connectivity for a few
// seconds; a WireGuard tunnel re-handshakes; a relay cold start pays
// TLS + WS + E2EE before it can answer), so a single fast probe races
// the network coming up. Retry on a widening grace ladder before
// tearing the connection down — the last attempt runs with the full
// connect budget so slow-but-alive transports get a real chance.
const retryDelaysMs = [4000, 10000];
const retryAt = (attempt: number) => {
window.setTimeout(() => {
if (nativeResumeValidationSeqRef.current !== validationSeq) return;
if (retry === 'switched') return;
if (retry === 'unchanged') {
refreshInPlace();
return;
}
if (retry === 'needs-login') {
setAutoConnectNotice({ kind: 'auth-expired', label: getAutoConnectTargetLabel() ?? '' });
}
disconnect();
});
}, 4000);
const lastAttempt = attempt === retryDelaysMs.length - 1;
void reprobeActiveConnection({ fast: !lastAttempt }).then((retry) => {
if (nativeResumeValidationSeqRef.current !== validationSeq) return;
if (retry === 'switched') return;
if (retry === 'unchanged') {
refreshInPlace();
return;
}
if (retry === 'needs-login') {
setAutoConnectNotice({ kind: 'auth-expired', label: getAutoConnectTargetLabel() ?? '' });
disconnect('retry-needs-login');
return;
}
if (!lastAttempt) {
retryAt(attempt + 1);
return;
}
disconnect(`retry-${retry}`);
});
}, retryDelaysMs[attempt]);
};
retryAt(0);
return;
}
if (outcome === 'switched') return;
@@ -764,6 +782,15 @@ export function MobileApp({ apis }: MobileAppProps) {
// stale. The SyncProvider is keyed by runtimeEndpointEpoch so it remounts too.
React.useEffect(() => {
return subscribeRuntimeEndpointChanged((detail) => {
// Catch-all trail entry: EVERY endpoint change lands here regardless of
// which code path triggered it, so a "kicked to the connect screen"
// report always shows what dropped the runtime even when the trigger
// itself is not instrumented.
logMobileConnectEvent('endpoint:changed', {
runtimeKey: detail.runtimeKey || 'none',
previousRuntimeKey: detail.previousRuntimeKey || 'none',
connected: Boolean(detail.apiBaseUrl),
});
// A LAN⇄relay swap for the SAME device keeps the runtime key stable. Treat
// that as a transport-only change: rebind the sync layer to the new
// transport but keep the user's session/connection state — no reconnecting
@@ -800,19 +827,30 @@ export function MobileApp({ apis }: MobileAppProps) {
}
let cancelled = false;
setAutoConnectPhase('attempting');
void autoConnectLastInstance()
.catch((): AutoConnectOutcome => ({ status: 'no-candidate' }))
.then((outcome) => {
if (cancelled) return;
// Landing on the connect screen silently reads as data loss — say WHY
// the saved instance didn't come back (unreachable vs revoked auth).
if (outcome.status === 'unreachable') {
setAutoConnectNotice({ kind: 'unreachable', label: outcome.label });
} else if (outcome.status === 'needs-login') {
setAutoConnectNotice({ kind: 'auth-expired', label: outcome.label });
}
setAutoConnectPhase('done');
});
void (async () => {
const outcome = await autoConnectLastInstance()
.catch((): AutoConnectOutcome => ({ status: 'no-candidate' }));
if (cancelled) return;
// Landing on the connect screen silently reads as data loss — say WHY
// the saved instance didn't come back (unreachable vs revoked auth).
if (outcome.status === 'unreachable') {
setAutoConnectNotice({ kind: 'unreachable', label: outcome.label });
} else if (outcome.status === 'needs-login') {
setAutoConnectNotice({ kind: 'auth-expired', label: outcome.label });
}
// Release the splash on the fast verdict — a dead server must not pin
// the logo for the full connect budget. The fast probe races a
// just-woken network/relay (WireGuard re-handshake, relay TLS + WS +
// E2EE cold start), so a false "unreachable" is common right after
// launch: retry once IN THE BACKGROUND with the full budget. A success
// switches the runtime and the app moves in from the connect screen on
// its own; a manual connect the user started meanwhile wins via
// skipIfConnected.
setAutoConnectPhase('done');
if (outcome.status === 'unreachable') {
void autoConnectLastInstance({ fast: false, skipIfConnected: true }).catch(() => null);
}
})();
return () => {
cancelled = true;
};
@@ -833,6 +871,7 @@ export function MobileApp({ apis }: MobileAppProps) {
if (!isNativeMobileApp || !getRuntimeApiBaseUrl()) return;
let cancelled = false;
const dropToConnectScreen = (notice: MobileConnectionNotice | null) => {
logMobileConnectEvent('cold-launch:drop', { kind: notice?.kind ?? 'unknown' });
if (notice) setAutoConnectNotice(notice);
switchRuntimeEndpoint({ apiBaseUrl: '', clientToken: null, runtimeKey: 'mobile-disconnected' });
setConnectionEpoch((value) => value + 1);
@@ -847,7 +886,14 @@ export function MobileApp({ apis }: MobileAppProps) {
return;
}
if (outcome === 'unreachable') {
// A fast probe racing the just-woken network/relay produces false
// "unreachable" verdicts (seen in the field: the same LAN candidate
// refuses on launch and answers 200 two minutes later). Show the
// connect screen on the fast verdict — no splash hostage — and retry
// once in the background with the full budget; a success reconnects
// the app from the connect screen on its own.
dropToConnectScreen(label ? { kind: 'unreachable', label } : null);
void autoConnectLastInstance({ fast: false, skipIfConnected: true }).catch(() => null);
return;
}
// 'no-connection': at cold start the runtime key may not map to a saved
@@ -0,0 +1,55 @@
import React from 'react';
import { Icon } from '@/components/icon/Icon';
import { Button } from '@/components/ui/button';
import { copyTextToClipboard } from '@/lib/clipboard';
import { useI18n } from '@/lib/i18n';
import { formatMobileConnectDebugEntry, getMobileConnectDebugEntries, getMobileConnectDebugText } from './mobileConnectionDebug';
// Hidden diagnostics surface for device-only connection bugs: renders the
// in-memory connection event trail with one-tap copy, so a user on a release
// build (no tethered debugger, no Web Inspector) can paste the exact probe
// sequence into a bug report. Opened via long-press easter eggs on the connect
// screen logo and the instances list — invisible unless you know it's there.
export const MobileConnectionDebugPanel: React.FC<{ onClose: () => void }> = ({ onClose }) => {
const { t } = useI18n();
const [copied, setCopied] = React.useState(false);
// Snapshot on open; a live-updating log under the user's finger would fight
// the copy button. Reopen to refresh.
const entries = React.useMemo(() => getMobileConnectDebugEntries(), []);
const handleCopy = React.useCallback(() => {
void copyTextToClipboard(getMobileConnectDebugText()).then((result) => {
if (!result.ok) return;
setCopied(true);
window.setTimeout(() => setCopied(false), 2000);
});
}, []);
return (
<div className="fixed inset-0 z-[70] flex flex-col bg-background pb-[var(--safe-area-inset-bottom,env(safe-area-inset-bottom,0px))] pt-[var(--safe-area-inset-top,env(safe-area-inset-top,0px))] text-foreground">
<div className="flex items-center justify-between gap-2 border-b border-border/70 px-4 py-2.5">
<h2 className="min-w-0 truncate typography-ui-label text-foreground">{t('mobile.connectionDebug.title')}</h2>
<div className="flex shrink-0 items-center gap-1.5">
<Button type="button" variant="outline" size="sm" onClick={handleCopy} disabled={entries.length === 0}>
<Icon name={copied ? 'check' : 'file-copy'} className="size-4" />
{copied ? t('mobile.connectionDebug.copied') : t('mobile.connectionDebug.copy')}
</Button>
<Button type="button" variant="ghost" size="icon" aria-label={t('mobile.connectionDebug.close')} onClick={onClose}>
<Icon name="close" className="size-[18px]" />
</Button>
</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-4 py-3">
{entries.length === 0 ? (
<p className="typography-small text-muted-foreground">{t('mobile.connectionDebug.empty')}</p>
) : (
<pre className="whitespace-pre-wrap break-words typography-code text-muted-foreground">
{entries.map(formatMobileConnectDebugEntry).join('\n')}
</pre>
)}
</div>
</div>
);
};
@@ -7,6 +7,8 @@ import { useI18n } from '@/lib/i18n';
import { cn } from '@/lib/utils';
import { connectionDisplayUrl, useMobileConnection } from './mobileConnections';
import { useDebugPanelLongPress } from './mobileConnectionDebug';
import { MobileConnectionDebugPanel } from './MobileConnectionDebugPanel';
import { isQrScanSupported, parseConnectionPayload, scanConnectionQr } from './mobileQrScan';
import { mobileConnectionInputClass, mobileInputKeyboardProps } from './mobileConnectionUi';
import { MobileQrConnectionLoading, MobileQrScannerOverlay } from './MobileQrScannerOverlay';
@@ -37,6 +39,10 @@ export const MobileConnectionWelcome: React.FC<{
// Which saved connection is being connected to, for the per-row spinner.
const [connectingId, setConnectingId] = React.useState<string | null>(null);
const [password, setPassword] = React.useState('');
// Hidden diagnostics: long-press the logo to open the connection event log —
// reachable even when a user has been bounced back to this screen.
const [debugOpen, setDebugOpen] = React.useState(false);
const debugLongPress = useDebugPanelLongPress(React.useCallback(() => setDebugOpen(true), []));
const handleSubmit = React.useCallback((event: React.FormEvent) => {
event.preventDefault();
@@ -127,10 +133,13 @@ export const MobileConnectionWelcome: React.FC<{
<>
{isScanning ? <MobileQrScannerOverlay onCancel={() => scanAbortRef.current?.abort()} /> : null}
{isCompletingScan ? <MobileQrConnectionLoading /> : null}
{debugOpen ? <MobileConnectionDebugPanel onClose={() => setDebugOpen(false)} /> : null}
<main className="oc-keyboard-fill-screen flex min-h-dvh flex-col overflow-y-auto bg-background px-6 pb-[calc(var(--safe-area-inset-bottom,env(safe-area-inset-bottom,0px))+28px)] pt-[calc(var(--safe-area-inset-top,env(safe-area-inset-top,0px))+28px)] text-foreground">
<div className="m-auto flex w-full max-w-[360px] shrink-0 flex-col items-center gap-9 py-8">
<div className="flex flex-col items-center gap-5 text-center">
<OpenChamberLogo width={72} height={72} className="size-[72px]" />
<span {...debugLongPress} className="select-none" style={{ touchAction: 'manipulation' }}>
<OpenChamberLogo width={72} height={72} className="size-[72px]" />
</span>
<h1 className="typography-h2 text-foreground">{t('mobile.connect.welcome.title')}</h1>
</div>
@@ -7,6 +7,8 @@ import { isRelayModeActive } from '@/lib/relay/runtime-tunnel';
import { cn } from '@/lib/utils';
import { connectionDisplayUrl, isActiveRuntimeConnection, useMobileConnection } from './mobileConnections';
import { useDebugPanelLongPress } from './mobileConnectionDebug';
import { MobileConnectionDebugPanel } from './MobileConnectionDebugPanel';
import { isQrScanSupported, scanConnectionQr } from './mobileQrScan';
import { mobileConnectionInputClass, mobileInputKeyboardProps } from './mobileConnectionUi';
import { MobileQrConnectionLoading, MobileQrScannerOverlay } from './MobileQrScannerOverlay';
@@ -37,6 +39,10 @@ export const MobileInstancesSurface: React.FC<{
const [formOpen, setFormOpen] = React.useState(false);
// Which row is being connected to, for the per-row spinner.
const [connectingId, setConnectingId] = React.useState<string | null>(null);
// Hidden diagnostics: long-press a connection row to open the connection
// event log (the long-press swallows the row's normal connect tap).
const [debugOpen, setDebugOpen] = React.useState(false);
const debugLongPress = useDebugPanelLongPress(React.useCallback(() => setDebugOpen(true), []));
// Populate/clear the form imperatively (on edit tap / cancel / save) rather than via
// an effect keyed on the derived connection object. With an effect, any churn of the
@@ -189,11 +195,12 @@ export const MobileInstancesSurface: React.FC<{
<>
{isScanning ? <MobileQrScannerOverlay onCancel={() => scanAbortRef.current?.abort()} /> : null}
{isCompletingScan ? <MobileQrConnectionLoading /> : null}
{debugOpen ? <MobileConnectionDebugPanel onClose={() => setDebugOpen(false)} /> : null}
<div className="flex h-full flex-col overflow-hidden">
<div className="flex-1 overflow-y-auto px-5 py-4">
<div className="space-y-6">
{connections.length > 0 ? (
<div className="overflow-hidden rounded-[18px] border border-border/70 bg-surface-elevated">
<div {...debugLongPress} className="overflow-hidden rounded-[18px] border border-border/70 bg-surface-elevated">
{connections.map((connection) => {
const confirming = confirmingDeleteId === connection.id;
const isActive = isActiveRuntimeConnection(connection);
@@ -287,7 +294,7 @@ export const MobileInstancesSurface: React.FC<{
})}
</div>
) : (
<p className="rounded-[18px] border border-dashed border-border/70 px-4 py-6 text-center typography-small text-muted-foreground">
<p {...debugLongPress} className="rounded-[18px] border border-dashed border-border/70 px-4 py-6 text-center typography-small text-muted-foreground">
{t('mobile.connect.saved.empty')}
</p>
)}
+3 -7
View File
@@ -3,7 +3,7 @@ import React from 'react';
import { isCapacitorApp } from '@/lib/platform';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { buildDeepLink, parseDeepLink, type DeepLinkIntent, type SessionsFilter, type ViewTarget } from './deepLinks';
import { parseDeepLink, type DeepLinkIntent, type SessionsFilter, type ViewTarget } from './deepLinks';
/**
* Navigation layer for {@link DeepLinkIntent}s the only place that knows how to *apply* a
@@ -93,13 +93,13 @@ const flush = (): void => {
};
/** Apply an intent now if possible, otherwise stash it until the app is ready / a handler appears. */
export const applyDeepLinkIntent = (intent: DeepLinkIntent): void => {
const applyDeepLinkIntent = (intent: DeepLinkIntent): void => {
pending = intent;
flush();
};
/** Convenience: parse a raw `openchamber://…` URL and apply it. No-op for unrecognised URLs. */
export const applyDeepLinkUrl = (raw: string | null | undefined): void => {
const applyDeepLinkUrl = (raw: string | null | undefined): void => {
const intent = parseDeepLink(raw);
if (intent) {
applyDeepLinkIntent(intent);
@@ -192,7 +192,3 @@ export const useDeepLinkSource = (options: { ready: boolean }): void => {
};
}, []);
};
// Re-export so producers (notifications, future widgets) have one import for the whole vocabulary.
export { buildDeepLink, parseDeepLink };
export type { DeepLinkIntent, SessionsFilter, ViewTarget };
+1 -44
View File
@@ -10,7 +10,7 @@
* context including, eventually, a tiny encoder shared with the native widget/extension.
*/
export const DEEP_LINK_SCHEME = 'openchamber';
const DEEP_LINK_SCHEME = 'openchamber';
export type SessionsFilter = 'all' | 'attention' | 'recent';
export type ViewTarget = 'files' | 'mcp' | 'instances' | 'update';
@@ -124,46 +124,3 @@ export function parseDeepLink(raw: string | null | undefined): DeepLinkIntent |
return null;
}
}
/**
* Build a canonical `openchamber://…` URL for an intent. Used by anything that needs to hand
* a deep link to iOS notification payloads, `widgetURL(...)`, Live Activity tap targets
* so every producer emits the exact shape {@link parseDeepLink} understands.
*/
export function buildDeepLink(intent: DeepLinkIntent): string {
const base = `${DEEP_LINK_SCHEME}://`;
const withQuery = (path: string, params: Record<string, string | undefined>): string => {
const search = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (typeof value === 'string' && value.length > 0) {
search.set(key, value);
}
}
const query = search.toString();
return query ? `${base}${path}?${query}` : `${base}${path}`;
};
switch (intent.type) {
case 'session':
return withQuery(`session/${encodeURIComponent(intent.sessionId)}`, { dir: intent.directory });
case 'new-session':
return withQuery('new', {
dir: intent.directory,
project: intent.projectId,
agent: intent.agent,
model: intent.model,
});
case 'sessions':
return withQuery('sessions', { filter: intent.filter });
case 'status':
return `${base}status`;
case 'settings':
return intent.section ? `${base}settings/${encodeURIComponent(intent.section)}` : `${base}settings`;
case 'changes':
return withQuery(intent.path ? `changes/${intent.path}` : 'changes', {
staged: intent.staged ? 'true' : undefined,
});
case 'view':
return `${base}view/${intent.target}`;
}
}
@@ -0,0 +1,106 @@
// In-memory capture of mobile connection lifecycle events, so device-only
// connection failures (Capacitor iOS/Android) can be diagnosed without a
// tethered debugger: the hidden debug panel renders this buffer and offers a
// one-tap copy for bug reports. Console logging stays the primary sink — this
// mirrors it. Never persisted; details are the already-masked logConnect
// payloads (no tokens or secrets reach this module).
import React from 'react';
type MobileConnectDebugEntry = {
at: number;
step: string;
detail: string;
};
const MAX_ENTRIES = 300;
// The trail documents THE CURRENT app run only — it resets on every launch.
// Days of accumulated history would bury the failure the panel exists to
// expose. (An earlier revision persisted the log across launches; the storage
// key is removed here so installs that ran it don't keep a stale blob around.)
const LEGACY_STORAGE_KEY = 'openchamber.mobile.connectLog.v1';
const entries: MobileConnectDebugEntry[] = [];
if (typeof window !== 'undefined') {
try {
window.localStorage.removeItem(LEGACY_STORAGE_KEY);
} catch {
// Storage unavailable — the in-memory trail still works.
}
}
export const recordMobileConnectDebug = (step: string, detail: string): void => {
entries.push({ at: Date.now(), step, detail });
if (entries.length > MAX_ENTRIES) entries.splice(0, entries.length - MAX_ENTRIES);
};
// Launch separator: makes "everything above happened in a previous run of the
// app" readable at a glance in the persisted trail.
if (typeof window !== 'undefined') {
recordMobileConnectDebug('app:launch', '{}');
}
export const getMobileConnectDebugEntries = (): MobileConnectDebugEntry[] => [...entries];
const formatTime = (at: number): string => {
const date = new Date(at);
const pad = (value: number, width = 2) => String(value).padStart(width, '0');
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
};
export const formatMobileConnectDebugEntry = (entry: MobileConnectDebugEntry): string =>
`${formatTime(entry.at)} ${entry.step}${entry.detail && entry.detail !== '{}' ? ` ${entry.detail}` : ''}`;
export const getMobileConnectDebugText = (): string =>
entries.map(formatMobileConnectDebugEntry).join('\n');
// Long-press detector for the hidden debug-panel triggers. Pointer-based with a
// movement threshold so scrolling and normal taps never fire it; the synthetic
// click that follows a long-press release is swallowed in the capture phase so
// the host element's normal tap action does not also run.
export const useDebugPanelLongPress = (onLongPress: () => void, delayMs = 700) => {
const timerRef = React.useRef<number | null>(null);
const originRef = React.useRef<{ x: number; y: number } | null>(null);
const firedRef = React.useRef(false);
const clear = React.useCallback(() => {
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
timerRef.current = null;
originRef.current = null;
}, []);
React.useEffect(() => clear, [clear]);
const onPointerDown = React.useCallback((event: React.PointerEvent) => {
firedRef.current = false;
originRef.current = { x: event.clientX, y: event.clientY };
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => {
timerRef.current = null;
firedRef.current = true;
onLongPress();
}, delayMs);
}, [delayMs, onLongPress]);
const onPointerMove = React.useCallback((event: React.PointerEvent) => {
const origin = originRef.current;
if (!origin) return;
if (Math.abs(event.clientX - origin.x) > 10 || Math.abs(event.clientY - origin.y) > 10) clear();
}, [clear]);
const onClickCapture = React.useCallback((event: React.MouseEvent) => {
if (!firedRef.current) return;
firedRef.current = false;
event.preventDefault();
event.stopPropagation();
}, []);
return {
onPointerDown,
onPointerMove,
onPointerUp: clear,
onPointerCancel: clear,
onClickCapture,
};
};
+83 -36
View File
@@ -26,6 +26,8 @@ import { createRelayTunnelClient } from '@/lib/relay/tunnel-client';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { getRuntimeApiBaseUrl, getRuntimeKey, switchRuntimeEndpoint } from '@/lib/runtime-switch';
import { recordMobileConnectDebug } from './mobileConnectionDebug';
const MOBILE_CONNECTIONS_STORAGE_KEY = 'openchamber.mobile.connections.v1';
const MOBILE_SECURE_STORAGE_PREFIX = 'openchamber.mobile.';
const MOBILE_DEVICE_ID_STORAGE_KEY = 'openchamber.mobile.deviceId';
@@ -162,7 +164,7 @@ type PairingRedeemResponse = {
// URL helpers
// ---------------------------------------------------------------------------
export const normalizeConnectionUrl = (value: string): string => {
const normalizeConnectionUrl = (value: string): string => {
const trimmed = value.trim();
if (!trimmed) return '';
const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
@@ -173,7 +175,7 @@ export const normalizeConnectionUrl = (value: string): string => {
return url.toString().replace(/\/+$/, '');
};
export const getConnectionLabel = (url: string): string => {
const getConnectionLabel = (url: string): string => {
try {
return new URL(url).host;
} catch {
@@ -189,7 +191,7 @@ const getConnectionStorageKey = (url: string): string => {
}
};
export const isSameConnectionUrl = (left: string, right: string): boolean =>
const isSameConnectionUrl = (left: string, right: string): boolean =>
getConnectionStorageKey(left) === getConnectionStorageKey(right);
// ---------------------------------------------------------------------------
@@ -199,7 +201,7 @@ export const isSameConnectionUrl = (left: string, right: string): boolean =>
// Stable identity for a relay connection. Also used as the runtime key passed
// to switchRuntimeEndpoint so "is this saved entry the active runtime?" checks
// can compare against getRuntimeKey().
export const relayConnectionRuntimeKey = (relay: MobileRelayConfig): string =>
const relayConnectionRuntimeKey = (relay: MobileRelayConfig): string =>
`relay:${relay.serverId}@${relay.relayUrl.trim()}`;
// Stable, non-fetchable pseudo-URL for a relay-only device (display only).
@@ -304,11 +306,22 @@ const logDetail = (detail: Record<string, unknown>): string => {
};
const logConnect = (step: string, detail: Record<string, unknown> = {}): void => {
console.info('[mobile-connect]', step, logDetail(detail));
const serialized = logDetail(detail);
console.info('[mobile-connect]', step, serialized);
recordMobileConnectDebug(step, serialized);
};
// Exported for surfaces that participate in the connection lifecycle outside
// this module (resume/online re-probes in MobileApp) so their decisions land in
// the same console + debug-panel trail as the probes themselves.
export const logMobileConnectEvent = (step: string, detail: Record<string, unknown> = {}): void => {
logConnect(step, detail);
};
const logStorage = (step: string, detail: Record<string, unknown> = {}): void => {
console.info('[mobile-storage]', step, logDetail(detail));
const serialized = logDetail(detail);
console.info('[mobile-storage]', step, serialized);
recordMobileConnectDebug(step, serialized);
};
const parseMaybeJson = (value: unknown): unknown => {
@@ -347,14 +360,14 @@ const nativeHttpRequest = async (url: string, init?: RequestInit): Promise<Mobil
json: async () => parseMaybeJson(response.data),
};
} catch (error) {
console.warn('[mobile-connect]', 'native-http failed', logDetail({ url, error: error instanceof Error ? error.message : String(error) }));
logConnect('native-http:failed', { url, error: error instanceof Error ? error.message : String(error) });
return null;
}
};
const browserFetchRequest = async (url: string, init?: RequestInit): Promise<MobileFetchResponse | null> => {
const response = await fetch(url, init).catch((error) => {
console.warn('[mobile-connect]', 'browser-fetch failed', logDetail({ url, error: error instanceof Error ? error.message : String(error) }));
logConnect('browser-fetch:failed', { url, error: error instanceof Error ? error.message : String(error) });
return null;
});
if (!response) return null;
@@ -777,7 +790,7 @@ export const upsertMobileConnection = async (
return next;
};
export const deleteMobileConnection = async (id: string): Promise<MobileSavedConnection[]> => {
const deleteMobileConnection = async (id: string): Promise<MobileSavedConnection[]> => {
const connections = readConnections();
const removed = connections.find((connection) => connection.id === id) ?? null;
const next = connections.filter((connection) => connection.id !== id);
@@ -835,6 +848,7 @@ const probeConnectionCandidates = async (
// /health is unauthenticated by design — never send the bearer token to an
// address whose identity has not been checked yet.
const health = await requestWithTimeout(`${url}/health`, { method: 'GET' }, requestOptions);
logConnect('probe:direct:health', { url, ok: health?.ok === true, status: health?.status ?? null, source: health?.source ?? null });
if (!health?.ok) continue;
if (expectedServerId) {
const payload = await health.json().catch(() => null);
@@ -850,6 +864,7 @@ const probeConnectionCandidates = async (
// the probe passes, and the app dies later on bootstrap's bearer-only
// requests. Cookie auth stays for the token-less (browser) flow.
const session = await requestWithTimeout(`${url}/auth/session`, { method: 'GET', credentials: token ? 'omit' : 'include', headers }, requestOptions);
logConnect('probe:direct:session', { url, ok: session?.ok === true, status: session?.status ?? null, source: session?.source ?? null, hasToken: Boolean(token) });
if (session?.status === 401) return { status: 'needs-login' };
if (!session || (!session.ok && session.status !== 404)) continue;
const status = await readSessionStatus(session);
@@ -868,11 +883,15 @@ const probeConnectionCandidates = async (
if (!relayCandidate) return { status: 'unreachable' };
// keepTunnel: an 'ok' probe hands its live tunnel to switchToTransport,
// which adopts it as the runtime tunnel — no second connect + handshake.
// Full-budget probes align relay with the direct-transport connect budget
// (8s) instead of inheriting probeRelaySession's 15s default: 8s is ample
// for TLS + WS + E2EE handshake, and a dead host must not pin the connect
// splash (or a resume retry) for 15 extra seconds.
const { outcome, tunnel } = await probeRelaySession(
relayCandidate.relay,
token,
undefined,
options?.fast ? MOBILE_FAST_PROBE_TIMEOUT_MS : undefined,
options?.fast ? MOBILE_FAST_PROBE_TIMEOUT_MS : MOBILE_CONNECT_TIMEOUT_MS,
{ keepTunnel: true },
);
if (outcome === 'ok') return { status: 'ok', transport: { kind: 'relay', relay: relayCandidate.relay, tunnel } };
@@ -989,36 +1008,49 @@ export type AutoConnectOutcome =
/** The saved token was rejected (expired/revoked) — the user must sign in again. */
| { status: 'needs-login'; label: string };
export const autoConnectLastInstance = async (): Promise<AutoConnectOutcome> => {
export const autoConnectLastInstance = async (options?: { fast?: boolean; skipIfConnected?: boolean }): Promise<AutoConnectOutcome> => {
const fast = options?.fast !== false;
await migrateLegacyInlineTokens();
const candidate = readConnections()[0]; // sorted most-recent-first
logConnect('auto-connect:start', { hasCandidate: Boolean(candidate), fast });
if (!candidate) return { status: 'no-candidate' };
// The runtime transport needs a bearer token; only auto-connect when one is
// already saved. A missing/expired token must go through the login UI.
// The runtime transport authenticates with a bearer token when the server
// issued one. A connection saved WITHOUT a token means its last successful
// connect was tokenless (server auth disabled) — probe it the same way; the
// probe itself reports needs-login if the server has since enabled auth. Only
// an EXPECTED token that cannot be read must go through the login UI.
let token: string | undefined;
if (isCapacitorApp()) {
if (!candidate.hasToken) {
return { status: 'no-candidate' };
}
token = await readSecureToken(secureTokenKeyOf(candidate));
if (!token) {
return { status: 'no-candidate' };
if (candidate.hasToken) {
token = await readSecureToken(secureTokenKeyOf(candidate));
if (!token) return { status: 'no-candidate' };
}
} else {
token = candidate.clientToken;
if (!token) return { status: 'no-candidate' };
if (!token && candidate.hasToken) return { status: 'no-candidate' };
}
// Fast probe: the cold-launch splash should decide in a couple of seconds,
// not sit through the full connect timeouts on a dead LAN candidate. A slow
// network that fails the fast probe still lands on the connect screen where
// a manual tap retries with the full budget.
const result = await probeConnectionCandidates(candidate.candidates, token, { fast: true });
// Fast probe by default: the cold-launch splash should decide in a couple of
// seconds, not sit through the full connect timeouts on a dead LAN candidate.
// Callers retrying after an 'unreachable' verdict pass fast:false so the slow
// retry gets the full connect budget (relay cold starts — TLS + WS + E2EE
// handshake — regularly overrun the fast window).
const result = await probeConnectionCandidates(candidate.candidates, token, { fast });
logConnect('auto-connect:probe', { status: result.status, candidates: candidate.candidates.map((c) => c.kind) });
if (result.status === 'needs-login') return { status: 'needs-login', label: candidate.label };
if (result.status !== 'ok') return { status: 'unreachable', label: candidate.label };
// Background-retry guard: while this slow probe ran, the user may have
// connected manually from the connect screen. Their choice wins — discard
// this result instead of hijacking the runtime (close the probe's unused
// relay tunnel; a direct transport holds nothing).
if (options?.skipIfConnected && getRuntimeApiBaseUrl()) {
if (result.transport.kind === 'relay') result.transport.tunnel?.close();
logConnect('auto-connect:superseded', {});
return { status: 'no-candidate' };
}
await upsertMobileConnection({ id: candidate.id, label: candidate.label, candidates: candidate.candidates }); // bump lastUsedAt (keeps token)
switchToTransport(result.transport, token, { runtimeKey: secureTokenKeyOf(candidate) });
switchToTransport(result.transport, token ?? null, { runtimeKey: secureTokenKeyOf(candidate) });
return { status: 'connected' };
};
@@ -1115,7 +1147,7 @@ const establishLiveTransport = async (
// tunnel via runtimeFetch. A transport failure/timeout is transient (the tunnel
// reconnects on its own) and must not masquerade as a revoked session, so only
// an explicit auth rejection reports invalid.
export const validateActiveRuntimeSession = async (input: {
const validateActiveRuntimeSession = async (input: {
url: string;
clientToken?: string | null;
}, options?: { fast?: boolean }): Promise<boolean> => {
@@ -1163,9 +1195,13 @@ export type ReprobeOutcome = 'switched' | 'unchanged' | 'unreachable' | 'needs-l
// validates the current transport over its live channel; only if that is dead does
// it fall through to the lower-priority candidates. 'unchanged' → keep the runtime
// and just refresh; 'unreachable'/'no-connection' → show the connect screen.
export const reprobeActiveConnection = async (): Promise<ReprobeOutcome> => {
export const reprobeActiveConnection = async (options?: { fast?: boolean }): Promise<ReprobeOutcome> => {
const fast = options?.fast !== false;
const active = findActiveConnection();
if (!active) return 'no-connection';
if (!active) {
logConnect('reprobe:no-connection', { runtimeKey: Boolean(getRuntimeKey()) });
return 'no-connection';
}
let token: string | undefined;
if (isCapacitorApp()) {
@@ -1173,7 +1209,15 @@ export const reprobeActiveConnection = async (): Promise<ReprobeOutcome> => {
} else {
token = active.clientToken;
}
if (!token) return 'unreachable';
// Tokenless is valid (server auth disabled — the probe reports needs-login if
// that changed); bail only when an EXPECTED token cannot be read. 'unreachable'
// (not needs-login) so the resume retry ladder re-reads the token — a transient
// secure-storage failure must not force a re-login.
if (!token && active.hasToken) {
logConnect('reprobe:no-token', { hasToken: true });
return 'unreachable';
}
logConnect('reprobe:start', { candidates: active.candidates.map((c) => c.kind), fast, hasToken: Boolean(token) });
const currentIndex = active.candidates.findIndex(
(candidate) => transportMatchesCurrentRuntime(candidate.kind === 'relay' ? { kind: 'relay', relay: candidate.relay } : { kind: 'direct', url: candidate.url }),
@@ -1181,10 +1225,11 @@ export const reprobeActiveConnection = async (): Promise<ReprobeOutcome> => {
// 1. A higher-priority transport becoming reachable means "came home" (relay → LAN).
const higher = currentIndex >= 0 ? active.candidates.slice(0, currentIndex) : active.candidates;
const better = await probeConnectionCandidates(higher, token, { fast: true });
const better = await probeConnectionCandidates(higher, token, { fast });
logConnect('reprobe:better', { status: better.status, probed: higher.length });
if (better.status === 'ok') {
await upsertMobileConnection({ id: active.id, label: active.label, candidates: active.candidates });
switchToTransport(better.transport, token, { runtimeKey: secureTokenKeyOf(active) });
switchToTransport(better.transport, token ?? null, { runtimeKey: secureTokenKeyOf(active) });
return 'switched';
}
// The shared token was explicitly rejected — no transport will accept it.
@@ -1192,7 +1237,8 @@ export const reprobeActiveConnection = async (): Promise<ReprobeOutcome> => {
// 2. No better transport — is the current one still alive on its live channel?
if (currentIndex >= 0) {
const stillValid = await validateActiveRuntimeSession({ url: getRuntimeApiBaseUrl(), clientToken: token }, { fast: true });
const stillValid = await validateActiveRuntimeSession({ url: getRuntimeApiBaseUrl(), clientToken: token }, { fast });
logConnect('reprobe:current', { stillValid });
if (stillValid) {
// Still on the same transport (typically: woke up on the relay, old LAN
// candidate dead). Ask the server for its current LAN addresses in the
@@ -1205,10 +1251,11 @@ export const reprobeActiveConnection = async (): Promise<ReprobeOutcome> => {
// 3. Current transport is dead — fall through to lower-priority candidates.
const lower = currentIndex >= 0 ? active.candidates.slice(currentIndex + 1) : [];
const fallback = await probeConnectionCandidates(lower, token, { fast: true });
const fallback = await probeConnectionCandidates(lower, token, { fast });
logConnect('reprobe:fallback', { status: fallback.status, probed: lower.length });
if (fallback.status === 'ok') {
await upsertMobileConnection({ id: active.id, label: active.label, candidates: active.candidates });
switchToTransport(fallback.transport, token, { runtimeKey: secureTokenKeyOf(active) });
switchToTransport(fallback.transport, token ?? null, { runtimeKey: secureTokenKeyOf(active) });
return 'switched';
}
if (fallback.status === 'needs-login') return 'needs-login';
@@ -1240,7 +1287,7 @@ let candidateRefreshInFlight = false;
// Only runs for relay-paired connections: their token/runtime key derives from
// the stable relay identity, so rewriting direct URLs cannot orphan the stored
// token. The response must echo the connection's serverId or it is ignored.
export const refreshActiveConnectionCandidates = async (): Promise<CandidateRefreshResult> => {
const refreshActiveConnectionCandidates = async (): Promise<CandidateRefreshResult> => {
if (candidateRefreshInFlight) return 'skipped';
const active = findActiveConnection();
if (!active) {
-7
View File
@@ -1,5 +1,3 @@
import type { ProjectEntry } from '@/lib/api/types';
export const normalizePath = (value?: string | null): string =>
(value || '').replace(/\\/g, '/').replace(/\/+$/g, '');
@@ -9,8 +7,3 @@ export const getProjectLabel = (path: string): string => {
const segments = normalized.split('/').filter(Boolean);
return segments[segments.length - 1]?.replace(/[-_]/g, ' ') || normalized;
};
export const getProjectDisplayLabel = (project: ProjectEntry | null, fallbackDirectory: string): string => {
if (project) return project.label?.trim() || getProjectLabel(project.path);
return getProjectLabel(fallbackDirectory);
};
+1 -1
View File
@@ -70,7 +70,7 @@ const projectLabelForDirectory = (directory: string | null, projects: ProjectEnt
return basename(directory);
};
export const buildMobileWidgetSnapshot = (): MobileWidgetSnapshot => {
const buildMobileWidgetSnapshot = (): MobileWidgetSnapshot => {
const sessions = useGlobalSessionsStore.getState().activeSessions;
const unseenBySession = useNotificationStore.getState().index.session.unseenCount;
const notifyOnSubtasks = useUIStore.getState().notifyOnSubtasks;
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<!-- Claude AI symbol (CC0, Wikimedia Commons File:Claude_AI_symbol.svg), monochrome for theme invert -->
<path fill="currentColor" d="m19.6 66.5 19.7-11 .3-1-.3-.5h-1l-3.3-.2-11.2-.3L14 53l-9.5-.5-2.4-.5L0 49l.2-1.5 2-1.3 2.9.2 6.3.5 9.5.6 6.9.4L38 49.1h1.6l.2-.7-.5-.4-.4-.4L29 41l-10.6-7-5.6-4.1-3-2-1.5-2-.6-4.2 2.7-3 3.7.3.9.2 3.7 2.9 8 6.1L37 36l1.5 1.2.6-.4.1-.3-.7-1.1L33 25l-6-10.4-2.7-4.3-.7-2.6c-.3-1-.4-2-.4-3l3-4.2L28 0l4.2.6L33.8 2l2.6 6 4.1 9.3L47 29.9l2 3.8 1 3.4.3 1h.7v-.5l.5-7.2 1-8.7 1-11.2.3-3.2 1.6-3.8 3-2L61 2.6l2 2.9-.3 1.8-1.1 7.7L59 27.1l-1.5 8.2h.9l1-1.1 4.1-5.4 6.9-8.6 3-3.5L77 13l2.3-1.8h4.3l3.1 4.7-1.4 4.9-4.4 5.6-3.7 4.7-5.3 7.1-3.2 5.7.3.4h.7l12-2.6 6.4-1.1 7.6-1.3 3.5 1.6.4 1.6-1.4 3.4-8.2 2-9.6 2-14.3 3.3-.2.1.2.3 6.4.6 2.8.2h6.8l12.6 1 3.3 2 1.9 2.7-.3 2-5.1 2.6-6.8-1.6-16-3.8-5.4-1.3h-.8v.4l4.6 4.5 8.3 7.5L89 80.1l.5 2.4-1.3 2-1.4-.2-9.2-7-3.6-3-8-6.8h-.5v.7l1.8 2.7 9.8 14.7.5 4.5-.7 1.4-2.6 1-2.7-.6-5.8-8-6-9-4.7-8.2-.5.4-2.9 30.2-1.3 1.5-3 1.2-2.5-2-1.4-3 1.4-6.2 1.6-8 1.3-6.4 1.2-7.9.7-2.6v-.2H49L43 72l-9 12.3-7.2 7.6-1.7.7-3-1.5.3-2.8L24 86l10-12.8 6-7.9 4-4.6-.1-.5h-.3L17.2 77.4l-4.7.6-2-2 .2-3 1-1 8-5.5Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,77 @@
import React from 'react';
import { Icon } from '@/components/icon/Icon';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
import { browserUrlLabel } from '@/lib/browser/url';
import type { BrowserHistoryEntry } from '@/lib/browser/history';
/**
* Addresses already visited in this project, offered under the address bar.
*
* Kept deliberately plain: it is a short list of places, so it borrows the
* app's dropdown surface rather than introducing a second look for the same
* idea. Selection is driven from the address bar's own keyboard handling, which
* is why the highlighted row arrives as a prop instead of being tracked here.
*/
export const BrowserAddressSuggestions: React.FC<{
entries: readonly BrowserHistoryEntry[];
activeIndex: number;
onSelect: (url: string) => void;
onForget: (url: string) => void;
onHighlight: (index: number) => void;
}> = ({ entries, activeIndex, onSelect, onForget, onHighlight }) => {
const { t } = useI18n();
if (entries.length === 0) return null;
return (
<div
className="oc-glass-popover oc-glass-floating absolute inset-x-0 top-full z-50 mt-1 overflow-hidden rounded-xl p-1"
role="listbox"
aria-label={t('contextPanel.browser.history.label')}
>
{entries.map((entry, index) => (
<div
key={entry.url}
role="option"
aria-selected={index === activeIndex}
className={cn(
'group flex cursor-pointer items-center gap-2 rounded-lg px-2 py-1',
index === activeIndex ? 'bg-interactive-hover' : 'hover:bg-interactive-hover',
)}
// Pointer down rather than click: the address bar loses focus first,
// and a blur that closes the list would cancel the click.
onPointerDown={(event) => {
event.preventDefault();
onSelect(entry.url);
}}
onPointerEnter={() => onHighlight(index)}
>
<Icon name="global" className="size-3.5 shrink-0 text-muted-foreground" aria-hidden="true" />
<div className="min-w-0 flex-1">
<div className="truncate typography-micro text-foreground">
{entry.title || browserUrlLabel(entry.url)}
</div>
<div className="truncate typography-micro text-muted-foreground">{entry.url}</div>
</div>
<button
type="button"
className={cn(
'shrink-0 rounded-md p-1 text-muted-foreground opacity-0 transition-opacity',
'hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100',
)}
aria-label={t('contextPanel.browser.history.forget')}
title={t('contextPanel.browser.history.forget')}
onPointerDown={(event) => {
event.preventDefault();
event.stopPropagation();
onForget(entry.url);
}}
>
<Icon name="close" className="size-3" aria-hidden="true" />
</button>
</div>
))}
</div>
);
};
@@ -0,0 +1,140 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/icon/Icon';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useI18n } from '@/lib/i18n';
import { cn } from '@/lib/utils';
import {
FILL_VIEWPORT,
VIEWPORT_PRESETS,
clampViewportSize,
presetViewport,
rotateViewport,
viewportSize,
type BrowserViewport,
} from '@/lib/browser/viewport';
export type BrowserColorScheme = 'system' | 'light' | 'dark';
/**
* Size and appearance controls for the previewed page.
*
* Shown only when asked for. The width and height boxes are the source of
* truth; the preset list is a shortcut into them, which is why choosing a
* preset and then typing a size are the same action from here on.
*/
export const BrowserDeviceBar: React.FC<{
viewport: BrowserViewport;
onViewportChange: (viewport: BrowserViewport) => void;
colorScheme: BrowserColorScheme;
onColorSchemeChange: (scheme: BrowserColorScheme) => void;
scale: number;
}> = ({ viewport, onViewportChange, colorScheme, onColorSchemeChange, scale }) => {
const { t } = useI18n();
const size = viewportSize(viewport);
const presetId = viewport.kind === 'preset' ? viewport.id : '';
const commitSize = (side: 'width' | 'height', raw: string) => {
const parsed = Number.parseInt(raw, 10);
if (!Number.isFinite(parsed)) return;
const current = size ?? { width: 1280, height: 800 };
onViewportChange({
kind: 'custom',
width: clampViewportSize(side === 'width' ? parsed : current.width),
height: clampViewportSize(side === 'height' ? parsed : current.height),
});
};
const inputClass = cn(
'h-6 w-14 rounded-full border border-border/50 bg-[var(--surface-elevated)] px-2 text-center',
'typography-micro tabular-nums text-foreground outline-none focus:border-[var(--interactive-focus-ring)]',
);
return (
<div className="flex items-center gap-1.5 border-b border-border bg-[var(--surface-background)] px-2 py-1">
<select
value={presetId}
onChange={(event) => {
const next = presetViewport(event.target.value);
onViewportChange(next ?? FILL_VIEWPORT);
}}
aria-label={t('contextPanel.browser.device.preset')}
className={cn(
'h-6 shrink-0 rounded-full border border-border/50 bg-[var(--surface-elevated)] px-2',
'typography-micro text-foreground outline-none focus:border-[var(--interactive-focus-ring)]',
)}
>
<option value="">{t('contextPanel.browser.device.responsive')}</option>
{VIEWPORT_PRESETS.map((preset) => (
<option key={preset.id} value={preset.id}>{preset.label}</option>
))}
</select>
<input
value={size ? String(size.width) : ''}
onChange={(event) => commitSize('width', event.target.value)}
placeholder="—"
inputMode="numeric"
aria-label={t('contextPanel.browser.device.width')}
className={inputClass}
/>
<span className="typography-micro text-muted-foreground">×</span>
<input
value={size ? String(size.height) : ''}
onChange={(event) => commitSize('height', event.target.value)}
placeholder="—"
inputMode="numeric"
aria-label={t('contextPanel.browser.device.height')}
className={inputClass}
/>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="xs"
className="w-6 shrink-0 rounded-full px-0 text-muted-foreground hover:text-foreground"
onClick={() => onViewportChange(rotateViewport(viewport))}
disabled={!size}
aria-label={t('contextPanel.browser.device.rotate')}
>
<Icon name="refresh" className="size-3.5" aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>{t('contextPanel.browser.device.rotate')}</TooltipContent>
</Tooltip>
{/* Only worth saying when the page is not shown at its real size. */}
{size && scale < 1 ? (
<span className="shrink-0 typography-micro tabular-nums text-muted-foreground">
{Math.round(scale * 100)}%
</span>
) : null}
<div className="ml-auto flex shrink-0 items-center gap-1">
{(['system', 'light', 'dark'] as const).map((scheme) => (
<Button
key={scheme}
type="button"
variant={colorScheme === scheme ? 'secondary' : 'ghost'}
size="xs"
className={cn(
'shrink-0 rounded-full px-2.5 typography-micro',
colorScheme === scheme ? 'text-foreground' : 'text-muted-foreground hover:text-foreground',
)}
onClick={() => onColorSchemeChange(scheme)}
aria-pressed={colorScheme === scheme}
>
{t(scheme === 'system'
? 'contextPanel.browser.device.schemeSystem'
: scheme === 'light'
? 'contextPanel.browser.device.schemeLight'
: 'contextPanel.browser.device.schemeDark')}
</Button>
))}
</div>
</div>
);
};
@@ -0,0 +1,143 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/icon/Icon';
import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
import { useI18n } from '@/lib/i18n';
import { fetchDevServers, mergeDevServerCandidates, type DevServerDiscovery } from '@/lib/browser/devServers';
import { clearAnnouncedDevServers, useAnnouncedDevServers } from '@/lib/browser/announcedServers';
import { browserUrlLabel, isLoopbackUrl } from '@/lib/browser/url';
import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
/**
* What the panel shows before anything is loaded.
*
* Rather than an inert placeholder, this lists the servers actually running,
* which is almost always what the user came here to open. Discovery failure is
* stated plainly instead of being rendered as "nothing is running" the two
* mean very different things to someone whose dev server is definitely up.
*/
/** The base path a server is served under, or '' when it sits at the root. */
const pathLabel = (url: string): string => {
try {
const path = new URL(url).pathname;
return path === '/' ? '' : path;
} catch {
return '';
}
};
/** Re-checked while the panel is open: a project's servers appear seconds apart. */
const REFRESH_INTERVAL_MS = 2_000;
/**
* True when the listed servers are on another machine and this client has no
* way to reach them. The desktop shell tunnels a local port for exactly this
* case; a browser tab has no equivalent, and its `localhost` is its own.
*/
const isUnreachableFromHere = (): boolean => {
if (typeof window === 'undefined') return false;
if (window.__OPENCHAMBER_ELECTRON__) return false;
const baseUrl = getRuntimeApiBaseUrl();
if (!baseUrl) return false;
try {
return !isLoopbackUrl(new URL(baseUrl, window.location.href).toString());
} catch {
return false;
}
};
export const BrowserEmptyState: React.FC<{
onOpen: (url: string) => void;
directory?: string;
}> = ({ onOpen, directory = '' }) => {
const { t } = useI18n();
const [discovery, setDiscovery] = React.useState<DevServerDiscovery>({ kind: 'loading' });
const announced = useAnnouncedDevServers(directory);
const [remoteOnly] = React.useState(isUnreachableFromHere);
React.useEffect(() => {
let active = true;
let timer: ReturnType<typeof setTimeout> | null = null;
const controller = new AbortController();
const poll = () => {
void fetchDevServers(controller.signal).then((result) => {
if (!active) return;
setDiscovery(result);
// One look is a snapshot of whichever servers happened to be up first.
timer = setTimeout(poll, REFRESH_INTERVAL_MS);
});
};
poll();
return () => {
active = false;
if (timer) clearTimeout(timer);
controller.abort();
};
}, []);
const candidates = React.useMemo(() => mergeDevServerCandidates({
announced,
discovered: discovery.kind === 'ready' ? discovery.servers : null,
}), [announced, discovery]);
return (
// The whole panel must not scroll: a centred column that overflows clips its
// own top, and no amount of scrolling reaches it. Only the list of servers
// scrolls, and it shrinks to whatever room is left before it does.
<div className="absolute inset-0 flex flex-col items-center justify-center gap-5 overflow-hidden bg-background p-6 text-center">
<OpenChamberLogo width={110} height={110} className="shrink-0 opacity-20" />
<div className="flex shrink-0 flex-col gap-1">
<span className="typography-ui-header text-foreground">{t('contextPanel.browser.empty')}</span>
<span className="typography-micro text-muted-foreground">{t('contextPanel.browser.emptyHint')}</span>
</div>
{candidates.length > 0 ? (
<div className="flex min-h-0 w-full max-w-sm flex-col gap-1">
<span className="shrink-0 typography-micro text-left text-muted-foreground">
{announced.length > 0
? t('contextPanel.browser.devServers.justStarted')
: t('contextPanel.browser.devServers.title')}
</span>
{remoteOnly ? (
<span className="shrink-0 pb-1 text-left typography-micro text-muted-foreground">
{t('contextPanel.browser.devServers.remoteOnly')}
</span>
) : null}
<div className="flex min-h-0 flex-col gap-1 overflow-y-auto pr-0.5">
{candidates.map((candidate) => (
<Button
key={candidate.port}
type="button"
variant="outline"
size="sm"
className="w-full shrink-0 justify-start gap-2"
onClick={() => {
// The offer is answered; leaving it up would keep suggesting
// servers behind a page the user is already looking at.
clearAnnouncedDevServers(directory);
onOpen(candidate.url);
}}
>
<Icon name="global" className="size-3.5 shrink-0" aria-hidden="true" />
<span className="truncate">{browserUrlLabel(candidate.url) || candidate.url}</span>
<span className="ml-auto truncate typography-micro text-muted-foreground">
{pathLabel(candidate.url)}
</span>
</Button>
))}
</div>
</div>
) : null}
{candidates.length === 0 && discovery.kind === 'unavailable' ? (
<span className="typography-micro text-muted-foreground">
{t('contextPanel.browser.devServers.unavailable')}
</span>
) : null}
</div>
);
};
@@ -0,0 +1,907 @@
import React from 'react';
import { toast } from '@/components/ui';
import { useThemeSystem } from '@/contexts/useThemeSystem';
import { invokeDesktopCommand } from '@/lib/desktopNative';
import { useI18n } from '@/lib/i18n';
import { cn } from '@/lib/utils';
import { openExternalUrl } from '@/lib/url';
import { useUIStore } from '@/stores/useUIStore';
import { BLANK_URL, isLoopbackUrl, isStartingServerFailure, normalizeBrowserUrl } from '@/lib/browser/url';
import { probeLoopbackStatus } from '@/lib/browser/devServers';
import {
cancelAnnotationSession,
runAnnotationSession,
type AnnotationHost,
type PageCapture,
} from '@/lib/browser/annotationSession';
import { resolveAnnotationOverlayTheme } from '@/lib/browser/overlayTheme';
import { registerBrowserController } from '@/lib/browser/controlClient';
import { suggestFromHistory } from '@/lib/browser/history';
import { selectBrowserHistory, useBrowserHistoryStore } from '@/stores/useBrowserHistoryStore';
import {
DevTunnelUnavailableError,
resolveBrowsableUrl,
shouldTunnelLoopbackUrl,
toDisplayUrl,
} from '@/lib/browser/devTunnel';
import {
buildClickScript,
buildInspectScript,
buildScrollScript,
buildSnapshotScript,
buildTypeScript,
} from '@/lib/browser/pageActions';
import { BrowserToolbar } from './BrowserToolbar';
import { BrowserDeviceBar, type BrowserColorScheme } from './BrowserDeviceBar';
import {
FILL_VIEWPORT,
fitViewport,
isViewportMode,
viewportForMode,
viewportSummary,
type BrowserViewport,
} from '@/lib/browser/viewport';
import { BrowserEmptyState } from './BrowserEmptyState';
import { useAnnotationAttach, useAnnotationOverlayLabels } from './useAnnotationAttach';
import { readEventPayload, useWebviewNavigation } from './useWebviewNavigation';
export type BrowserPaneProps = {
initialUrl: string;
directory: string;
tabID: string;
};
/**
* Chromium is the only host that can give us a real page: cookies, service
* workers, HMR sockets, DevTools, and same-document access for annotation. When
* it is unavailable the surface degrades to a plain iframe that can display a
* page but cannot inspect one, rather than pretending otherwise.
*/
const isChromiumHost = (): boolean => (
typeof window !== 'undefined' && Boolean(window.__OPENCHAMBER_ELECTRON__)
);
/** How long to keep waiting for a dev server that is still coming up. */
const DEV_SERVER_WAIT_MS = 40_000;
/** Chromium's zoom is exponential: factor = 1.2 ^ level. */
const ZOOM_STEP = 0.5;
const ZOOM_MIN = -3;
const ZOOM_MAX = 4;
const BROWSER_PARTITION = 'persist:openchamber-browser';
/** Kept small: this rides along with every snapshot. */
const CONSOLE_PROBLEM_LIMIT = 20;
const DEV_SERVER_RETRY_DELAY_MS = 600;
/**
* A shorter budget for a server that *answers* but with a 5xx, which is what a
* dev gateway does while the app behind it is still starting. Kept short and
* applied only before the first good load, so a genuine server error a build
* failure page, say is shown promptly instead of being hidden behind a
* spinner.
*/
const GATEWAY_WAIT_MS = 20_000;
const WebviewBrowser: React.FC<BrowserPaneProps> = ({ initialUrl, directory, tabID }) => {
const { t } = useI18n();
const { currentTheme } = useThemeSystem();
const webviewRef = React.useRef<WebviewElement | null>(null);
// Tracked in state as well as a ref: effects that attach listeners must re-run
// when the view appears, which a stable ref cannot tell them.
const [webviewElement, setWebviewElement] = React.useState<WebviewElement | null>(null);
const attachWebview = React.useCallback((node: WebviewElement | null) => {
webviewRef.current = node;
setWebviewElement(node);
}, []);
const setContextPanelTabTargetPath = useUIStore((state) => state.setContextPanelTabTargetPath);
// Captured once: the webview owns its history from here on, and re-deriving
// this from props would drag the view back to where the tab started.
const initialUrlRef = React.useRef(normalizeBrowserUrl(initialUrl));
const startUrl = initialUrlRef.current !== BLANK_URL ? initialUrlRef.current : '';
// The view is created with its final URL already in `src`, never navigated
// into place afterwards. A tab opened in the background renders hidden, where
// an imperative navigation is lost, and mutating `src` after the element
// exists is not reliably honoured either — both leave a panel that never
// loads. `null` means "still resolving", and the view is not rendered yet.
const [initialSrc, setInitialSrc] = React.useState<string | null>(startUrl ? null : BLANK_URL);
const [address, setAddress] = React.useState(startUrl);
const [isAnnotating, setIsAnnotating] = React.useState(false);
const [isWaitingForServer, setIsWaitingForServer] = React.useState(false);
const [zoomLevel, setZoomLevel] = React.useState(0);
const [showDeviceBar, setShowDeviceBar] = React.useState(false);
const [viewport, setViewport] = React.useState<BrowserViewport>(FILL_VIEWPORT);
// Read inside agent actions, which are not re-created when the viewport
// changes and would otherwise report whatever it was when they were built.
const viewportRef = React.useRef(viewport);
viewportRef.current = viewport;
/**
* Errors and warnings the page logged, reported with the next snapshot.
*
* A page that looks right and is throwing looks identical to one that is
* fine, and finding out otherwise used to mean opening DevTools by hand.
*/
const consoleProblemsRef = React.useRef<Array<{ level: string; message: string; source: string }>>([]);
const [colorScheme, setColorScheme] = React.useState<BrowserColorScheme>('system');
const [stageSize, setStageSize] = React.useState({ width: 0, height: 0 });
const stageRef = React.useRef<HTMLDivElement | null>(null);
/** When the current run of retries began, per URL. */
const retryRef = React.useRef<{ url: string; startedAt: number } | null>(null);
/** Set once this tab has seen a page that was not a startup error. */
const servedOkRef = React.useRef(false);
const openedAtRef = React.useRef(Date.now());
const persistUrl = React.useCallback((url: string) => {
if (!url || url === BLANK_URL || !directory || !tabID) return;
setContextPanelTabTargetPath(directory, tabID, url);
}, [directory, tabID, setContextPanelTabTargetPath]);
const navigation = useWebviewNavigation(webviewElement, {
initialUrl: startUrl,
onUrlChange: React.useCallback((url: string) => {
const display = toDisplayUrl(url);
setAddress(display);
persistUrl(display);
}, [persistUrl]),
});
/** Set when a remote dev server could not be reached from this machine. */
const [tunnelFailedUrl, setTunnelFailedUrl] = React.useState<string | null>(null);
const attachAnnotation = useAnnotationAttach(directory);
const overlayLabels = useAnnotationOverlayLabels();
const isLoading = navigation.status.kind === 'loading';
const history = useBrowserHistoryStore(selectBrowserHistory(directory));
const recordHistoryVisit = useBrowserHistoryStore((state) => state.recordVisit);
const forgetHistoryVisit = useBrowserHistoryStore((state) => state.forget);
// Recorded once a page has actually loaded, and with the title it reported:
// an address that failed to open is not somewhere to offer going back to.
React.useEffect(() => {
if (navigation.status.kind !== 'ready') return;
recordHistoryVisit(directory, {
url: toDisplayUrl(navigation.status.url),
title: navigation.status.title,
});
}, [directory, navigation.status, recordHistoryVisit]);
const suggestions = React.useMemo(
() => suggestFromHistory(history, address),
[history, address],
);
const loadUrl = React.useCallback((value: string) => {
const next = normalizeBrowserUrl(value);
if (next === BLANK_URL) return;
// The address bar shows what the user asked for; a tunnel only changes
// where the bytes come from, and surfacing 127.0.0.1:<random> would be
// confusing and useless to copy.
setAddress(next);
setTunnelFailedUrl(null);
void resolveBrowsableUrl(next).then((target) => {
const webview = webviewRef.current;
if (!webview) {
setInitialSrc(target);
return;
}
try {
webview.loadURL(target);
} catch {
// Not attached yet: hand the navigation to the attribute, which
// Chromium applies once the view attaches.
setInitialSrc(target);
}
}).catch((error: unknown) => {
// Loading the address here anyway would answer from this machine while
// showing the remote one's address. Say what happened instead.
if (error instanceof DevTunnelUnavailableError) setTunnelFailedUrl(next);
});
}, []);
// Resolving through the tunnel is what lets a persisted loopback URL reach a
// dev server on a remote host; locally it returns the URL unchanged.
React.useEffect(() => {
if (!startUrl) return;
let active = true;
void resolveBrowsableUrl(startUrl)
.then((target) => { if (active) setInitialSrc(target); })
.catch((error: unknown) => {
if (!active) return;
if (error instanceof DevTunnelUnavailableError) {
// The view still needs a src or the panel stays blank forever; it
// gets a blank one, with the failure stated over it.
setTunnelFailedUrl(startUrl);
setInitialSrc(BLANK_URL);
return;
}
setInitialSrc(startUrl);
});
return () => { active = false; };
// Only ever the initial navigation; later changes come from the user.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const annotationHost = React.useMemo<AnnotationHost>(() => ({
executeJavaScript: async (code: string, userGesture?: boolean) => {
const webview = webviewRef.current;
if (!webview) throw new Error('Browser view is not available');
return webview.executeJavaScript(code, userGesture);
},
capturePage: async (): Promise<PageCapture | null> => {
const webview = webviewRef.current;
if (!webview) return null;
const webContentsId = webview.getWebContentsId();
if (!Number.isFinite(webContentsId)) return null;
return await invokeDesktopCommand<PageCapture>('desktop_browser_capture_page', { webContentsId });
},
}), []);
const handleAnnotate = React.useCallback(() => {
if (isAnnotating) {
setIsAnnotating(false);
void cancelAnnotationSession(annotationHost);
return;
}
if (!navigation.url) {
toast.error(t('contextPanel.browser.annotate.noPage'));
return;
}
const theme = resolveAnnotationOverlayTheme(
currentTheme.metadata.variant === 'light' ? 'light' : 'dark',
);
setIsAnnotating(true);
void runAnnotationSession({
host: annotationHost,
theme,
labels: overlayLabels,
})
.then(async (result) => {
setIsAnnotating(false);
if (!result) return;
await attachAnnotation(result);
})
.catch(() => {
setIsAnnotating(false);
toast.error(t('contextPanel.browser.annotate.failed'));
});
}, [annotationHost, attachAnnotation, currentTheme, isAnnotating, navigation.url, overlayLabels, t]);
// Escape leaves annotation mode from the app side too: the overlay owns the
// in-page Escape, but the panel can be focused instead.
React.useEffect(() => {
if (!isAnnotating) return;
const handler = (event: KeyboardEvent) => {
if (event.key !== 'Escape') return;
event.preventDefault();
event.stopImmediatePropagation();
setIsAnnotating(false);
void cancelAnnotationSession(annotationHost);
};
window.addEventListener('keydown', handler, true);
return () => window.removeEventListener('keydown', handler, true);
}, [annotationHost, isAnnotating]);
// Agent-driven actions. Waiting for the page to settle after a navigation is
// deliberate: a snapshot taken mid-load describes a page that no longer
// exists by the time the agent reads it.
const waitForIdle = React.useCallback(async (timeoutMs = 8_000): Promise<boolean> => {
const startedAt = Date.now();
for (;;) {
const webview = webviewRef.current;
if (!webview) return false;
let busy = false;
try {
busy = webview.isLoading();
} catch {
return false;
}
if (!busy) return true;
// A page with a looping video or a long-lived stream can report loading
// indefinitely. Give up waiting and act on it anyway rather than letting
// the whole action expire.
if (Date.now() - startedAt > timeoutMs) return false;
await new Promise((resolve) => setTimeout(resolve, 120));
}
}, []);
const runControlAction = React.useCallback(async (
action: string,
parameters: Record<string, unknown>,
): Promise<unknown> => {
const webview = webviewRef.current;
if (!webview) throw new Error('The browser panel is not ready');
// Showing the bar when the agent sizes the page keeps the change visible:
// the user should see which layout is being looked at, not just that it
// suddenly narrowed.
const applyViewportParameter = (): void => {
if (!isViewportMode(parameters.viewport)) return;
setViewport(viewportForMode(parameters.viewport));
setShowDeviceBar(true);
};
if (action === 'browser.back' || action === 'browser.forward') {
const goingBack = action === 'browser.back';
const canMove = goingBack ? webview.canGoBack() : webview.canGoForward();
if (!canMove) {
throw new Error(goingBack
? 'There is nothing to go back to in this tab'
: 'There is nothing to go forward to in this tab');
}
if (goingBack) webview.goBack();
else webview.goForward();
await new Promise((resolve) => setTimeout(resolve, 150));
await waitForIdle();
let title = '';
try { title = webview.getTitle() || ''; } catch { title = ''; }
return { url: toDisplayUrl(webview.getURL()), title };
}
if (action === 'browser.capture') {
// Wait for a settled page first: a screenshot of a half-painted layout is
// worse than none, because it looks like a finished one.
await waitForIdle();
const capture = await annotationHost.capturePage();
if (!capture) throw new Error('The page could not be captured');
let title = '';
try { title = webview.getTitle() || ''; } catch { title = ''; }
return {
...capture,
url: toDisplayUrl(webview.getURL()),
title,
viewport: viewportSummary(viewportRef.current),
};
}
if (action === 'browser.resize') {
if (!isViewportMode(parameters.viewport)) throw new Error('viewport is required');
applyViewportParameter();
// Let the resize land before reporting it, so a snapshot that follows
// describes the new layout rather than the old one.
await new Promise((resolve) => setTimeout(resolve, 200));
await waitForIdle();
return { viewport: viewportSummary(viewportForMode(parameters.viewport)) };
}
if (action === 'browser.open') {
const url = typeof parameters.url === 'string' ? parameters.url : '';
if (!url) throw new Error('url is required');
applyViewportParameter();
loadUrl(url);
await new Promise((resolve) => setTimeout(resolve, 150));
const settled = await waitForIdle(25_000);
let title = '';
try {
title = webview.getTitle() || '';
} catch {
title = '';
}
// `settled: false` means the page is still fetching, not that opening
// failed — the agent can snapshot it and decide for itself.
return {
url: normalizeBrowserUrl(url),
title,
opened: true,
settled,
viewport: viewportSummary(viewportRef.current),
};
}
await waitForIdle();
const asOptionalString = (value: unknown): string | undefined => (
typeof value === 'string' && value ? value : undefined
);
const buildScript = (): string | null => {
switch (action) {
case 'browser.snapshot':
return buildSnapshotScript({ selector: asOptionalString(parameters.selector) });
case 'browser.click':
return buildClickScript({
selector: asOptionalString(parameters.selector),
text: asOptionalString(parameters.text),
});
case 'browser.type':
return buildTypeScript({
selector: String(parameters.selector ?? ''),
value: String(parameters.value ?? ''),
submit: parameters.submit === true,
});
case 'browser.inspect':
return buildInspectScript({ selector: String(parameters.selector ?? '') });
case 'browser.scroll':
return buildScrollScript({
selector: asOptionalString(parameters.selector),
direction: asOptionalString(parameters.direction),
});
default:
return null;
}
};
const script = buildScript();
if (!script) throw new Error(`Unsupported browser action: ${action}`);
const result = await webview.executeJavaScript(script, true);
if (!result || typeof result !== 'object') {
throw new Error('The page returned no result');
}
const record = result as Record<string, unknown>;
if (record.ok !== true) {
throw new Error(typeof record.error === 'string' && record.error ? record.error : 'Browser action failed');
}
// A snapshot has to say which layout it describes, or the agent cannot tell
// a mobile rendering from a desktop one.
if (action === 'browser.snapshot') {
const problems = consoleProblemsRef.current;
return {
...record,
viewport: viewportSummary(viewportRef.current),
...(problems.length > 0 ? { consoleProblems: [...problems] } : {}),
};
}
// A click or a submit commonly starts a navigation; let it land so the
// agent's next snapshot sees the page the action produced.
if (action === 'browser.click' || (action === 'browser.type' && parameters.submit === true)) {
await new Promise((resolve) => setTimeout(resolve, 150));
await waitForIdle();
}
return result;
}, [annotationHost, loadUrl, waitForIdle]);
React.useEffect(
() => registerBrowserController({ run: runControlAction }),
[runControlAction],
);
// Leaving the tab must not strand an overlay or live style overrides on the page.
React.useEffect(() => {
const host = annotationHost;
return () => { void cancelAnnotationSession(host); };
}, [annotationHost]);
React.useEffect(() => {
if (!webviewElement) return;
const onConsoleMessage = (event: Event) => {
const detail = event as unknown as { level?: number; message?: string; sourceId?: string; line?: number };
// 2 is warning, 3 is error; anything quieter is the page talking to itself.
if (typeof detail.level !== 'number' || detail.level < 2) return;
const source = detail.sourceId ? `${detail.sourceId}${detail.line ? `:${detail.line}` : ''}` : '';
consoleProblemsRef.current.push({
level: detail.level >= 3 ? 'error' : 'warning',
message: String(detail.message ?? '').slice(0, 400),
source,
});
if (consoleProblemsRef.current.length > CONSOLE_PROBLEM_LIMIT) {
consoleProblemsRef.current.splice(0, consoleProblemsRef.current.length - CONSOLE_PROBLEM_LIMIT);
}
};
// Each page gets its own record; carrying the last one over would blame a
// new page for the previous page's failures.
const onStartLoading = () => { consoleProblemsRef.current = []; };
webviewElement.addEventListener('console-message', onConsoleMessage);
webviewElement.addEventListener('did-start-loading', onStartLoading);
return () => {
webviewElement.removeEventListener('console-message', onConsoleMessage);
webviewElement.removeEventListener('did-start-loading', onStartLoading);
};
}, [webviewElement]);
/**
* Keeps loopback navigations on the machine the page came from.
*
* A tunnelled page can send the view to another local port a docs server
* behind a dev gateway, an API on its own port. That navigation happens
* inside the view, so nothing resolved it, and it would be looked for on this
* machine instead of the host.
*
* A link or a script navigation is caught before it happens. A server
* redirect cannot be: by the time the view reports it, it is already loading.
* That one is recovered from its failure instead, once per address, so a port
* that genuinely is not there still fails honestly.
*/
const retunneledUrlsRef = React.useRef(new Set<string>());
// Asking for an address again is a fresh request, so the recovery budget
// comes back with it. The automatic retry deliberately does not reset it.
const loadUrlFromUser = React.useCallback((value: string) => {
retunneledUrlsRef.current.clear();
loadUrl(value);
}, [loadUrl]);
React.useEffect(() => {
if (!webviewElement) return;
const onWillNavigate = (event: Event) => {
const detail = readEventPayload<{ url?: string }>(event);
const target = typeof detail.url === 'string' ? detail.url : '';
if (!target || !shouldTunnelLoopbackUrl(target)) return;
event.preventDefault();
loadUrl(target);
};
const onFailLoad = (event: Event) => {
const detail = readEventPayload<{
errorCode?: number;
validatedURL?: string;
isMainFrame?: boolean;
}>(event);
if (detail.isMainFrame === false) return;
// Superseded navigations are not failures.
if (detail.errorCode === -3) return;
const target = typeof detail.validatedURL === 'string' ? detail.validatedURL : '';
if (!target || !shouldTunnelLoopbackUrl(target)) return;
if (retunneledUrlsRef.current.has(target)) return;
retunneledUrlsRef.current.add(target);
loadUrl(target);
};
webviewElement.addEventListener('will-navigate', onWillNavigate);
webviewElement.addEventListener('did-fail-load', onFailLoad);
return () => {
webviewElement.removeEventListener('will-navigate', onWillNavigate);
webviewElement.removeEventListener('did-fail-load', onFailLoad);
};
}, [loadUrl, webviewElement]);
// Popups open in place; a detached window would escape the panel entirely.
React.useEffect(() => {
if (!webviewElement) return;
const onNewWindow = (event: Event) => {
const detail = (event as CustomEvent<{ url?: string }>).detail;
event.preventDefault();
if (detail?.url) loadUrl(detail.url);
};
webviewElement.addEventListener('new-window', onNewWindow);
return () => webviewElement.removeEventListener('new-window', onNewWindow);
}, [loadUrl, webviewElement]);
const applyZoom = React.useCallback((level: number) => {
const next = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, level));
setZoomLevel(next);
try {
webviewRef.current?.setZoomLevel(next);
} catch {
// Not attached yet; the next change applies it.
}
}, []);
const clearBrowsingData = React.useCallback((what: 'cookies' | 'cache') => {
void invokeDesktopCommand('desktop_browser_clear_data', {
partition: BROWSER_PARTITION,
cookies: what === 'cookies',
cache: what === 'cache',
})
.then(() => {
toast.success(t(what === 'cookies'
? 'contextPanel.browser.clearedCookies'
: 'contextPanel.browser.clearedCache'));
// Cleared storage only shows in a page that reloads without it.
try { webviewRef.current?.reloadIgnoringCache(); } catch { /* not attached */ }
})
.catch(() => toast.error(t('contextPanel.browser.clearFailed')));
}, [t]);
// The stage is measured rather than assumed: the panel is resizable, and a
// viewport that fitted a moment ago may not fit now.
React.useEffect(() => {
const stage = stageRef.current;
if (!stage || typeof ResizeObserver === 'undefined') return;
const observer = new ResizeObserver((entries) => {
const rect = entries[0]?.contentRect;
if (rect) setStageSize({ width: rect.width, height: rect.height });
});
observer.observe(stage);
return () => observer.disconnect();
}, []);
const applyColorScheme = React.useCallback((scheme: BrowserColorScheme) => {
setColorScheme(scheme);
const webview = webviewRef.current;
if (!webview) return;
let webContentsId = -1;
try {
webContentsId = webview.getWebContentsId();
} catch {
return;
}
void invokeDesktopCommand('desktop_browser_set_color_scheme', { webContentsId, scheme })
.catch((error: unknown) => {
setColorScheme('system');
toast.error(error instanceof Error ? error.message : t('contextPanel.browser.device.schemeFailed'));
});
}, [t]);
const handleReload = React.useCallback(() => {
try {
if (isLoading) webviewRef.current?.stop();
else webviewRef.current?.reload();
} catch {
// Not attached yet.
}
}, [isLoading]);
// A page opened the moment its dev server launched is not ready twice over:
// first nothing is listening at all, then a gateway answers while the app
// behind it is still starting. Neither is an error the user can act on, and
// both used to leave them pressing reload. Both are waited out here.
const status = navigation.status;
React.useEffect(() => {
const reloadSoon = (): (() => void) => {
setIsWaitingForServer(true);
const timer = setTimeout(() => {
try {
webviewRef.current?.reload();
} catch {
// View went away; the next mount starts over.
}
}, DEV_SERVER_RETRY_DELAY_MS);
return () => clearTimeout(timer);
};
// Nothing is listening yet.
if (status.kind === 'failed') {
if (!isStartingServerFailure(status.code, status.url)) {
setIsWaitingForServer(false);
return;
}
const now = Date.now();
const run = retryRef.current?.url === status.url
? retryRef.current
: { url: status.url, startedAt: now };
retryRef.current = run;
if (now - run.startedAt > DEV_SERVER_WAIT_MS) {
setIsWaitingForServer(false);
return;
}
return reloadSoon();
}
// Mid-navigation: leave whatever state the previous decision set, so a
// retry does not flash the page behind the waiting screen and back.
if (status.kind === 'loading') return;
retryRef.current = null;
if (status.kind !== 'ready' || !status.url || !isLoopbackUrl(status.url)) {
servedOkRef.current = true;
setIsWaitingForServer(false);
return;
}
if (servedOkRef.current || Date.now() - openedAtRef.current > GATEWAY_WAIT_MS) {
servedOkRef.current = true;
setIsWaitingForServer(false);
return;
}
// The page loaded, but a 5xx here means the server answered on behalf of an
// app that is not up yet. Checked by status rather than by reading the page:
// guessing from its contents would mean encoding what each dev server's
// error page looks like, which is exactly the trap this panel came out of.
let cancelled = false;
let cancelReload: (() => void) | null = null;
// Probe the address on the host, not the local tunnel port: the check runs
// on the server, where our ephemeral port means nothing. Asking about it
// failed every time, which read as "settled" and left the page on the error
// until a manual reload.
void probeLoopbackStatus(toDisplayUrl(status.url)).then((httpStatus) => {
if (cancelled) return;
if (httpStatus === null || httpStatus < 500) {
servedOkRef.current = true;
setIsWaitingForServer(false);
return;
}
cancelReload = reloadSoon();
});
return () => {
cancelled = true;
cancelReload?.();
};
}, [status]);
const failed = navigation.status.kind === 'failed' && !isWaitingForServer ? navigation.status : null;
const layout = fitViewport(viewport, stageSize);
return (
<div className="absolute inset-0 flex flex-col bg-background">
<BrowserToolbar
address={address}
onAddressChange={setAddress}
onSubmit={loadUrlFromUser}
suggestions={suggestions}
onForgetSuggestion={(url) => forgetHistoryVisit(directory, url)}
onBack={() => { try { webviewRef.current?.goBack(); } catch { /* not attached */ } }}
onForward={() => { try { webviewRef.current?.goForward(); } catch { /* not attached */ } }}
onReload={handleReload}
onOpenExternal={() => void openExternalUrl(navigation.url || address)}
canGoBack={navigation.canGoBack}
canGoForward={navigation.canGoForward}
isLoading={isLoading}
onAnnotate={handleAnnotate}
isAnnotating={isAnnotating}
onOpenDevTools={() => { try { webviewRef.current?.openDevTools(); } catch { /* not attached */ } }}
onHardReload={() => { try { webviewRef.current?.reloadIgnoringCache(); } catch { /* not attached */ } }}
onZoomIn={() => applyZoom(zoomLevel + ZOOM_STEP)}
onZoomOut={() => applyZoom(zoomLevel - ZOOM_STEP)}
onZoomReset={() => applyZoom(0)}
zoomPercent={Math.round(Math.pow(1.2, zoomLevel) * 100)}
onClearCookies={() => clearBrowsingData('cookies')}
onClearCache={() => clearBrowsingData('cache')}
onToggleDeviceBar={() => setShowDeviceBar((current) => !current)}
isDeviceBarOpen={showDeviceBar}
/>
{showDeviceBar ? (
<BrowserDeviceBar
viewport={viewport}
onViewportChange={setViewport}
colorScheme={colorScheme}
onColorSchemeChange={applyColorScheme}
scale={layout?.scale ?? 1}
/>
) : null}
<div
ref={stageRef}
className={cn(
'relative min-h-0 flex-1 bg-background',
// A sized viewport sits on a backdrop so its edges are visible; at
// fill there is nothing to frame.
layout && 'flex items-center justify-center overflow-hidden bg-[var(--surface-muted)]',
)}
>
{initialSrc !== null ? (
<webview
ref={attachWebview}
src={initialSrc}
partition="persist:openchamber-browser"
allowpopups
style={layout
? {
// Laid out at the chosen size and scaled visually: the page must
// measure itself at the width being tested, not at the panel's.
width: `${layout.width}px`,
height: `${layout.height}px`,
transform: `scale(${layout.scale})`,
border: 'none',
flex: 'none',
boxShadow: '0 2px 18px rgba(0,0,0,.28)',
}
: { width: '100%', height: '100%', border: 'none' }}
/>
) : null}
{initialSrc !== null && !startUrl && !navigation.url && !isLoading ? (
<BrowserEmptyState onOpen={loadUrlFromUser} directory={directory} />
) : null}
{isWaitingForServer ? (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2 bg-background p-6 text-center">
<span className="typography-ui-header text-foreground">{t('contextPanel.browser.waitingForServer')}</span>
<span className="typography-micro text-muted-foreground">{t('contextPanel.browser.waitingForServerHint')}</span>
</div>
) : null}
{tunnelFailedUrl ? (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2 bg-background p-6 text-center">
<span className="typography-ui-header text-foreground">{t('contextPanel.browser.tunnelFailed')}</span>
<span className="typography-micro text-muted-foreground">
{t('contextPanel.browser.tunnelFailedHint', { url: tunnelFailedUrl })}
</span>
</div>
) : null}
{failed && !tunnelFailedUrl ? (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2 bg-background p-6 text-center">
<span className="typography-ui-header text-foreground">
{failed.crashed ? t('contextPanel.browser.crashed') : t('contextPanel.browser.loadFailed')}
</span>
<span className="typography-micro text-muted-foreground">
{failed.crashed
? t('contextPanel.browser.crashedHint')
: failed.description || t('contextPanel.browser.loadFailedUnknown')}
</span>
</div>
) : null}
{isLoading ? (
<div className="pointer-events-none absolute inset-x-0 top-0 h-0.5 overflow-hidden">
<div className="h-full w-1/3 animate-[browser-progress_1.1s_ease-in-out_infinite] bg-[var(--primary)]" />
</div>
) : null}
</div>
</div>
);
};
/**
* Non-Chromium runtimes get a plain iframe. Same-origin policy makes the page
* opaque to us here: no navigation events, no annotation, no console. The
* toolbar reflects that instead of offering controls that would silently fail.
*/
const IframeBrowser: React.FC<BrowserPaneProps> = ({ initialUrl, directory, tabID }) => {
const { t } = useI18n();
const setContextPanelTabTargetPath = useUIStore((state) => state.setContextPanelTabTargetPath);
const normalized = normalizeBrowserUrl(initialUrl);
const startUrl = normalized !== BLANK_URL ? normalized : '';
const [address, setAddress] = React.useState(startUrl);
const [loadedUrl, setLoadedUrl] = React.useState(startUrl);
const [history, setHistory] = React.useState<string[]>(startUrl ? [startUrl] : []);
const [historyIndex, setHistoryIndex] = React.useState(startUrl ? 0 : -1);
const [reloadNonce, bumpReload] = React.useReducer((value: number) => value + 1, 0);
const persistUrl = React.useCallback((url: string) => {
if (!url || url === BLANK_URL || !directory || !tabID) return;
setContextPanelTabTargetPath(directory, tabID, url);
}, [directory, tabID, setContextPanelTabTargetPath]);
const visitedAddresses = useBrowserHistoryStore(selectBrowserHistory(directory));
const recordHistoryVisit = useBrowserHistoryStore((state) => state.recordVisit);
const forgetHistoryVisit = useBrowserHistoryStore((state) => state.forget);
const suggestions = React.useMemo(
() => suggestFromHistory(visitedAddresses, address),
[visitedAddresses, address],
);
const navigate = React.useCallback((value: string) => {
const next = normalizeBrowserUrl(value);
if (next === BLANK_URL) return;
setAddress(next);
setLoadedUrl(next);
persistUrl(next);
// The page is opaque here, so there is no load event and no title to wait
// for; what was asked for is the only thing this runtime can record.
recordHistoryVisit(directory, { url: next });
setHistory((current) => {
const kept = historyIndex >= 0 ? current.slice(0, historyIndex + 1) : [];
if (kept[kept.length - 1] === next) {
setHistoryIndex(kept.length - 1);
return kept;
}
setHistoryIndex(kept.length);
return [...kept, next];
});
}, [directory, historyIndex, persistUrl, recordHistoryVisit]);
const goTo = React.useCallback((index: number) => {
const next = history[index];
if (!next) return;
setHistoryIndex(index);
setAddress(next);
setLoadedUrl(next);
persistUrl(next);
}, [history, persistUrl]);
return (
<div className="absolute inset-0 flex flex-col bg-background">
<BrowserToolbar
address={address}
onAddressChange={setAddress}
onSubmit={navigate}
suggestions={suggestions}
onForgetSuggestion={(url) => forgetHistoryVisit(directory, url)}
onBack={() => goTo(historyIndex - 1)}
onForward={() => goTo(historyIndex + 1)}
onReload={bumpReload}
onOpenExternal={() => void openExternalUrl(loadedUrl || address)}
canGoBack={historyIndex > 0}
canGoForward={historyIndex >= 0 && historyIndex < history.length - 1}
isLoading={false}
/>
<div className="relative min-h-0 flex-1 bg-background">
{loadedUrl ? (
<iframe
key={`${loadedUrl}|${reloadNonce}`}
src={loadedUrl}
title={t('contextPanel.browser.frameTitle')}
className="h-full w-full border-none bg-white"
sandbox="allow-scripts allow-forms allow-same-origin allow-popups"
/>
) : (
<BrowserEmptyState onOpen={navigate} />
)}
</div>
</div>
);
};
export const BrowserPane: React.FC<BrowserPaneProps> = (props) => {
const [chromium] = React.useState(isChromiumHost);
return chromium ? <WebviewBrowser {...props} /> : <IframeBrowser {...props} />;
};
@@ -0,0 +1,241 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/icon/Icon';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useI18n } from '@/lib/i18n';
import { BrowserAddressSuggestions } from './BrowserAddressSuggestions';
import type { BrowserHistoryEntry } from '@/lib/browser/history';
import { cn } from '@/lib/utils';
import type { IconName } from '@/components/icon/icons';
type ToolbarButtonProps = {
icon: IconName;
label: string;
onClick: () => void;
disabled?: boolean;
pressed?: boolean;
};
const ToolbarButton: React.FC<ToolbarButtonProps> = ({ icon, label, onClick, disabled, pressed }) => (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant={pressed ? 'secondary' : 'ghost'}
size="xs"
className={cn(
'w-6 shrink-0 rounded-full px-0 text-muted-foreground',
'hover:text-foreground',
pressed && 'text-foreground',
)}
onClick={onClick}
disabled={disabled}
aria-label={label}
aria-pressed={pressed}
>
<Icon name={icon} className="size-3.5" aria-hidden="true" />
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>{label}</TooltipContent>
</Tooltip>
);
export type BrowserToolbarProps = {
address: string;
/** Addresses already visited in this project, offered while typing. */
suggestions?: readonly BrowserHistoryEntry[];
onForgetSuggestion?: (url: string) => void;
onAddressChange: (value: string) => void;
onSubmit: (value: string) => void;
onBack: () => void;
onForward: () => void;
onReload: () => void;
onOpenExternal: () => void;
canGoBack: boolean;
canGoForward: boolean;
isLoading: boolean;
/** These need a real Chromium host; hidden without one. */
onAnnotate?: () => void;
onOpenDevTools?: () => void;
isAnnotating?: boolean;
onHardReload?: () => void;
onZoomIn?: () => void;
onZoomOut?: () => void;
onZoomReset?: () => void;
/** Whole percent, e.g. 110. Controls hide at 100 to keep the bar quiet. */
zoomPercent?: number;
onClearCookies?: () => void;
onClearCache?: () => void;
onToggleDeviceBar?: () => void;
isDeviceBarOpen?: boolean;
};
export const BrowserToolbar: React.FC<BrowserToolbarProps> = ({
address,
suggestions = [],
onForgetSuggestion,
onAddressChange,
onSubmit,
onBack,
onForward,
onReload,
onOpenExternal,
canGoBack,
canGoForward,
isLoading,
onAnnotate,
onOpenDevTools,
isAnnotating,
onHardReload,
onZoomIn,
onZoomOut,
onZoomReset,
zoomPercent = 100,
onClearCookies,
onClearCache,
onToggleDeviceBar,
isDeviceBarOpen,
}) => {
const { t } = useI18n();
const [isAddressFocused, setIsAddressFocused] = React.useState(false);
const [activeSuggestion, setActiveSuggestion] = React.useState(-1);
const visibleSuggestions = isAddressFocused ? suggestions : [];
// A new list is a new choice; keeping an old index would highlight whatever
// happens to sit in that position now.
React.useEffect(() => {
setActiveSuggestion(-1);
}, [address, isAddressFocused]);
const submitAddress = (value: string) => {
setIsAddressFocused(false);
setActiveSuggestion(-1);
onSubmit(value);
};
const onAddressKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (visibleSuggestions.length === 0) return;
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault();
const step = event.key === 'ArrowDown' ? 1 : -1;
const count = visibleSuggestions.length;
// Wraps through "nothing selected", so the typed address stays reachable.
setActiveSuggestion((current) => {
const next = current + step;
if (next < -1) return count - 1;
if (next >= count) return -1;
return next;
});
return;
}
if (event.key === 'Escape' && activeSuggestion >= 0) {
event.preventDefault();
setActiveSuggestion(-1);
return;
}
if (event.key === 'Enter' && activeSuggestion >= 0) {
event.preventDefault();
const chosen = visibleSuggestions[activeSuggestion];
if (chosen) submitAddress(chosen.url);
}
};
return (
<div className="flex items-center gap-1 border-b border-border bg-[var(--surface-background)] px-2 py-1">
<ToolbarButton icon="arrow-left" label={t('contextPanel.browser.back')} onClick={onBack} disabled={!canGoBack} />
<ToolbarButton icon="arrow-right" label={t('contextPanel.browser.forward')} onClick={onForward} disabled={!canGoForward} />
<ToolbarButton
icon="refresh"
label={isLoading ? t('contextPanel.browser.stop') : t('contextPanel.browser.reload')}
onClick={onReload}
/>
{onHardReload ? (
<ToolbarButton icon="restart" label={t('contextPanel.browser.hardReload')} onClick={onHardReload} />
) : null}
<form
className="relative min-w-0 flex-1"
onSubmit={(event) => {
event.preventDefault();
submitAddress(address);
}}
>
<input
value={address}
onChange={(event) => onAddressChange(event.target.value)}
onFocus={() => setIsAddressFocused(true)}
onBlur={() => setIsAddressFocused(false)}
onKeyDown={onAddressKeyDown}
spellCheck={false}
autoComplete="off"
role="combobox"
aria-expanded={visibleSuggestions.length > 0}
aria-controls="openchamber-browser-address-suggestions"
className={cn(
'h-6 w-full rounded-full border border-border/50 bg-[var(--surface-elevated)] px-3',
'typography-micro text-foreground outline-none focus:border-[var(--interactive-focus-ring)]',
)}
aria-label={t('contextPanel.browser.addressAria')}
/>
<div id="openchamber-browser-address-suggestions">
<BrowserAddressSuggestions
entries={visibleSuggestions}
activeIndex={activeSuggestion}
onSelect={submitAddress}
onForget={(url) => onForgetSuggestion?.(url)}
onHighlight={setActiveSuggestion}
/>
</div>
</form>
{onZoomOut && onZoomIn ? (
<div className="flex shrink-0 items-center">
<ToolbarButton icon="subtract" label={t('contextPanel.browser.zoomOut')} onClick={onZoomOut} />
{zoomPercent !== 100 && onZoomReset ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="xs"
className="shrink-0 rounded-full px-1.5 typography-micro tabular-nums text-muted-foreground"
onClick={onZoomReset}
aria-label={t('contextPanel.browser.zoomReset')}
>
{zoomPercent}%
</Button>
</TooltipTrigger>
<TooltipContent sideOffset={6}>{t('contextPanel.browser.zoomReset')}</TooltipContent>
</Tooltip>
) : null}
<ToolbarButton icon="add" label={t('contextPanel.browser.zoomIn')} onClick={onZoomIn} />
</div>
) : null}
{onClearCookies ? (
<ToolbarButton icon="delete-bin" label={t('contextPanel.browser.clearCookies')} onClick={onClearCookies} />
) : null}
{onClearCache ? (
<ToolbarButton icon="database-2" label={t('contextPanel.browser.clearCache')} onClick={onClearCache} />
) : null}
{onToggleDeviceBar ? (
<ToolbarButton
icon="smartphone"
label={t('contextPanel.browser.deviceToolbar')}
onClick={onToggleDeviceBar}
pressed={isDeviceBarOpen}
/>
) : null}
{onAnnotate ? (
<ToolbarButton
icon="markup"
label={t('contextPanel.browser.annotate.toggle')}
onClick={onAnnotate}
pressed={isAnnotating}
/>
) : null}
{onOpenDevTools ? (
<ToolbarButton icon="terminal-box" label={t('contextPanel.browser.devTools')} onClick={onOpenDevTools} />
) : null}
<ToolbarButton icon="external-link" label={t('contextPanel.browser.openExternal')} onClick={onOpenExternal} />
</div>
);
};
@@ -0,0 +1,71 @@
import React from 'react';
import { toast } from '@/components/ui';
import { useI18n } from '@/lib/i18n';
import { useInlineCommentDraftStore } from '@/stores/useInlineCommentDraftStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useInputStore } from '@/sync/input-store';
import { formatBrowserAnnotationPrompt } from '@/lib/browser/annotationPrompt';
import type { AnnotationSessionResult } from '@/lib/browser/annotationSession';
import type { BrowserAnnotationOverlayLabels } from '@/lib/browser/annotationOverlay';
/**
* Attaches a finished annotation to the active composer.
*
* The screenshot is attached before the text so that a failed image upload
* cannot leave a prompt claiming an attachment that never arrived the text
* states what actually happened.
*/
export const useAnnotationAttach = (directory: string) => {
const { t } = useI18n();
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const newSessionDraftOpen = useSessionUIStore((state) => state.newSessionDraft?.open);
const addInlineCommentDraft = useInlineCommentDraftStore((state) => state.addDraft);
const addAttachedFile = useInputStore((state) => state.addAttachedFile);
return React.useCallback(async (result: AnnotationSessionResult): Promise<void> => {
const sessionKey = currentSessionId ?? (newSessionDraftOpen ? 'draft' : null);
if (!sessionKey) {
toast.error(t('contextPanel.browser.annotate.noSession'));
return;
}
let screenshotAttached = false;
if (result.screenshot) {
try {
await addAttachedFile(result.screenshot);
screenshotAttached = true;
} catch {
screenshotAttached = false;
}
}
addInlineCommentDraft({ directory, sessionKey }, {
source: 'preview-annotation',
fileLabel: result.payload.pageUrl || 'browser',
startLine: 1,
endLine: 1,
code: formatBrowserAnnotationPrompt({
payload: result.payload,
screenshotAttached,
intro: t('contextPanel.browser.annotate.intro'),
}),
language: 'markdown',
text: '',
});
toast.success(t('contextPanel.browser.annotate.attached'));
}, [addAttachedFile, addInlineCommentDraft, currentSessionId, directory, newSessionDraftOpen, t]);
};
/** Overlay labels, resolved through i18n so the in-page UI follows the app locale. */
export const useAnnotationOverlayLabels = (): BrowserAnnotationOverlayLabels => {
const { t } = useI18n();
return React.useMemo(() => ({
select: t('contextPanel.browser.annotate.tool.element'),
marquee: t('contextPanel.browser.annotate.tool.region'),
draw: t('contextPanel.browser.annotate.tool.draw'),
commentPlaceholder: t('contextPanel.browser.annotate.commentPlaceholder'),
submit: t('contextPanel.browser.annotate.submit'),
}), [t]);
};
@@ -0,0 +1,237 @@
import React from 'react';
import { IDLE_NAV_STATUS, type BrowserNavStatus } from '@/lib/browser/contract';
import { useBrowserFaviconStore } from '@/stores/useBrowserFaviconStore';
import {
INITIAL_CRASH_RECOVERY_STATE,
planCrashRecovery,
type CrashRecoveryState,
} from '@/lib/browser/crashRecovery';
/**
* Translates `<webview>` lifecycle events into a single navigation status.
*
* Chromium reports failures and successes through separate events that can
* arrive in either order, and it emits `did-fail-load` for sub-resources as
* well as for the main frame. Both are handled here so the panel only ever
* sees one authoritative state:
*
* - Sub-frame failures are ignored; only the main frame changes the status.
* - `ERR_ABORTED` is not a failure. It is what Chromium reports when a
* navigation is superseded by the next one, and treating it as an error puts
* an error screen over a page that is loading perfectly well.
*
* Takes the element rather than a ref so the listeners attach when the view
* actually appears. A ref is stable, so an effect keyed on it runs once and
* silently attaches nothing at all when the view mounts a render later.
*
* `<webview>` puts its event payload directly on the event object rather than
* under `detail`, so reading `detail` yields a failure with no code and no
* description an error screen that says nothing. Both shapes are read here.
*
* A lost renderer is handled here too. It is reported by neither of the above:
* the page simply stops existing, and the panel would otherwise stay blank with
* no indication that anything happened.
*/
/** Chromium's code for "this navigation was replaced by another one". */
const ERR_ABORTED = -3;
/**
* `about:blank` is the view's resting state, not somewhere the user went. It
* arrives through `did-navigate` like any other address, and taking it at face
* value puts it in the address bar and makes the panel look like it is showing
* a page which hides the empty state that would otherwise offer somewhere to
* go.
*/
const isRealPageUrl = (url: unknown): url is string => (
typeof url === 'string' && url.length > 0 && url !== 'about:blank'
);
type FailLoadDetail = {
errorCode?: number;
errorDescription?: string;
validatedURL?: string;
isMainFrame?: boolean;
};
/** Reads a webview event payload, whichever shape this Electron version uses. */
export const readEventPayload = <T extends object>(event: Event): Partial<T> => {
const record = event as unknown as { detail?: unknown };
if (record.detail && typeof record.detail === 'object') return record.detail as Partial<T>;
return event as unknown as Partial<T>;
};
export type WebviewNavigation = {
readonly status: BrowserNavStatus;
readonly url: string;
readonly title: string;
readonly canGoBack: boolean;
readonly canGoForward: boolean;
};
export const useWebviewNavigation = (
webview: WebviewElement | null,
{ initialUrl, onUrlChange }: { initialUrl: string; onUrlChange: (url: string) => void },
): WebviewNavigation => {
const [status, setStatus] = React.useState<BrowserNavStatus>(
initialUrl ? { kind: 'loading', url: initialUrl } : IDLE_NAV_STATUS,
);
const [url, setUrl] = React.useState(initialUrl);
const [title, setTitle] = React.useState('');
const [canGoBack, setCanGoBack] = React.useState(false);
const [canGoForward, setCanGoForward] = React.useState(false);
const urlChangeRef = React.useRef(onUrlChange);
urlChangeRef.current = onUrlChange;
// Survives re-attaches so a view that keeps crashing cannot restart its own
// budget by being remounted.
const crashStateRef = React.useRef<CrashRecoveryState>(INITIAL_CRASH_RECOVERY_STATE);
React.useEffect(() => {
if (!webview) return;
const readCurrentUrl = (): string => {
try {
const value = webview.getURL();
return isRealPageUrl(value) ? value : '';
} catch {
return '';
}
};
const syncHistory = () => {
try {
setCanGoBack(webview.canGoBack());
setCanGoForward(webview.canGoForward());
} catch {
// Webview not attached yet; the next event resyncs.
}
};
const commitUrl = (next: string) => {
if (!isRealPageUrl(next)) return;
setUrl(next);
urlChangeRef.current(next);
};
const onStartLoading = () => {
const current = readCurrentUrl();
setStatus({ kind: 'loading', url: current });
};
const onStopLoading = () => {
const current = readCurrentUrl();
let pageTitle = '';
try {
pageTitle = webview.getTitle() || '';
} catch {
pageTitle = '';
}
setTitle(pageTitle);
commitUrl(current);
syncHistory();
// A failure already produced a terminal status; do not overwrite it with
// the `did-stop-loading` that always follows.
setStatus((previous) => (
previous.kind === 'failed' && previous.url === current
? previous
: { kind: 'ready', url: current, title: pageTitle }
));
};
const onNavigate = (event: Event) => {
const detail = readEventPayload<{ url?: string }>(event);
if (isRealPageUrl(detail.url)) {
commitUrl(detail.url);
syncHistory();
}
};
const onFaviconUpdated = (event: Event) => {
const detail = readEventPayload<{ favicons?: string[] }>(event);
const icon = Array.isArray(detail.favicons) ? detail.favicons.find(Boolean) : '';
const page = readCurrentUrl();
if (icon && page) useBrowserFaviconStore.getState().resolve(page, icon);
};
const onTitleUpdated = (event: Event) => {
const detail = readEventPayload<{ title?: string }>(event);
if (typeof detail.title === 'string') setTitle(detail.title);
};
const onFailLoad = (event: Event) => {
const detail = readEventPayload<FailLoadDetail>(event);
if (detail.isMainFrame === false) return;
const code = typeof detail.errorCode === 'number' ? detail.errorCode : 0;
if (code === ERR_ABORTED) return;
setStatus({
kind: 'failed',
url: isRealPageUrl(detail.validatedURL) ? detail.validatedURL : readCurrentUrl(),
code,
description: typeof detail.errorDescription === 'string' ? detail.errorDescription : '',
});
};
let recoveryTimer: ReturnType<typeof setTimeout> | null = null;
const onCrashed = () => {
const target = readCurrentUrl();
const plan = planCrashRecovery(crashStateRef.current, Date.now());
if (!plan) {
// Out of attempts: say what happened rather than reload again. The
// toolbar's own reload stays available, which is the user's call.
setStatus({ kind: 'failed', url: target, code: 0, description: '', crashed: true });
return;
}
crashStateRef.current = plan.state;
setStatus({ kind: 'loading', url: target });
recoveryTimer = setTimeout(() => {
recoveryTimer = null;
try {
webview.reload();
} catch {
setStatus({ kind: 'failed', url: target, code: 0, description: '', crashed: true });
}
}, plan.delayMs);
};
webview.addEventListener('did-start-loading', onStartLoading);
webview.addEventListener('did-stop-loading', onStopLoading);
webview.addEventListener('did-navigate', onNavigate);
webview.addEventListener('did-navigate-in-page', onNavigate);
webview.addEventListener('page-title-updated', onTitleUpdated);
webview.addEventListener('page-favicon-updated', onFaviconUpdated);
webview.addEventListener('did-fail-load', onFailLoad);
// Electron renamed this event; older builds still emit only the old name.
webview.addEventListener('render-process-gone', onCrashed);
webview.addEventListener('crashed', onCrashed);
// The webview may already be settled by the time this effect runs. Only
// treat it as settled when a page is actually loaded: a freshly created
// view reports "not loading" before its guest attaches, and settling on
// that would declare an empty page ready and hide the real one behind an
// empty state.
try {
if (!webview.isLoading() && readCurrentUrl()) onStopLoading();
} catch {
// Not attached yet.
}
return () => {
if (recoveryTimer !== null) clearTimeout(recoveryTimer);
webview.removeEventListener('render-process-gone', onCrashed);
webview.removeEventListener('crashed', onCrashed);
webview.removeEventListener('did-start-loading', onStartLoading);
webview.removeEventListener('did-stop-loading', onStopLoading);
webview.removeEventListener('did-navigate', onNavigate);
webview.removeEventListener('did-navigate-in-page', onNavigate);
webview.removeEventListener('page-title-updated', onTitleUpdated);
webview.removeEventListener('page-favicon-updated', onFaviconUpdated);
webview.removeEventListener('did-fail-load', onFailLoad);
};
}, [webview]);
return { status, url, title, canGoBack, canGoForward };
};

Some files were not shown because too many files have changed in this diff Show More