diff --git a/.agents/skills/communication-style/SKILL.md b/.agents/skills/communication-style/SKILL.md new file mode 100644 index 00000000..d1760fb8 --- /dev/null +++ b/.agents/skills/communication-style/SKILL.md @@ -0,0 +1,81 @@ +--- +name: communication-style +description: Use it always. +author: poteto (pstack) +--- + +# Communication style + +Edit text to remove AI patterns and add human voice. + +## Process + +1. Scan for the patterns below. +2. Rewrite. Preserve meaning, match intended tone. +3. Add soul (see next section). +4. Self-audit: "What makes this obviously AI generated?" Fix remaining tells. + +## Adding soul + +Removing patterns is half the job. Sterile, voiceless writing is just as obvious. + +- **Have opinions.** React to facts instead of neutrally listing pros and cons. +- **Vary rhythm.** Short sentences. Then longer ones that take their time. Mix it up. +- **Acknowledge complexity.** "Impressive but also kind of unsettling" beats "impressive." +- **Use "I" when it fits.** First person isn't unprofessional. +- **Let some mess in.** Perfect structure looks machine-made. +- **Be specific.** Not "this is concerning" but "there's something unsettling about agents churning away at 3am." + +## Patterns to detect and fix + +### Content + +1. **Puffery.** "pivotal moment", "testament to", "evolving landscape", "setting the stage for", "indelible mark", "deeply rooted". Cut puffery, state what happened. +2. **Name-dropping.** Listing media outlets without context. Pick one, say what was said. +3. **Superficial -ing phrases.** "highlighting...", "ensuring...", "reflecting...", "showcasing...", "fostering...". Delete or expand with real sources. +4. **Promotional language.** "nestled", "vibrant", "breathtaking", "groundbreaking", "renowned", "stunning", "must-visit". Use neutral descriptions. +5. **Vague attributions.** "Experts believe", "Industry reports suggest", "Some critics argue". Name the source or delete. +6. **Formulaic challenges.** "Despite challenges... continues to thrive." Replace with specific facts. + +### Language + +7. **AI vocabulary.** Additionally, crucial, delve, enduring, enhance, fostering, garner, interplay, intricate, landscape (abstract), pivotal, showcase, tapestry (abstract), testament, underscore, vibrant. Replace with plain words. +8. **Fancy ways to say "is".** "serves as", "stands as", "boasts", "features". Just say "is" or "has". +9. **"Not just X, but Y."** State the point directly instead. +10. **Rule of three.** Forcing ideas into groups of three. Use the natural number. +11. **Synonym cycling.** Protagonist, main character, central figure, hero all in one paragraph. Pick one, repeat it. +12. **False ranges.** "from X to Y" where X and Y aren't on a meaningful scale. List topics directly. + +### Style + +13. **Em dash overuse.** Avoid em dashes entirely. Use periods or commas only (no parentheses, no en dashes, no hyphen-as-dash substitutes). Em dashes are an AI tell, and reaching for parentheses instead just trades one tell for another. If a thought needs separation, end the sentence or use a comma. +14. **Colon overuse.** Colons are fine before a list or example. Not as mid-sentence connectors. "If you're coming from traditional automation: instead of registering event handlers, you describe conditions" adds nothing with the colon. Rewrite to let the point stand on its own without comparison framing. "Describing when the scheduler should fire works best as plain English." Same meaning, no crutch punctuation. +15. **Boldface overuse.** Don't bold every proper noun or acronym. +16. **Inline-header lists.** The tell is a bold label and colon that restates the line: "**Performance:** Performance improved...". Convert to prose. A bold lead-in that ends in a period, names the item, and is followed by genuinely new detail ("**Schema in TypeScript.** Tables live in one file.") is fine. +17. **Title case headings.** Use sentence case. +18. **Decorative emojis.** Remove from headings and bullets. +19. **Curly quotes.** Replace with straight quotes. + +### Communication artifacts + +20. **Chatbot phrases.** "I hope this helps!", "Let me know if...", "Of course!", "Certainly!", "Found the smoking gun!" Remove. +21. **Cutoff disclaimers.** "While specific details are limited..." Find sources or remove. +22. **Sycophantic tone.** "Great question! You're absolutely right!" Respond directly. + +### Filler + +23. **Filler phrases.** "In order to" becomes "To". "Due to the fact that" becomes "Because". "It is important to note that" gets deleted. +24. **Excessive hedging.** "could potentially possibly be argued that it might" becomes "may". +25. **Generic conclusions.** "The future looks bright." State specific plans or facts. + +### Jargon + +26. **Abstract metaphor nouns.** Substrate, wedge, vector, locus, vantage, nexus, primitive (as noun), harness (as metaphor), surface (as in "API surface"), bedrock, scaffolding (as metaphor), modality, paradigm, gold-plating, ratchet (as metaphor), evacuate (for moving code), endgame, north star, flywheel. These read as technical but usually have a plainer concrete word. "Substrate" becomes "base". "Wedge in" becomes "add". "Vector" becomes "way" or "method". "Gold-plating" becomes "more than the job needs". "Ratchet" becomes the mechanism's real name or "a limit that only tightens". "Evacuate" becomes "move out". "Endgame" becomes "the last phase". Pick the concrete word. + +### Plain speech + +27. **Say what it does, not how it feels.** "the database stays close at hand", "SQL you can read", "types that follow your schema" name a feeling. The fix names the mechanism or a number: "`.toSQL()` returns the exact string sent to the database", "a column rename fails the build". Ask what the sentence tells the reader to do or know, then write it. If you can't restate it as a concrete instruction, fact, or number, cut it. One more check: if the sentence could appear unchanged in another project's docs, it says nothing about this one. Cut it. +28. **Shorten or split dense sentences.** If the reader has to backtrack to parse a sentence, break it in two or drop clauses. One idea per sentence. +29. **Active voice.** Prefer it. Catch "is/are/was/were + past participle" and name the actor: "queries are validated" becomes "the compiler validates queries", "the file is parsed by the loader" becomes "the loader parses the file". Passive is fine only when the actor is unknown or genuinely doesn't matter. +30. **Cut adverbs, or use a stronger verb.** "runs quickly" becomes "is fast" or the number. "significantly improves" becomes the measured delta. An adverb propping up a weak verb means the verb is wrong. +31. **Prefer the plain word.** "utilize" becomes "use", "leverage" becomes "use", "facilitate" becomes "help", "numerous" becomes "many", "in the event that" becomes "if". The fancier synonym is rarely clearer. diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml index 04fbeb9c..e82bbba0 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/pr-review.yml @@ -1,12 +1,7 @@ name: pr-review on: - pull_request_target: - types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] + workflow_dispatch: concurrency: # PR conversation comments arrive as `issue_comment` events, so their PR number diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 26cef37e..414a56f0 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -24,13 +24,13 @@ jobs: - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: repo-token: ${{ steps.app-token.outputs.token }} - days-before-stale: 60 + days-before-stale: 28 days-before-close: 7 stale-issue-label: stale stale-pr-label: stale stale-issue-message: > This issue has been automatically marked as stale because it has not had - any activity in the last 60 days. It will be closed in 7 days if no + any activity in the last 28 days. It will be closed in 7 days if no further activity occurs. close-issue-message: > This issue has been automatically closed because it has been stale for @@ -38,7 +38,7 @@ jobs: reopen the issue. stale-pr-message: > This pull request has been automatically marked as stale because it has - not had any activity in the last 60 days. It will be closed in 7 days + not had any activity in the last 28 days. It will be closed in 7 days if no further activity occurs. close-pr-message: > This pull request has been automatically closed because it has been diff --git a/.gitignore b/.gitignore index 51ed5714..03d5f68f 100644 --- a/.gitignore +++ b/.gitignore @@ -68,5 +68,8 @@ data/ workspaces/ *.pid .worktrees/ + +# Marks a disposable clone dedicated to unattended maintenance tasks. +.maintenance-clone test-results/ artifacts/ diff --git a/.opencode/agent/reproduce-issue.md b/.opencode/agent/reproduce-issue.md index de21e373..640ee322 100644 --- a/.opencode/agent/reproduce-issue.md +++ b/.opencode/agent/reproduce-issue.md @@ -1,7 +1,7 @@ --- mode: primary hidden: true -model: opencode-go/deepseek-v4-flash +model: opencode-go/mimo-v2.5 color: "#c0392b" permission: edit: allow diff --git a/.opencode/agent/summarize.md b/.opencode/agent/summarize.md index 99db887e..c3cd2b21 100644 --- a/.opencode/agent/summarize.md +++ b/.opencode/agent/summarize.md @@ -1,7 +1,7 @@ --- mode: primary hidden: true -model: opencode-go/deepseek-v4-flash +model: opencode-go/mimo-v2.5 color: "#4f8f8f" permission: edit: deny diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md index 4df9199a..314f874d 100644 --- a/.opencode/agent/triage.md +++ b/.opencode/agent/triage.md @@ -1,7 +1,7 @@ --- mode: primary hidden: true -model: opencode-go/deepseek-v4-flash +model: opencode-go/mimo-v2.5 color: "#c4920a" permission: edit: deny diff --git a/.opencode/commands/as-fixes.md b/.opencode/commands/as-fixes.md index fb9152b6..8a4d87fc 100644 --- a/.opencode/commands/as-fixes.md +++ b/.opencode/commands/as-fixes.md @@ -13,7 +13,20 @@ 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. +If the output is not empty, decide which of two situations you are in. + +If the repository root contains a `.maintenance-clone` marker file, this working copy is a disposable clone dedicated to unattended maintenance. Nothing in it is human work in progress, so leftover changes are debris from an earlier task that failed to clean up after itself. Recover the clone rather than stopping: + +``` +git checkout -- . +git clean -fd +git checkout main +git pull +``` + +Report exactly which files you discarded, then continue with the task. A failed predecessor must not be able to jam the pipeline for every later run. + +If the marker file is absent, this is a working copy a person uses. Stop immediately and report that the worktree has uncommitted changes. Do not stash, reset, discard, commit, or switch branches. Then run: @@ -295,7 +308,18 @@ So, before you consider a selected file done: - 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`. +Skip a finding when the fix would require unclear behavior changes, when the change would be so large that the pull request stops being reviewable, or when the only way you can see to close it is one of the forbidden patterns. That last case is not a loophole, it is the required outcome: an honest skip is always better than a laundered fix, and choosing the forbidden pattern to satisfy "finish the file" is the worse failure of the two. Ordinary difficulty, on its own, is still not a reason. If skipped, give the specific reason in the PR body under `## Non-goals`. + +### When the whole file is an external-data boundary + +Some files exist to receive data from outside the program: provider APIs, quota endpoints, extension host messages, configuration on disk. In such a file, most or all findings can share one root cause, and the honest fix is a real parsed boundary with named contracts, which is a substantial piece of work rather than a lint cleanup. + +Recognize this early, before editing. Read the file first and ask whether closing its findings means designing a data contract that does not exist yet. If it does, choose one of two outcomes, and never a third: + +- Do the work properly for a coherent part of the file: define the contract for one provider, one endpoint, or one message, parse it at its boundary, and leave the rest with a clear explanation of the remaining root cause. A correct partial fix with a named boundary is a good pull request. +- Conclude that the file is a poor batch selection, abort per "Aborting cleanly", and say in your report that the file needs a deliberate data-contract change rather than an unattended cleanup. + +What you must not do is invent a generic JSON contract to make the findings disappear. Generic record types, primitive unions, and `unknown`-based aliases over external data are exactly the patterns these rules exist to reject, and reintroducing them under time pressure defeats the purpose of the whole task. 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. @@ -311,6 +335,21 @@ Hard prohibitions. Each of these makes the lint output greener while making the - 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. +## Aborting cleanly + +You may reach a point where the batch cannot be completed correctly: validation keeps failing, or the only remaining way to close the findings is a pattern this task forbids. Stopping there is the right decision. Stopping there and walking away from a modified working copy is not. + +Whatever edits exist in the working copy at that moment are your own, made minutes ago in this session. They are not human work in progress, and nothing is lost by removing them. Leaving them behind jams every scheduled run that follows, because those runs correctly refuse to operate on a dirty worktree. + +So when you abort, in this order: + +1. Revert every file you modified: `git checkout -- `, plus `git clean -fd` for files you created. Verify with `git status --porcelain` that the result is empty. +2. Release the claim so the files return to the pool: ``bun run deslop -- release --run ``. +3. Return to `main`. +4. Report what you attempted, precisely why you stopped, and confirm that both the worktree is clean and the claim is released. + +Never leave a partially fixed working copy as a message to the next run. If a file resists a correct fix, that belongs in your report, not on disk. + After edits, run: `bun run deslop -- check-batch --run ` diff --git a/.opencode/commands/as-follow-up.md b/.opencode/commands/as-follow-up.md index 6e1c6faf..c7877b7e 100644 --- a/.opencode/commands/as-follow-up.md +++ b/.opencode/commands/as-follow-up.md @@ -13,7 +13,20 @@ 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. +If the output is not empty, decide which of two situations you are in. + +If the repository root contains a `.maintenance-clone` marker file, this working copy is a disposable clone dedicated to unattended maintenance. Nothing in it is human work in progress, so leftover changes are debris from an earlier task that failed to clean up after itself. Recover the clone rather than stopping: + +``` +git checkout -- . +git clean -fd +git checkout main +git pull +``` + +Report exactly which files you discarded, then continue with the task. A failed predecessor must not be able to jam the pipeline for every later run. + +If the marker file is absent, this is a working copy a person uses. Stop immediately and report that the worktree has uncommitted changes. Do not stash, reset, discard, commit, or switch branches. List the active batches: diff --git a/.opencode/commands/maintenance-review.md b/.opencode/commands/maintenance-review.md index 74eddeea..d0ec290f 100644 --- a/.opencode/commands/maintenance-review.md +++ b/.opencode/commands/maintenance-review.md @@ -29,7 +29,9 @@ 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. +If the output is not empty and the repository root contains a `.maintenance-clone` marker file, this is a disposable maintenance clone and the changes are debris from an earlier failed task. Recover it with `git checkout -- .`, `git clean -fd`, `git checkout main`, `git pull`, report exactly which files you discarded, and continue. + +If the marker file is absent, 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. diff --git a/.opencode/commands/rd-fixes.md b/.opencode/commands/rd-fixes.md index b610a4d0..df2d6bb8 100644 --- a/.opencode/commands/rd-fixes.md +++ b/.opencode/commands/rd-fixes.md @@ -13,7 +13,20 @@ 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. +If the output is not empty, decide which of two situations you are in. + +If the repository root contains a `.maintenance-clone` marker file, this working copy is a disposable clone dedicated to unattended maintenance. Nothing in it is human work in progress, so leftover changes are debris from an earlier task that failed to clean up after itself. Recover the clone rather than stopping: + +``` +git checkout -- . +git clean -fd +git checkout main +git pull +``` + +Report exactly which files you discarded, then continue with the task. A failed predecessor must not be able to jam the pipeline for every later run. + +If the marker file is absent, this is a working copy a person uses. Stop immediately and report that the worktree has uncommitted changes. Do not stash, reset, discard, commit, or switch branches. Then run: @@ -36,10 +49,26 @@ Workflow: - 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 ` 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`. +- Skip a diagnostic when the fix would require unclear behavior changes, when the change would be so large that the pull request stops being reviewable, or when the only way you can see to close it is a change you would not defend in review. An honest skip is always better than a forced fix. Ordinary difficulty, on its own, is still not a reason. If skipped, give the specific reason in the PR body under `## Non-goals`. +- If a whole selected file turns out to need a deliberate architectural change rather than a cleanup, abort per "Aborting cleanly" and report that the file was a poor batch selection. - 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. +## Aborting cleanly + +You may reach a point where the batch cannot be completed correctly: validation keeps failing, or the only remaining way to close the findings is a pattern this task forbids. Stopping there is the right decision. Stopping there and walking away from a modified working copy is not. + +Whatever edits exist in the working copy at that moment are your own, made minutes ago in this session. They are not human work in progress, and nothing is lost by removing them. Leaving them behind jams every scheduled run that follows, because those runs correctly refuse to operate on a dirty worktree. + +So when you abort, in this order: + +1. Revert every file you modified: `git checkout -- `, plus `git clean -fd` for files you created. Verify with `git status --porcelain` that the result is empty. +2. Release the claim so the files return to the pool: ``bun run doctor -- release --run ``. +3. Return to `main`. +4. Report what you attempted, precisely why you stopped, and confirm that both the worktree is clean and the claim is released. + +Never leave a partially fixed working copy as a message to the next run. If a file resists a correct fix, that belongs in your report, not on disk. + After edits, run: `bun run doctor -- check-batch --run ` diff --git a/.opencode/commands/rd-follow-up.md b/.opencode/commands/rd-follow-up.md index 478a03a7..7bac4435 100644 --- a/.opencode/commands/rd-follow-up.md +++ b/.opencode/commands/rd-follow-up.md @@ -13,7 +13,20 @@ 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. +If the output is not empty, decide which of two situations you are in. + +If the repository root contains a `.maintenance-clone` marker file, this working copy is a disposable clone dedicated to unattended maintenance. Nothing in it is human work in progress, so leftover changes are debris from an earlier task that failed to clean up after itself. Recover the clone rather than stopping: + +``` +git checkout -- . +git clean -fd +git checkout main +git pull +``` + +Report exactly which files you discarded, then continue with the task. A failed predecessor must not be able to jam the pipeline for every later run. + +If the marker file is absent, this is a working copy a person uses. Stop immediately and report that the worktree has uncommitted changes. Do not stash, reset, discard, commit, or switch branches. List the active batches: diff --git a/AGENTS.md b/AGENTS.md index 0ea1ebb6..9d2a437d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,6 +79,10 @@ task-required reference named by those skills. Skills are canonical for their detailed workflows and checklists. Treating this table as optional advice is a process violation. +**Always load `.agents/skills/communication-style/SKILL.md` at the start of +every task, before any analysis, tool call, or response. Apply its guidance to +all messages and written output, not only to user-facing copy or documentation.** + | Trigger | Required skill | |---|---| | Source/dependency changes, exports or package contracts, build/generated assets, or module ownership | `openchamber-change-discipline` | diff --git a/CHANGELOG.md b/CHANGELOG.md index 46731e19..08499ec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,99 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +- **Chat scrolling rebuilt around your message.** Sending a message parks it near the top of the view and the reply streams into the space below it, so you read from where you asked instead of chasing the bottom. Streamed text arrives a paragraph at a time (code blocks line by line) with a soft fade, and the view glides after it in one continuous motion instead of snapping per line. Scrolling up during a stream immediately hands you the wheel — nothing yanks the view back — and the scroll-to-bottom pill appears on the left, carrying the model's working status while you're away from the live edge. Sending from anywhere mid-conversation jumps you straight to your new message, and opening a session goes straight to the newest message with no scroll animation. +- **Chat context attachments:** everything you attach to a message — diff/file/plan comments, terminal selections, browser annotations, PR comments and failed checks, linked issues and PRs — now shows up in the conversation as a compact context card: a header naming the source, the captured content behind an expander, and your comment below it. Previously most of these arrived as a wall of raw text inside your message. +- **Faster session switching in large workspaces** (thanks @c-w-xiaohei): switching sessions no longer rebuilds the whole sidebar, returning to a recently viewed session restores its rendered messages instead of re-rendering them (file links included), and scrolling long conversations costs less. In a workspace with thousands of loaded sessions, end-to-end switch time dropped by roughly half. +- **Session tabs (opt-in):** the web/desktop header can show your open sessions as browser-style tabs — turn them on in Settings → General → Navigation → Session tabs. Every session you open joins the strip, clicking a tab switches the whole workspace (chat, project, panels), and closing one (its × button, middle-click, or Alt+W — rebindable in Shortcuts) never touches the session itself. Tabs reorder by drag, scroll behind the header buttons when there are many and carry the sidebar's running/unread dot. Each tab has the full session menu — on the "..." button or right-click — plus Close other tabs; renaming works right in the tab. +- Chat: comment on a reply — select text in a chat message (or in a rendered markdown preview in Files) and choose Comment to attach exactly that quote, with a source line range when it can be located, plus your note to the next message. The selection stays highlighted while you type, and the selection menu was restyled — Add to chat is now Add to input. +- Diff: comment like a review — hovering a line shows a + button in the gutter; clicking it, clicking a line, or dragging across lines opens the comment editor for that line or range. The comment editor and saved-comment cards match the chat's comment style. +- Composer: hovering or tapping a context chip above the input opens a stacked preview of everything attached, where a comment can be edited in place or an item removed before sending. +- Mobile: the chat comment input overlays the composer exactly and rides the keyboard; Enter makes a new line there, with attach on the button. +- Terminal: terminals no longer vanish or die behind your back. Opening the app in another browser tab, on another device, or after a reload shows the terminals already running on the server instead of an empty list, and terminals sitting in background tabs are no longer closed by the server's idle cleanup while the app is open. +- Search: every searchable picker — branches, projects, agents, models, providers, stashes, SSH hosts, skills, archived sessions — now uses one matcher: best matches come first, multi-word queries match in any order, and punctuation doesn't matter (so "gpt4o" finds "gpt-4o"). Ctrl/Cmd+P also matches the whole file path, not just the file name, and the git branch and gitmoji pickers stopped silently dropping rows a second built-in filter didn't like. Sidebar session search and the Todos/Memory/Plans/Notes filters match the same way. +- Chat: @ file mentions rank files and directories together by how well they match, so the file you typed is at the top instead of below unrelated directories. Multi-word queries match in any order, and long paths keep the folder next to the file name visible so identical-looking index.md rows are distinguishable. +- Chat: a new "Follow new content while streaming" checkbox (Settings → Chat → Streaming, on by default) turns the automatic following off entirely — your message still parks at the top on send, but the view never moves on its own afterwards. +- Mobile: narrowing a browser window past phone size switches into the mobile app layout (and back when widened) instead of squeezing the desktop layout. The old/new mobile layout setting is gone — phones always get the mobile layout. +- Chat: streamed code blocks are syntax-highlighted while they stream, and finished messages no longer jump when a code block's line numbers fill in at the end of a reply. +- Chat: finished replies no longer flicker — tool cards stopped re-rendering (and replaying their reveal animation) when they completed, and resizing the window no longer throws the conversation up and down while you're at the bottom. +- Chat: clicking the last item in the prompt rail now always lands on it, and rail jumps teleport instead of a long smooth scroll that could stop halfway. +- Mobile: scrolling during a streaming reply works again — a drag immediately takes over, the scroll-to-bottom pill shows up, and the load-older button no longer throws you to the bottom of the chat. +- Fixed file links in messages being checked twice against the filesystem, and against the wrong project directory on the first pass. +- Fixed the selected project or session briefly jumping back to a previous choice when settings responses arrived out of order. +- Terminal: mobile keyboards no longer capitalize the first letter of every command on iOS and Android. +- Desktop: a freshly installed or updated build no longer keeps loading the previous version's interface from cache. +- Devices: re-pairing a phone (or logging in again) keeps the device's existing name in Connected Devices instead of resetting it to "OpenChamber Mobile". +- Relay: paired devices no longer get logged out when the app restarts (for example during an update) while another local OpenChamber process is running — the restarted app keeps serving them instead of a bystander process taking over. +- Sessions: headers now find archived sessions too, so an archived session's title no longer goes missing. +- Files: the editor toolbar is now always docked under the file tabs; the floating hover toolbar and its setting were removed. +- UI: the chat's top and bottom scroll fades are back, and the first uncached open of a session fades the conversation in instead of popping. +- UI: the timeline dialog now fits small screens instead of squeezing the message list to a couple of rows (thanks to @gaojunran). +- Chat: OpenCode notices now share one style. +- UI: draft target menus stay inside the chat area instead of overlapping the header. +- UI: Linear and Cloudflare tools now show their own icons. +- UI: sidebar item tooltips no longer appear instantly on passing hover. +- UI: the btw panel's shadow is lighter, matching the composer. + +## [1.20.0] - 2026-08-23 + +- **Session: /btw side questions.** Type `/btw` followed by your question to ask something off-topic in a temporary session forked from the current conversation, so it inherits the full context but leaves the chat itself untouched. The answer streams into a panel above the composer, which talks to that session while the panel is open; you can collapse it to a slim header bar, keep it as a full session, or discard it. The temporary session stays out of the sidebar and session lists until you keep it (thanks to @jaygupta17). +- **Chat sessions:** start chats without choosing a project. They live in their own Chats section, rather than inheriting a project's repository and worktree context. +- **Desktop/Remote instances:** adding an SSH connection now starts from the hosts in your SSH config instead of a blank command field. Ports, install method and passwords moved behind Advanced settings, and each connection shows Connected, Connecting, or Needs attention with the failure text and a button that resolves it. +- Desktop/Remote instances: connecting to a remote machine now works when bun, OpenChamber or the opencode CLI live in your home directory rather than on the system path. Installing no longer fails with a permission error, and a missing opencode CLI is now reported before the connection starts instead of as a stack trace. +- Desktop/Remote instances: a managed remote server can now also be published to the remote machine's own network, so other devices there reach it without the SSH tunnel. It requires a UI password, and stays private to the tunnel otherwise. +- Desktop/Remote instances: disconnecting from a connection set to not keep the server running now actually stops that remote server. +- Skills catalog: browse curated GitHub skill collections in a card-based catalog with cross-source search, skill counts, stars, recent updates, and links back to each skill's repository. +- Diff: the context-panel diff can now show every change on the current branch against its base branch. OpenChamber detects the base when Git knows it, or lets you choose one once when it does not. +- Dictation: speech is now transcribed after you stop recording. The composer shows a live waveform and timer, and long recordings split at pauses instead of cutting words. +- Settings: the project selector on Providers, Agents, MCP, Commands and Skills now only changes what those pages show. It used to switch the whole app, so opening another project's configuration moved your chat, session list and file tree with it. +- Settings/Projects: a project can now pin a thinking level next to its model, for models that offer levels. Both sit in one Defaults for new chats group, laid out like the Sessions defaults. +- Settings/General: changing the default model, variant or agent no longer repoints an open chat that already carries a model you picked for it. Chats following the default still switch immediately. +- Settings/Providers: the provider you select no longer jumps to a different one on its own. Changing the chat's model or agent, and background provider refreshes, used to move the settings selection with them. +- Settings/Integrations: the experimental page now only lists integrations that can be installed; unavailable and Coming soon entries were removed. +- Chat: file paths in messages now open from the session's project, even if you last browsed files in another project (thanks to @tomzx). +- Chat: app links such as `spotify://` now ask for confirmation before opening another app. You can trust an app link type on one device and manage trusted links in Settings. +- Files/Desktop: files opened from outside the workspace remain readable after their temporary access expires instead of failing until you reopen them (thanks to @pascalandr). +- Diff: creating an inline comment now opens the chat and focuses the composer for your follow-up. +- Chat: in the expanded composer, Enter now starts a new line and Cmd/Ctrl+Enter sends, so a long prompt is harder to send by accident. +- Providers: expanded support for custom providers. +- Small Model: summaries, goal audits, commit messages, and walkthroughs now support more providers. +- Git: generated commit messages now match the repository's recent commit style and language. +- Git: generating a pull request description now picks up the repository's own PR template when it has one, so the draft comes back in your project's sections and checklists instead of the built-in Summary/Why/Testing layout. +- Sidebar: switch between the full project list and a focused view of one project. Sessions created outside OpenChamber now also appear in the sidebar and Recent list without a page refresh (thanks to @tomzx). +- Chat: if OpenCode restarts while a response is still running, the chat now stops with an interrupted state and a notification to continue instead of hanging silently (thanks to @sum117). +- Chat: while a reply streams, the model status line under the last message now turns into the finished message's info row in place, instead of jumping when the reply completes. +- Chat: newly sent messages and syntax-highlighted code blocks no longer briefly flicker. Bash output can also grow with its content instead of being cut off. +- Chat: long user messages can be expanded even when their final layout finishes after they first appear. +- Chat: in a chat without a project, the work status card again steps aside when the context panel is open, instead of sitting next to it. +- Usage: Z.ai credit limits now appear alongside its other quota windows. +- Git: pull-request checks in Work status stay current as their status changes. +- UI: the default dialog close button is easier to click or tap (thanks to @rockinrimmer). +- Desktop/Windows: the close button now aligns correctly with the rest of the window chrome. + +## [1.19.0] - 2026-08-19 + - **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. +- **Project knowledge:** the Project notes panel is now Project knowledge, with notes, todos, plans and their search in a resizable sidebar. Notes are cards you expand by clicking anywhere on them, plans open and edit in the panel itself instead of a separate tab, and notes and plans can be pinned as context. +- **Files:** drag files onto the Files sidebar to upload them into the project or a specific folder; existing files require confirmation before replacement, and open previews refresh after an upload (thanks to @makeittech, @alanzchen). +- Settings: OpenChamber no longer replaces a full OpenCode config with an empty `$schema`-only stub when the file uses JSON5-style unquoted keys; Settings changes now fail instead of wiping plugins, MCP servers, and providers (thanks to @makeittech). +- Chat: an open conversation no longer keeps re-coloring the same code blocks in the background, so browsing files with a chat open stops pinning a CPU core and spinning up the fans (thanks to @makeittech). +- Stability/Proxy: the local server now reuses its connection to OpenCode instead of opening a new one for every API request. Under sustained traffic the old behavior could use up every outgoing network port on the machine, at which point nothing on the computer could open a new connection until the traffic stopped and the ports were released (thanks to @alohaninja). - 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. +- Usage/Command Code: Command Code plan limits now appear in the Usage page and work status panel. - 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). +- Git/Worktrees: creating a worktree from a pull request now falls back to GitHub's pull-request reference when the source fork was deleted or cannot be reached, instead of failing before creating the worktree (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: typing with Chinese, Japanese, or Korean input methods no longer interrupts composition or jumps the cursor to the end of the composer (thanks to @makeittech). - 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. +- Chat: the context meter no longer climbs over 100% (330% readouts) after turns with many tool calls and no longer jumps when reopening an older session; it now shows what the window actually holds, everywhere the value appears — header, context sidebar, work status panel, mini chat, and mobile (thanks to @pocharlies). +- Chat/Attachments: extracted Office and OpenDocument content is now capped and presented more compactly, preventing large documents and their images from overwhelming the message context. - 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. +- Files: files reached through a symlink inside the workspace now open correctly instead of being rejected as outside the workspace. - Settings: the session retention action you pick is now saved instead of being dropped (thanks to @Gautam0507). +- Mobile: connecting through an ngrok address now bypasses ngrok's browser warning page instead of failing the server check. +- Mobile/iOS: text selection in the chat composer now uses native CodeMirror selection handles. +- Desktop: browser pages served from a self-signed loopback HTTPS address now load instead of being blocked by the certificate warning. - 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). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e6ba882..7db3d8bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -204,7 +204,7 @@ state why it remains valid. If there is genuinely no user-visible change, say so and provide a concrete reason; deleting the evidence section is not an exemption. -### Review Enforcement +### Review enforcement The automated reviewer performs one unified review of correctness, repository guidance compliance, pull request quality, and evidence. It independently @@ -238,6 +238,16 @@ verify a trustworthy result, in which case it applies `review:automation-failed` Each completed review creates a new comment tied to its reviewed HEAD so the conversation remains chronological. Previous review comments are not rewritten. +### Keeping PRs active + +Stale PRs add review load and make it hard to tell what's still being worked on, so the stale bot keeps the open list current. A PR with no activity for 28 days is automatically labeled `stale`, and closed 7 days later if it stays inactive. To keep a PR open: + +- Push updates or respond to review feedback +- Leave a comment if you're waiting on a reviewer +- Add the `pinned`, `security`, or `help wanted` label to exempt a long-running PR from the stale bot + +Reopening a closed PR is fine if it becomes relevant again. + ## Project Structure ``` diff --git a/bun.lock b/bun.lock index 7dff8dfe..3155488f 100644 --- a/bun.lock +++ b/bun.lock @@ -6,31 +6,31 @@ "name": "openchamber-monorepo", "dependencies": { "@base-ui/react": "^1.4.0", - "@codemirror/autocomplete": "^6.20.0", - "@codemirror/commands": "^6.10.1", + "@codemirror/autocomplete": "^6.20.3", + "@codemirror/commands": "^6.11.0", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", - "@codemirror/lang-html": "^6.4.11", - "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-html": "^6.4.12", + "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-json": "^6.0.2", - "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-markdown": "^6.5.2", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-rust": "^6.0.2", "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-xml": "^6.1.0", - "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "6.12.2", - "@codemirror/lint": "^6.9.2", - "@codemirror/search": "^6.6.0", - "@codemirror/state": "^6.5.4", - "@codemirror/view": "6.39.13", + "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/language": "6.12.4", + "@codemirror/lint": "^6.9.7", + "@codemirror/search": "^6.7.1", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "6.43.9", "@heroui/scroll-shadow": "^2.3.18", "@heroui/system": "^2.4.23", "@heroui/theme": "^2.4.23", "@lezer/highlight": "^1.2.3", "@octokit/rest": "^22.0.1", - "@opencode-ai/sdk": "1.18.18", + "@opencode-ai/sdk": "1.18.21", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -97,7 +97,7 @@ }, "packages/electron": { "name": "@openchamber/electron", - "version": "1.18.4", + "version": "1.20.0", "dependencies": { "@openchamber/web": "workspace:*", "electron-context-menu": "^4.1.2", @@ -134,7 +134,7 @@ }, "packages/ui": { "name": "@openchamber/ui", - "version": "1.18.4", + "version": "1.20.0", "dependencies": { "@aparajita/capacitor-secure-storage": "^8.0.0", "@base-ui/react": "^1.4.0", @@ -143,34 +143,35 @@ "@capacitor/keyboard": "^8.0.0", "@capacitor/push-notifications": "^8.1.1", "@capacitor/status-bar": "^8.0.0", - "@codemirror/autocomplete": "^6.20.0", - "@codemirror/commands": "^6.10.1", + "@codemirror/autocomplete": "^6.20.3", + "@codemirror/commands": "^6.11.0", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", - "@codemirror/lang-html": "^6.4.11", - "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-html": "^6.4.12", + "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-json": "^6.0.2", - "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-markdown": "^6.5.2", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-rust": "^6.0.2", "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-xml": "^6.1.0", - "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "6.12.2", + "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/language": "6.12.4", "@codemirror/language-data": "^6.5.2", - "@codemirror/legacy-modes": "^6.5.2", - "@codemirror/lint": "^6.9.2", - "@codemirror/search": "^6.6.0", - "@codemirror/state": "^6.5.4", - "@codemirror/view": "6.39.13", + "@codemirror/legacy-modes": "^6.5.3", + "@codemirror/lint": "^6.9.7", + "@codemirror/search": "^6.7.1", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "6.43.9", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@legendapp/list": "3.3.8", "@lezer/highlight": "^1.2.3", - "@opencode-ai/sdk": "1.18.18", + "@opencode-ai/sdk": "1.18.21", "@pierre/diffs": "1.3.0-beta.6", - "@replit/codemirror-vim": "^6.3.0", + "@replit/codemirror-vim": "^6.4.0", "@simplewebauthn/browser": "13.3.0", "@tanstack/react-virtual": "3.14.5", "@xenova/transformers": "^2.17.2", @@ -227,6 +228,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.5.0", "globals": "^16.3.0", + "happy-dom": "^18.0.1", "nodemon": "^3.1.7", "tailwindcss": "^4.0.0", "tsx": "^4.20.6", @@ -238,10 +240,10 @@ }, "packages/vscode": { "name": "openchamber", - "version": "1.18.4", + "version": "1.20.0", "dependencies": { "@openchamber/ui": "workspace:*", - "@opencode-ai/sdk": "1.18.18", + "@opencode-ai/sdk": "1.18.21", "adm-zip": "^0.6.0", "jsonc-parser": "^3.3.1", "react": "^19.1.1", @@ -261,16 +263,15 @@ }, "packages/web": { "name": "@openchamber/web", - "version": "1.18.4", + "version": "1.20.0", "bin": { "openchamber": "./bin/cli.js", }, "dependencies": { "@clack/prompts": "^1.1.0", "@octokit/rest": "^22.0.1", - "@opencode-ai/sdk": "1.18.18", + "@opencode-ai/sdk": "1.18.21", "@simplewebauthn/server": "13.3.1", - "adm-zip": "^0.6.0", "bun-pty": "^0.4.5", "compression": "^1.8.1", "cron-parser": "^4.9.0", @@ -306,7 +307,6 @@ "@remixicon/react": "^4.7.0", "@simplewebauthn/browser": "13.3.0", "@tailwindcss/postcss": "^4.0.0", - "@types/adm-zip": "^0.5.7", "@types/node": "^24.3.1", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", @@ -355,8 +355,18 @@ "bun-pty@0.4.8": "bun-patches/bun-pty@0.4.8.patch", }, "overrides": { - "@codemirror/language": "6.12.2", - "@codemirror/view": "6.39.13", + "@codemirror/autocomplete": "6.20.3", + "@codemirror/commands": "6.11.0", + "@codemirror/lang-html": "6.4.12", + "@codemirror/lang-javascript": "6.2.5", + "@codemirror/lang-markdown": "6.5.2", + "@codemirror/lang-yaml": "6.1.3", + "@codemirror/language": "6.12.4", + "@codemirror/legacy-modes": "6.5.3", + "@codemirror/lint": "6.9.7", + "@codemirror/search": "6.7.1", + "@codemirror/state": "6.7.1", + "@codemirror/view": "6.43.9", }, "packages": { "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], @@ -603,9 +613,9 @@ "@clack/prompts": ["@clack/prompts@1.1.0", "", { "dependencies": { "@clack/core": "1.1.0", "sisteransi": "^1.0.5" } }, "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g=="], - "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg=="], + "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g=="], - "@codemirror/commands": ["@codemirror/commands@6.10.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ=="], + "@codemirror/commands": ["@codemirror/commands@6.11.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.7.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA=="], "@codemirror/lang-angular": ["@codemirror/lang-angular@0.1.4", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-javascript": "^6.1.2", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.3" } }, "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g=="], @@ -615,11 +625,11 @@ "@codemirror/lang-go": ["@codemirror/lang-go@6.0.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/go": "^1.0.0" } }, "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg=="], - "@codemirror/lang-html": ["@codemirror/lang-html@6.4.11", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw=="], + "@codemirror/lang-html": ["@codemirror/lang-html@6.4.12", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w=="], "@codemirror/lang-java": ["@codemirror/lang-java@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/java": "^1.0.0" } }, "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ=="], - "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="], + "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.5", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A=="], "@codemirror/lang-jinja": ["@codemirror/lang-jinja@6.0.0", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.4.0" } }, "sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw=="], @@ -629,7 +639,7 @@ "@codemirror/lang-liquid": ["@codemirror/lang-liquid@6.3.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.1" } }, "sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw=="], - "@codemirror/lang-markdown": ["@codemirror/lang-markdown@6.5.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.7.1", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.3.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/markdown": "^1.0.0" } }, "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw=="], + "@codemirror/lang-markdown": ["@codemirror/lang-markdown@6.5.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.7.1", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.3.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/markdown": "^1.0.0" } }, "sha512-AwBOdkWYuA//WcM0xO5PfHPUcmz/O2i5o0Nsg1U69SII/loCJlFI1Romd9xp2HYb1kYJRGZotyqRghuHH5n8Kw=="], "@codemirror/lang-php": ["@codemirror/lang-php@6.0.2", "", { "dependencies": { "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/php": "^1.0.0" } }, "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA=="], @@ -647,21 +657,21 @@ "@codemirror/lang-xml": ["@codemirror/lang-xml@6.1.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/xml": "^1.0.0" } }, "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg=="], - "@codemirror/lang-yaml": ["@codemirror/lang-yaml@6.1.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.0.0", "@lezer/yaml": "^1.0.0" } }, "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw=="], + "@codemirror/lang-yaml": ["@codemirror/lang-yaml@6.1.3", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.0.0", "@lezer/yaml": "^1.0.0" } }, "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ=="], - "@codemirror/language": ["@codemirror/language@6.12.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg=="], + "@codemirror/language": ["@codemirror/language@6.12.4", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A=="], "@codemirror/language-data": ["@codemirror/language-data@6.5.2", "", { "dependencies": { "@codemirror/lang-angular": "^0.1.0", "@codemirror/lang-cpp": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-go": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/lang-java": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/lang-jinja": "^6.0.0", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-less": "^6.0.0", "@codemirror/lang-liquid": "^6.0.0", "@codemirror/lang-markdown": "^6.0.0", "@codemirror/lang-php": "^6.0.0", "@codemirror/lang-python": "^6.0.0", "@codemirror/lang-rust": "^6.0.0", "@codemirror/lang-sass": "^6.0.0", "@codemirror/lang-sql": "^6.0.0", "@codemirror/lang-vue": "^0.1.1", "@codemirror/lang-wast": "^6.0.0", "@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-yaml": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/legacy-modes": "^6.4.0" } }, "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg=="], - "@codemirror/legacy-modes": ["@codemirror/legacy-modes@6.5.2", "", { "dependencies": { "@codemirror/language": "^6.0.0" } }, "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q=="], + "@codemirror/legacy-modes": ["@codemirror/legacy-modes@6.5.3", "", { "dependencies": { "@codemirror/language": "^6.0.0" } }, "sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg=="], - "@codemirror/lint": ["@codemirror/lint@6.9.4", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw=="], + "@codemirror/lint": ["@codemirror/lint@6.9.7", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.42.0", "crelt": "^1.0.5" } }, "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg=="], - "@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="], + "@codemirror/search": ["@codemirror/search@6.7.1", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA=="], - "@codemirror/state": ["@codemirror/state@6.5.4", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw=="], + "@codemirror/state": ["@codemirror/state@6.7.1", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A=="], - "@codemirror/view": ["@codemirror/view@6.39.13", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-QBO8ZsgJLCbI28KdY0/oDy5NQLqOQVZCozBknxc2/7L98V+TVYFHnfaCsnGh1U+alpd2LOkStVwYY7nW2R1xbw=="], + "@codemirror/view": ["@codemirror/view@6.43.9", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-sTuUzTpPMFebRhg6dawChoKKgndIwfjmJgKVxBefPElcU2NwQ6AFroupk0SFqEerQyZOGRfDNnSN8Dw/lMAsXw=="], "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], @@ -909,6 +919,8 @@ "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], + "@legendapp/list": ["@legendapp/list@3.3.8", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "*", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom", "react-native"] }, "sha512-GM4Hca/6WDvcY33XXCieR9MaG9CoZmACzwqQwRhKFSaNKfQV1lTLiTOpWuA9wnE8n8+6WeA52DwNKC9yrnPPeg=="], + "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], "@lezer/common": ["@lezer/common@1.5.1", "", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="], @@ -995,7 +1007,7 @@ "@openchamber/web": ["@openchamber/web@workspace:packages/web"], - "@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.18", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-zJlwXskIR47V1dkPJqeKBgq7nejG1uU8lJaGIGqbX3MWRCT8vKn0fEotbxuPCKnTdmWsDyNGNg9q1qIliDSMDA=="], + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.21", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-k6iHQ5C8wOPglk+LgFyYnst168cGMQYumgpbVoeXJ+iC1AtvwD5zmjuF8CxMze/y9G1K2bOeO6p9yRvA7eHZLA=="], "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.78.0", "", { "os": "android", "cpu": "arm" }, "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw=="], @@ -1191,7 +1203,9 @@ "@remixicon/react": ["@remixicon/react@4.9.0", "", { "peerDependencies": { "react": ">=18.2.0" } }, "sha512-5/jLDD4DtKxH2B4QVXTobvV1C2uL8ab9D5yAYNtFt+w80O0Ys1xFOrspqROL3fjrZi+7ElFUWE37hBfaAl6U+Q=="], - "@replit/codemirror-vim": ["@replit/codemirror-vim@6.3.0", "", { "peerDependencies": { "@codemirror/commands": "6.x.x", "@codemirror/language": "6.x.x", "@codemirror/search": "6.x.x", "@codemirror/state": "6.x.x", "@codemirror/view": "6.x.x" } }, "sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ=="], + "@replit/codemirror-vim": ["@replit/codemirror-vim@6.4.0", "", { "dependencies": { "@replit/codemirror-vim-core": "^0.1.0" }, "peerDependencies": { "@codemirror/commands": "6.x.x", "@codemirror/language": "6.x.x", "@codemirror/search": "6.x.x", "@codemirror/state": "6.x.x", "@codemirror/view": "6.x.x" } }, "sha512-t9UMDNhkmeAkl0uRbiJVotv97bGD6mf4GyJJoEbrjUqa/Pov0s9eL+4AaI0Xto3OGri17i9qwIpT0JbVlVXt8A=="], + + "@replit/codemirror-vim-core": ["@replit/codemirror-vim-core@0.1.0", "", {}, "sha512-1i6EBKpcNfDKvTmTh6N6g9lL6udD5t+uFNh4JCqozRnVlvUGOps7h/QzS2ne4zcvPUjvApKpmcP7Grc3fNbZiQ=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], @@ -1441,6 +1455,8 @@ "@types/vscode": ["@types/vscode@1.109.0", "", {}, "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw=="], + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + "@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=="], @@ -2129,6 +2145,8 @@ "guid-typescript": ["guid-typescript@1.0.9", "", {}, "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="], + "happy-dom": ["happy-dom@18.0.1", "", { "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", "whatwg-mimetype": "^3.0.0" } }, "sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA=="], + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -3341,7 +3359,7 @@ "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], - "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], @@ -3579,6 +3597,8 @@ "cheerio/undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], + "cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "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=="], @@ -3619,6 +3639,8 @@ "globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], + "happy-dom/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="], + "iconv-corefoundation/cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], @@ -3805,6 +3827,8 @@ "glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "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=="], "micromark-extension-math/katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], diff --git a/package.json b/package.json index f9c145f5..6e4cd2b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openchamber-monorepo", - "version": "1.18.4", + "version": "1.20.0", "description": "OpenChamber monorepo workspace for web, ui, and desktop runtimes", "private": true, "type": "module", @@ -91,31 +91,31 @@ }, "dependencies": { "@base-ui/react": "^1.4.0", - "@codemirror/autocomplete": "^6.20.0", - "@codemirror/commands": "^6.10.1", + "@codemirror/autocomplete": "^6.20.3", + "@codemirror/commands": "^6.11.0", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", - "@codemirror/lang-html": "^6.4.11", - "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-html": "^6.4.12", + "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-json": "^6.0.2", - "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-markdown": "^6.5.2", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-rust": "^6.0.2", "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-xml": "^6.1.0", - "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "6.12.2", - "@codemirror/lint": "^6.9.2", - "@codemirror/search": "^6.6.0", - "@codemirror/state": "^6.5.4", - "@codemirror/view": "6.39.13", + "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/language": "6.12.4", + "@codemirror/lint": "^6.9.7", + "@codemirror/search": "^6.7.1", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "6.43.9", "@heroui/scroll-shadow": "^2.3.18", "@heroui/system": "^2.4.23", "@heroui/theme": "^2.4.23", "@lezer/highlight": "^1.2.3", "@octokit/rest": "^22.0.1", - "@opencode-ai/sdk": "1.18.18", + "@opencode-ai/sdk": "1.18.21", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -148,8 +148,18 @@ "zustand": "^5.0.8" }, "overrides": { - "@codemirror/language": "6.12.2", - "@codemirror/view": "6.39.13" + "@codemirror/autocomplete": "6.20.3", + "@codemirror/commands": "6.11.0", + "@codemirror/lang-html": "6.4.12", + "@codemirror/lang-javascript": "6.2.5", + "@codemirror/lang-markdown": "6.5.2", + "@codemirror/lang-yaml": "6.1.3", + "@codemirror/language": "6.12.4", + "@codemirror/legacy-modes": "6.5.3", + "@codemirror/lint": "6.9.7", + "@codemirror/search": "6.7.1", + "@codemirror/state": "6.7.1", + "@codemirror/view": "6.43.9" }, "devDependencies": { "@clack/prompts": "^1.1.0", diff --git a/packages/docs/content/docs/de/integrations.mdx b/packages/docs/content/docs/de/integrations.mdx new file mode 100644 index 00000000..38b24051 --- /dev/null +++ b/packages/docs/content/docs/de/integrations.mdx @@ -0,0 +1,54 @@ +--- +title: Integrationen +description: Nutze dein Claude- oder Cursor-Abo als Provider. +--- + +# Integrationen + +Eine Integration ist ein kleines Plugin, das OpenChamber einen Provider hinzufügt — auf Basis eines Abos, das du bereits hast. Verwalten kannst du sie unter **Settings → Integrations**. + +> **Experimentelle Funktion.** Wir bemühen uns, die Richtlinien der Anbieter zu respektieren, aber Kontobeschränkungen und Sperrungen liegen bei jedem Anbieter. Nutze Integrationen auf eigenes Risiko. + +Verfügbare Integrationen: + +- **Claude Code** — dein Claude Pro- oder Max-Plan, ohne API-Keys +- **Cursor** — die Modell-Limits deines Cursor-Plans + +## Integration installieren + +1. Öffne **Settings → Integrations**. +2. Suche die Integration und wähle **Install**. +3. Starte OpenCode neu, wenn darum gebeten wird — der Provider erscheint nach dem Neustart. +4. Wähle **Set up** und melde dich an. Die Modelle erscheinen danach in der Modellauswahl im Chat. + +Integrationen werden für deinen Benutzer installiert und funktionieren damit in jedem Projekt. Aktualisieren oder entfernen kannst du sie jederzeit über dieselbe Karte. + +## Claude Code + +Claude Code nutzt deinen Claude Pro- oder Max-Plan — ohne API-Keys und ohne separate Claude-App. + +1. Installiere die Integration (siehe oben). +2. Wähle **Set up** und melde dich an. Wenn du die Claude Code CLI noch nicht hast, bietet die Einrichtung an, sie zuerst zu installieren, und meldet dich danach an. + +Claude Code ist die einzige Integration hier, die ihre Provider-CLI installiert und angemeldet benötigt. Cursor braucht seine CLI nicht. + +**Wie dein Claude-Konto geschützt bleibt:** Diese Integration nutzt das offizielle Claude Agent SDK von Anthropic und deine installierte Claude Code CLI. Sie kapert kein OAuth, extrahiert oder wiederholt keine Browser-Tokens, gibt sich nicht als nicht unterstützter Client aus und umgeht nicht Anthropics Authentifizierung. Sie bleibt auf dem von Anthropic unterstützten Zugriffsweg und trägt daher nicht das mit Token-Hijacking oder unautorisierten Authentifizierungsumgehungen verbundene Sperrrisiko. + +## Cursor + +Cursor macht die Modelle deines Cursor-Plans in OpenChamber nutzbar. + +1. Installiere die Integration (siehe oben). +2. Wähle **Set up**, öffne den Link und genehmige den Zugriff im Browser. Kein API-Key nötig. Die Modellliste lädt nach der Anmeldung automatisch. + +## Aktualisieren oder entfernen + +- **Update** installiert die neueste veröffentlichte Version des Plugins. +- **Remove** löscht das Plugin aus deiner OpenCode-Konfiguration. Der Provider wird beim nächsten Neuladen von OpenCode nicht mehr geladen. + +Wenn eine Karte meldet, dass Einträge manuell verwaltet werden müssen, wähle **Manage plugins** und bereinige die Duplikate dort. + +## Verwandtes + +- [Anbieter, Modelle und Agenten](/de/providers/) — weitere Provider verbinden und Modelle wählen +- [Nutzung und Kontingente](/de/usage/) — verfolge, wie viel du genutzt hast diff --git a/packages/docs/content/docs/de/providers.mdx b/packages/docs/content/docs/de/providers.mdx index 4d5738f5..f559dab2 100644 --- a/packages/docs/content/docs/de/providers.mdx +++ b/packages/docs/content/docs/de/providers.mdx @@ -45,5 +45,6 @@ Provider-Anmeldungen werden von OpenCode gespeichert, nicht von OpenChamber, dah ## Weiterführend +- [Integrationen](/integrations/) — nutze ein Claude-, Command-Code- oder Cursor-Abo als Provider - [MCP-Server](/mcp/) — füge Agents zusätzliche Werkzeuge hinzu - [Nutzung & Kontingente](/usage/) — verfolge, wie viel du verbraucht hast diff --git a/packages/docs/content/docs/de/skills-catalog.mdx b/packages/docs/content/docs/de/skills-catalog.mdx index afb5f31e..7026837b 100644 --- a/packages/docs/content/docs/de/skills-catalog.mdx +++ b/packages/docs/content/docs/de/skills-catalog.mdx @@ -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 ClawHub-Community-Registry — oder nutze die Suche. +2. Durchsuche die eingebauten Quellen — wie das Anthropic-Skills-Repo — 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. diff --git a/packages/docs/content/docs/es/integrations.mdx b/packages/docs/content/docs/es/integrations.mdx new file mode 100644 index 00000000..17582762 --- /dev/null +++ b/packages/docs/content/docs/es/integrations.mdx @@ -0,0 +1,54 @@ +--- +title: Integraciones +description: Usa tu suscripción de Claude o Cursor como proveedor. +--- + +# Integraciones + +Una integración es un pequeño plugin que añade un proveedor a OpenChamber usando una suscripción que ya tienes. Las gestionas en **Settings → Integrations**. + +> **Función experimental.** Buscamos respetar las políticas de los proveedores, pero las restricciones y suspensiones de cuentas son decisión de cada proveedor. Usa las integraciones bajo tu propia responsabilidad. + +Integraciones disponibles: + +- **Claude Code** — tu plan Claude Pro o Max, sin claves de API +- **Cursor** — los límites de modelos de tu plan de Cursor + +## Instalar una integración + +1. Abre **Settings → Integrations**. +2. Busca la integración y elige **Install**. +3. Reinicia OpenCode cuando se te pida — el proveedor aparece tras el reinicio. +4. Elige **Set up** e inicia sesión. Los modelos aparecerán luego en el selector de modelos del chat. + +Las integraciones se instalan para tu usuario, así que funcionan en todos los proyectos. Puedes actualizarlas o eliminarlas desde la misma tarjeta en cualquier momento. + +## Claude Code + +Claude Code usa tu plan Claude Pro o Max — sin claves de API y sin una app de Claude aparte. + +1. Instala la integración (arriba). +2. Elige **Set up** e inicia sesión. Si aún no tienes la CLI de Claude Code, la configuración ofrece instalarla primero y luego iniciar sesión. + +Claude Code es la única integración de esta página que requiere tener la CLI de su proveedor instalada y con sesión iniciada. Cursor no requiere su CLI. + +**Cómo se protege tu cuenta de Claude:** esta integración usa el Claude Agent SDK oficial de Anthropic y tu CLI de Claude Code instalada. No secuestra OAuth, no extrae ni reutiliza tokens del navegador, no se hace pasar por un cliente no admitido ni omite la autenticación de Anthropic. Se mantiene en la vía de acceso admitida por Anthropic, por lo que no conlleva el riesgo de baneo asociado al secuestro de tokens o a rodeos de autenticación no autorizados. + +## Cursor + +Cursor hace disponibles en OpenChamber los modelos incluidos en tu plan de Cursor. + +1. Instala la integración (arriba). +2. Elige **Set up**, abre el enlace y autoriza el acceso en tu navegador. No necesitas clave de API. La lista de modelos se carga automáticamente tras iniciar sesión. + +## Actualizar o eliminar + +- **Update** instala la última versión publicada del plugin. +- **Remove** elimina el plugin de tu configuración de OpenCode. El proveedor deja de cargarse cuando OpenCode se recarga. + +Si una tarjeta indica que las entradas requieren gestión manual, elige **Manage plugins** y limpia ahí los duplicados. + +## Relacionado + +- [Proveedores, modelos y agentes](/es/providers/) — conecta otros proveedores y elige modelos +- [Uso y cuotas](/es/usage/) — sigue cuánto has usado diff --git a/packages/docs/content/docs/es/providers.mdx b/packages/docs/content/docs/es/providers.mdx index e6df93d2..65526b23 100644 --- a/packages/docs/content/docs/es/providers.mdx +++ b/packages/docs/content/docs/es/providers.mdx @@ -45,5 +45,6 @@ Los inicios de sesión de los proveedores los guarda OpenCode, no OpenChamber, a ## Relacionado +- [Integraciones](/es/integrations/) — usa una suscripción de Claude o Cursor como proveedor - [Servidores MCP](/es/mcp/) — añade herramientas extra para los agentes - [Uso y cuotas](/es/usage/) — controla cuánto has consumido diff --git a/packages/docs/content/docs/es/skills-catalog.mdx b/packages/docs/content/docs/es/skills-catalog.mdx index eadec080..c29ffb71 100644 --- a/packages/docs/content/docs/es/skills-catalog.mdx +++ b/packages/docs/content/docs/es/skills-catalog.mdx @@ -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 ClawHub— o busca. +2. Explora las fuentes integradas —como el repositorio de skills de Anthropic— 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. diff --git a/packages/docs/content/docs/fr/integrations.mdx b/packages/docs/content/docs/fr/integrations.mdx new file mode 100644 index 00000000..2628a870 --- /dev/null +++ b/packages/docs/content/docs/fr/integrations.mdx @@ -0,0 +1,54 @@ +--- +title: Intégrations +description: Utilise ton abonnement Claude ou Cursor comme fournisseur. +--- + +# Intégrations + +Une intégration est un petit plugin qui ajoute un fournisseur à OpenChamber à partir d'un abonnement que tu possèdes déjà. Tu les gères dans **Settings → Integrations**. + +> **Fonctionnalité expérimentale.** Nous cherchons à respecter les règles des fournisseurs, mais les restrictions et suspensions de compte relèvent de leur décision. Utilise les intégrations à tes risques. + +Intégrations disponibles : + +- **Claude Code** — ton plan Claude Pro ou Max, sans clés API +- **Cursor** — les limites de modèles de ton plan Cursor + +## Installer une intégration + +1. Ouvre **Settings → Integrations**. +2. Trouve l'intégration et choisis **Install**. +3. Redémarre OpenCode quand c'est demandé — le fournisseur apparaît après le redémarrage. +4. Choisis **Set up** et connecte-toi. Les modèles apparaissent ensuite dans le sélecteur de modèles du chat. + +Les intégrations s'installent pour ton utilisateur et fonctionnent donc dans tous les projets. Tu peux les mettre à jour ou les retirer à tout moment depuis la même carte. + +## Claude Code + +Claude Code utilise ton plan Claude Pro ou Max — sans clés API et sans application Claude séparée. + +1. Installe l'intégration (ci-dessus). +2. Choisis **Set up** et connecte-toi. Si tu n'as pas encore la CLI Claude Code, la configuration propose de l'installer d'abord, puis de te connecter. + +Claude Code est la seule intégration ici qui exige que la CLI de son fournisseur soit installée et connectée. Cursor n'exige pas sa CLI. + +**Comment ton compte Claude reste protégé :** cette intégration utilise le Claude Agent SDK officiel d'Anthropic et ta CLI Claude Code installée. Elle ne détourne pas l'OAuth, n'extrait ni rejoue de tokens de navigateur, ne se fait pas passer pour un client non pris en charge et ne contourne pas l'authentification d'Anthropic. Elle reste sur la voie d'accès prise en charge par Anthropic et ne porte donc pas le risque de bannissement associé au détournement de tokens ou aux contournements d'authentification non autorisés. + +## Cursor + +Cursor rend disponibles dans OpenChamber les modèles inclus dans ton plan Cursor. + +1. Installe l'intégration (ci-dessus). +2. Choisis **Set up**, ouvre le lien et autorise l'accès dans ton navigateur. Aucune clé API n'est nécessaire. La liste des modèles se charge automatiquement après la connexion. + +## Mettre à jour ou retirer + +- **Update** installe la dernière version publiée du plugin. +- **Remove** supprime le plugin de ta configuration OpenCode. Le fournisseur cesse d'être chargé au prochain rechargement d'OpenCode. + +Si une carte indique que les entrées nécessitent une gestion manuelle, choisis **Manage plugins** et nettoie les doublons à cet endroit. + +## À voir aussi + +- [Fournisseurs, modèles et agents](/fr/providers/) — connecter d'autres fournisseurs et choisir des modèles +- [Utilisation et quotas](/fr/usage/) — suis ta consommation diff --git a/packages/docs/content/docs/fr/providers.mdx b/packages/docs/content/docs/fr/providers.mdx index dde4838e..a3ce0149 100644 --- a/packages/docs/content/docs/fr/providers.mdx +++ b/packages/docs/content/docs/fr/providers.mdx @@ -45,5 +45,6 @@ Les connexions aux fournisseurs sont stockées par OpenCode, pas OpenChamber ; e ## Pages liées +- [Intégrations](/integrations/) — utiliser un abonnement Claude ou Cursor comme fournisseur - [Serveurs MCP](/mcp/) — ajouter des outils supplémentaires aux agents - [Utilisation et quotas](/usage/) — suivre votre consommation diff --git a/packages/docs/content/docs/fr/skills-catalog.mdx b/packages/docs/content/docs/fr/skills-catalog.mdx index 72123162..e6eb2323 100644 --- a/packages/docs/content/docs/fr/skills-catalog.mdx +++ b/packages/docs/content/docs/fr/skills-catalog.mdx @@ -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 ClawHub — ou lancez une recherche. +2. Parcourez les sources intégrées — comme le dépôt de skills Anthropic — ou lancez une recherche. 3. Choisissez un skill et installez-le. 4. Choisissez où l’installer : pour tout ce que vous faites, ou seulement pour le projet actuel. diff --git a/packages/docs/content/docs/integrations.mdx b/packages/docs/content/docs/integrations.mdx new file mode 100644 index 00000000..8aef2d38 --- /dev/null +++ b/packages/docs/content/docs/integrations.mdx @@ -0,0 +1,54 @@ +--- +title: Integrations +description: Use your Claude or Cursor subscription as a provider. +--- + +# Integrations + +An integration is a small plugin that adds a provider to OpenChamber using a subscription you already have. You manage them at **Settings → Integrations**. + +> **Experimental feature.** We aim to respect provider policies, but account restrictions and suspensions remain each provider's decision. Use integrations at your own risk. + +Available integrations: + +- **Claude Code** — your Claude Pro or Max plan, no API keys +- **Cursor** — the model limits of your Cursor plan + +## Install an integration + +1. Open **Settings → Integrations**. +2. Find the integration and choose **Install**. +3. Restart OpenCode when asked — the provider appears after the restart. +4. Choose **Set up** and sign in. The models then appear in the chat model picker. + +Integrations install for your user, so they work in every project. You can update or remove them from the same card at any time. + +## Claude Code + +Claude Code uses your Claude Pro or Max plan — no API keys and no separate Claude app. + +1. Install the integration (above). +2. Choose **Set up** and sign in. If you don't have the Claude Code CLI yet, setup offers to install it first and then sign you in. + +Claude Code is the only integration here that requires its provider CLI to be installed and signed in. Cursor does not require its CLI. + +**How your Claude account stays safe:** this integration uses Anthropic's official Claude Agent SDK and your installed Claude Code CLI. It does not hijack OAuth, extract or replay browser tokens, impersonate an unsupported client, or bypass Anthropic's authentication flow. It stays on Anthropic's supported access path, so it does not carry the account-ban risk of token hijacking or unauthorized authentication workarounds. + +## Cursor + +Cursor makes the models included in your Cursor plan available in OpenChamber. + +1. Install the integration (above). +2. Choose **Set up**, open the link, and approve access in your browser. No API key is required. The model list loads automatically after you sign in. + +## Update or remove + +- **Update** installs the latest published version of the plugin. +- **Remove** deletes the plugin from your OpenCode config. The provider stops loading after OpenCode refreshes. + +If a card says the entries need manual management, choose **Manage plugins** and clean up the duplicates there. + +## Related + +- [Providers, Models & Agents](/providers/) — connect other providers and pick models +- [Usage & Quotas](/usage/) — track how much you've used diff --git a/packages/docs/content/docs/ja/integrations.mdx b/packages/docs/content/docs/ja/integrations.mdx new file mode 100644 index 00000000..b13ac0cd --- /dev/null +++ b/packages/docs/content/docs/ja/integrations.mdx @@ -0,0 +1,54 @@ +--- +title: 統合機能 +description: Claude または Cursor のサブスクリプションをプロバイダーとして使う。 +--- + +# 統合機能 + +統合機能(インテグレーション)は、すでに持っているサブスクリプションを使って OpenChamber にプロバイダーを追加する小さなプラグインです。**Settings → Integrations** で管理します。 + +> **実験的な機能。** プロバイダーの方針を尊重するよう努めていますが、アカウントの制限や停止は各プロバイダーの判断に委ねられます。自己責任で連携を使用してください。 + +利用できる統合機能: + +- **Claude Code** — Claude Pro または Max プラン、API キー不要 +- **Cursor** — Cursor プランのモデル利用枠 + +## 統合機能をインストールする + +1. **Settings → Integrations** を開きます。 +2. 統合機能を見つけて **Install** を選びます。 +3. 求められたら OpenCode を再起動します — 再起動後にプロバイダーが現れます。 +4. **Set up** を選んでサインインします。するとチャットのモデル選択にモデルが表示されます。 + +統合機能はユーザー単位でインストールされるため、すべてのプロジェクトで使えます。同じカードからいつでも更新や削除ができます。 + +## Claude Code + +Claude Code は Claude Pro または Max プランを使います — API キーも Claude アプリも不要です。 + +1. 統合機能をインストールします(上記)。 +2. **Set up** を選んでサインインします。Claude Code CLI がまだない場合は、セットアップがまずインストールを提案し、その後サインインします。 + +Claude Code は、ここで唯一プロバイダーの CLI のインストールとサインインを必要とする統合機能です。Cursor は CLI を必要としません。 + +**Claude アカウントが守られる仕組み:** この統合機能は Anthropic の公式 Claude Agent SDK と、インストール済みの Claude Code CLI を使用します。OAuth の乗っ取り、ブラウザートークンの抽出や再生、未対応クライアントへの偽装、Anthropic の認証フローの回避は一切行いません。Anthropic がサポートする正規のアクセス経路を使うため、トークン乗っ取りや不正な認証の回避につきもののアカウント停止リスクはありません。 + +## Cursor + +Cursor は Cursor プランに含まれるモデルを OpenChamber で使えるようにします。 + +1. 統合機能をインストールします(上記)。 +2. **Set up** を選び、リンクを開いてブラウザーでアクセスを許可します。API キーは不要です。サインイン後、モデル一覧は自動的に読み込まれます。 + +## 更新と削除 + +- **Update** はプラグインの最新の公開バージョンをインストールします。 +- **Remove** は OpenCode の設定からプラグインを削除します。OpenCode が再読み込みされるとプロバイダーは読み込まれなくなります。 + +カードに手動での管理が必要だと表示された場合は、**Manage plugins** を選んで重複を整理してください。 + +## 関連情報 + +- [プロバイダー、モデル、エージェント](/ja/providers/) — 他のプロバイダーの接続とモデルの選択 +- [使用量とクォータ](/ja/usage/) — 利用量を追跡 diff --git a/packages/docs/content/docs/ja/providers.mdx b/packages/docs/content/docs/ja/providers.mdx index 56b1616b..7e9c0525 100644 --- a/packages/docs/content/docs/ja/providers.mdx +++ b/packages/docs/content/docs/ja/providers.mdx @@ -45,5 +45,6 @@ OpenChamber が何かを行うには、少なくとも 1 つの AI プロバイ ## 関連 +- [統合機能](/integrations/) — Claude または Cursor のサブスクリプションをプロバイダーとして使う - [MCP サーバー](/mcp/) — エージェントに追加ツールを加える - [使用量とクォータ](/usage/) — 使った量を追跡する diff --git a/packages/docs/content/docs/ja/skills-catalog.mdx b/packages/docs/content/docs/ja/skills-catalog.mdx index 8991f05f..029a0882 100644 --- a/packages/docs/content/docs/ja/skills-catalog.mdx +++ b/packages/docs/content/docs/ja/skills-catalog.mdx @@ -12,7 +12,7 @@ Skills Catalog では、自分で書く代わりに、他の人が公開した ## スキルをインストールする 1. カタログを開きます。 -2. 組み込みソース(Anthropic skills repo と ClawHub community registry)を閲覧するか、検索します。 +2. 組み込みソース(Anthropic skills repo など)を閲覧するか、検索します。 3. スキルを選び、インストールします。 4. インストール先を選びます。すべての作業で使うか、現在のプロジェクトだけで使うかです。 diff --git a/packages/docs/content/docs/ko/integrations.mdx b/packages/docs/content/docs/ko/integrations.mdx new file mode 100644 index 00000000..0663879e --- /dev/null +++ b/packages/docs/content/docs/ko/integrations.mdx @@ -0,0 +1,54 @@ +--- +title: 통합 기능 +description: Claude 또는 Cursor 구독을 공급자로 사용하세요. +--- + +# 통합 기능 + +통합 기능(인테그레이션)은 이미 가지고 있는 구독을 사용해 OpenChamber에 공급자를 추가하는 작은 플러그인입니다. **Settings → Integrations**에서 관리합니다. + +> **실험 단계 기능.** 프로바이더 정책을 존중하려 노력하지만, 계정 제한과 정지는 각 프로바이더의 결정입니다. 본인의 책임 아래 통합 기능을 사용하세요. + +사용 가능한 통합 기능: + +- **Claude Code** — Claude Pro 또는 Max 플랜, API 키 불필요 +- **Cursor** — Cursor 플랜의 모델 한도 + +## 통합 기능 설치 + +1. **Settings → Integrations**를 엽니다. +2. 통합 기능을 찾아 **Install**을 선택합니다. +3. 요청되면 OpenCode를 다시 시작합니다 — 재시작 후 공급자가 나타납니다. +4. **Set up**를 선택하고 로그인합니다. 그러면 채팅의 모델 선택기에 모델이 나타납니다. + +통합 기능은 사용자 단위로 설치되므로 모든 프로젝트에서 작동합니다. 같은 카드에서 언제든 업데이트하거나 제거할 수 있습니다. + +## Claude Code + +Claude Code는 Claude Pro 또는 Max 플랜을 사용합니다 — API 키도 별도의 Claude 앱도 필요 없습니다. + +1. 통합 기능을 설치합니다(위 참고). +2. **Set up**를 선택하고 로그인합니다. Claude Code CLI가 아직 없으면 설정에서 먼저 설치를 제안한 뒤 로그인을 진행합니다. + +Claude Code는 여기에서 유일하게 공급자 CLI 설치와 로그인을 필요로 하는 통합 기능입니다. Cursor는 CLI가 필요 없습니다. + +**Claude 계정이 안전하게 유지되는 방식:** 이 통합 기능은 Anthropic의 공식 Claude Agent SDK와 설치된 Claude Code CLI를 사용합니다. OAuth 탈취, 브라우저 토큰 추출·재사용, 지원되지 않는 클라이언트로의 위장, Anthropic 인증 우회를 하지 않습니다. Anthropic이 지원하는 정상 경로를 사용하므로 토큰 탈취나 비인가 인증 우회에 따른 계정 정지 위험이 없습니다. + +## Cursor + +Cursor는 Cursor 플랜에 포함된 모델을 OpenChamber에서 사용할 수 있게 합니다. + +1. 통합 기능을 설치합니다(위 참고). +2. **Set up**를 선택하고 링크를 열어 브라우저에서 접근을 승인합니다. API 키는 필요 없습니다. 로그인 후 모델 목록이 자동으로 로드됩니다. + +## 업데이트 및 제거 + +- **Update**는 플러그인의 최신 공개 버전을 설치합니다. +- **Remove**는 OpenCode 설정에서 플러그인을 삭제합니다. OpenCode가 다시 로드되면 공급자는 더 이상 로드되지 않습니다. + +카드에 항목을 수동으로 관리해야 한다고 표시되면 **Manage plugins**를 선택해 중복 항목을 정리하세요. + +## 관련 문서 + +- [공급자, 모델, 에이전트](/ko/providers/) — 다른 공급자 연결과 모델 선택 +- [사용량 및 할당량](/ko/usage/) — 사용량 추적 diff --git a/packages/docs/content/docs/ko/providers.mdx b/packages/docs/content/docs/ko/providers.mdx index 16869b26..98fb109d 100644 --- a/packages/docs/content/docs/ko/providers.mdx +++ b/packages/docs/content/docs/ko/providers.mdx @@ -45,5 +45,6 @@ OpenChamber가 무언가를 하려면 먼저 최소한 하나의 AI 공급자가 ## 관련 항목 +- [통합 기능](/ko/integrations/) — Claude 또는 Cursor 구독을 공급자로 사용 - [MCP Servers](/ko/mcp/) — 에이전트에 추가 도구를 제공합니다 - [Usage & Quotas](/ko/usage/) — 사용량을 추적합니다 diff --git a/packages/docs/content/docs/ko/skills-catalog.mdx b/packages/docs/content/docs/ko/skills-catalog.mdx index ba16461a..d2cbb407 100644 --- a/packages/docs/content/docs/ko/skills-catalog.mdx +++ b/packages/docs/content/docs/ko/skills-catalog.mdx @@ -12,7 +12,7 @@ Skills Catalog를 사용하면 직접 작성하는 대신 다른 사람이 게 ## 스킬 설치하기 1. 카탈로그를 엽니다. -2. 내장된 소스(Anthropic 스킬 저장소와 ClawHub 커뮤니티 레지스트리)를 둘러보거나 검색합니다. +2. 내장된 소스(예: Anthropic 스킬 저장소)를 둘러보거나 검색합니다. 3. 스킬을 선택하고 설치합니다. 4. 설치 위치를 선택합니다. 모든 작업에 적용할지, 현재 프로젝트에만 적용할지 선택합니다. diff --git a/packages/docs/content/docs/pl/integrations.mdx b/packages/docs/content/docs/pl/integrations.mdx new file mode 100644 index 00000000..7e3ddf2c --- /dev/null +++ b/packages/docs/content/docs/pl/integrations.mdx @@ -0,0 +1,54 @@ +--- +title: Integracje +description: Używaj subskrypcji Claude lub Cursor jako dostawcy. +--- + +# Integracje + +Integracja to mała wtyczka, która dodaje dostawcę do OpenChamber na podstawie subskrypcji, którą już masz. Zarządzasz nimi w **Settings → Integrations**. + +> **Funkcja eksperymentalna.** Staramy się przestrzegać zasad dostawców, ale ograniczenia i zawieszenia kont pozostają decyzją każdego dostawcy. Używaj integracji na własne ryzyko. + +Dostępne integracje: + +- **Claude Code** — Twój plan Claude Pro lub Max, bez kluczy API +- **Cursor** — limity modeli z Twojego planu Cursor + +## Instalacja integracji + +1. Otwórz **Settings → Integrations**. +2. Znajdź integrację i wybierz **Install**. +3. Uruchom OpenCode ponownie, gdy o to poproszą — dostawca pojawi się po restarcie. +4. Wybierz **Set up** i zaloguj się. Modele pojawią się potem w selektorze modeli na czacie. + +Integracje instalują się dla Twojego użytkownika, więc działają w każdym projekcie. W każdej chwili możesz je zaktualizować lub usunąć z tej samej karty. + +## Claude Code + +Claude Code korzysta z Twojego planu Claude Pro lub Max — bez kluczy API i bez osobnej aplikacji Claude. + +1. Zainstaluj integrację (patrz wyżej). +2. Wybierz **Set up** i zaloguj się. Jeśli nie masz jeszcze Claude Code CLI, konfiguracja zaoferuje najpierw jego instalację, a potem logowanie. + +Claude Code jest jedyną integracją tutaj, która wymaga zainstalowanego i zalogowanego CLI swojego dostawcy. Cursor nie wymaga swojego CLI. + +**Jak chronione jest Twoje konto Claude:** ta integracja używa oficjalnego Claude Agent SDK od Anthropic i Twojego zainstalowanego Claude Code CLI. Nie przechwytuje OAuth, nie wyodrębnia ani nie odtwarza tokenów przeglądarki, nie podszywa się pod nieobsługiwany klient i nie omija uwierzytelniania Anthropic. Działa na obsługiwanej przez Anthropic ścieżce dostępu, więc nie niesie ryzyka zablokowania konta związanego z przechwytywaniem tokenów lub nieautoryzowanymi obejściami uwierzytelniania. + +## Cursor + +Cursor udostępnia w OpenChamber modele zawarte w Twoim planie Cursor. + +1. Zainstaluj integrację (patrz wyżej). +2. Wybierz **Set up**, otwórz link i zatwierdź dostęp w przeglądarce. Klucz API nie jest potrzebny. Lista modeli wczyta się automatycznie po zalogowaniu. + +## Aktualizacja i usuwanie + +- **Update** instaluje najnowszą opublikowaną wersję wtyczki. +- **Remove** usuwa wtyczkę z konfiguracji OpenCode. Dostawca przestanie się ładować po odświeżeniu OpenCode. + +Jeśli karta wskazuje, że wpisy wymagają ręcznego zarządzania, wybierz **Manage plugins** i wyczyść tam duplikaty. + +## Powiązane + +- [Dostawcy, modele i agenci](/pl/providers/) — podłączanie innych dostawców i wybór modeli +- [Zużycie i limity](/pl/usage/) — śledź, ile wykorzystałeś diff --git a/packages/docs/content/docs/pl/providers.mdx b/packages/docs/content/docs/pl/providers.mdx index 8675b91a..4c777176 100644 --- a/packages/docs/content/docs/pl/providers.mdx +++ b/packages/docs/content/docs/pl/providers.mdx @@ -45,5 +45,6 @@ Logowania dostawców są przechowywane przez OpenCode, a nie OpenChamber, więc ## Powiązane +- [Integracje](/pl/integrations/) — używaj subskrypcji Claude lub Cursor jako dostawcy - [Serwery MCP](/pl/mcp/) — dodaj agentom dodatkowe narzędzia - [Zużycie i limity](/pl/usage/) — śledź, ile już wykorzystałeś diff --git a/packages/docs/content/docs/pl/skills-catalog.mdx b/packages/docs/content/docs/pl/skills-catalog.mdx index d3544fa1..d38a842f 100644 --- a/packages/docs/content/docs/pl/skills-catalog.mdx +++ b/packages/docs/content/docs/pl/skills-catalog.mdx @@ -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 ClawHub — albo wyszukaj. +2. Przeglądaj wbudowane źródła — na przykład repozytorium skilli Anthropic — albo wyszukaj. 3. Wybierz skill i zainstaluj go. 4. Wybierz, gdzie go zainstalować: dla wszystkiego, co robisz, albo tylko dla bieżącego projektu. diff --git a/packages/docs/content/docs/providers.mdx b/packages/docs/content/docs/providers.mdx index 6b8a8e83..2505ced4 100644 --- a/packages/docs/content/docs/providers.mdx +++ b/packages/docs/content/docs/providers.mdx @@ -57,5 +57,6 @@ Provider sign-ins are stored by OpenCode, not OpenChamber, so they're shared wit ## Related +- [Integrations](/integrations/) — use a Claude or Cursor subscription as a provider - [MCP Servers](/mcp/) — add extra tools for agents - [Usage & Quotas](/usage/) — track how much you've used diff --git a/packages/docs/content/docs/pt-br/integrations.mdx b/packages/docs/content/docs/pt-br/integrations.mdx new file mode 100644 index 00000000..82ed2e68 --- /dev/null +++ b/packages/docs/content/docs/pt-br/integrations.mdx @@ -0,0 +1,54 @@ +--- +title: Integrações +description: Use sua assinatura Claude ou Cursor como provedor. +--- + +# Integrações + +Uma integração é um pequeno plugin que adiciona um provedor ao OpenChamber usando uma assinatura que você já tem. Você as gerencia em **Settings → Integrations**. + +> **Recurso experimental.** Buscamos respeitar as políticas dos provedores, mas restrições e suspensões de conta continuam sendo decisão de cada provedor. Use as integrações por sua conta e risco. + +Integrações disponíveis: + +- **Claude Code** — seu plano Claude Pro ou Max, sem chaves de API +- **Cursor** — os limites de modelos do seu plano Cursor + +## Instalar uma integração + +1. Abra **Settings → Integrations**. +2. Encontre a integração e escolha **Install**. +3. Reinicie o OpenCode quando solicitado — o provedor aparece após a reinicialização. +4. Escolha **Set up** e faça login. Os modelos aparecem então no seletor de modelos do chat. + +As integrações são instaladas para o seu usuário, então funcionam em todos os projetos. Você pode atualizá-las ou removê-las do mesmo cartão a qualquer momento. + +## Claude Code + +O Claude Code usa seu plano Claude Pro ou Max — sem chaves de API e sem um app Claude separado. + +1. Instale a integração (acima). +2. Escolha **Set up** e faça login. Se você ainda não tem a CLI do Claude Code, a configuração oferece instalá-la primeiro e depois fazer login. + +O Claude Code é a única integração aqui que exige que a CLI do provedor esteja instalada e autenticada. Cursor não exige sua CLI. + +**Como sua conta Claude fica protegida:** esta integração usa o Claude Agent SDK oficial da Anthropic e a CLI do Claude Code instalada em sua máquina. Ela não sequestra OAuth, não extrai nem reproduz tokens do navegador, não se passa por um cliente não suportado e não contorna a autenticação da Anthropic. Ela permanece no caminho de acesso suportado pela Anthropic, portanto não traz o risco de banimento de conta associado a sequestro de tokens ou a contornos de autenticação não autorizados. + +## Cursor + +O Cursor torna disponíveis no OpenChamber os modelos incluídos no seu plano Cursor. + +1. Instale a integração (acima). +2. Escolha **Set up**, abra o link e autorize o acesso no navegador. Nenhuma chave de API é necessária. A lista de modelos carrega automaticamente após o login. + +## Atualizar ou remover + +- **Update** instala a versão publicada mais recente do plugin. +- **Remove** exclui o plugin da sua configuração do OpenCode. O provedor deixa de ser carregado quando o OpenCode é recarregado. + +Se um cartão indicar que as entradas precisam de gerenciamento manual, escolha **Manage plugins** e limpe as duplicatas lá. + +## Relacionado + +- [Provedores, modelos e agentes](/pt-br/providers/) — conecte outros provedores e escolha modelos +- [Uso e cotas](/pt-br/usage/) — acompanhe quanto você usou diff --git a/packages/docs/content/docs/pt-br/providers.mdx b/packages/docs/content/docs/pt-br/providers.mdx index 035002bc..70ece24f 100644 --- a/packages/docs/content/docs/pt-br/providers.mdx +++ b/packages/docs/content/docs/pt-br/providers.mdx @@ -45,5 +45,6 @@ Os logins de provedores são armazenados pelo OpenCode, não pelo OpenChamber, e ## Relacionado +- [Integrações](/pt-br/integrations/) — use uma assinatura Claude ou Cursor como provedor - [Servidores MCP](/pt-br/mcp/) — adicione ferramentas extras para os agentes - [Uso e Cotas](/pt-br/usage/) — acompanhe quanto você já usou diff --git a/packages/docs/content/docs/pt-br/skills-catalog.mdx b/packages/docs/content/docs/pt-br/skills-catalog.mdx index eea841f4..b28c44f3 100644 --- a/packages/docs/content/docs/pt-br/skills-catalog.mdx +++ b/packages/docs/content/docs/pt-br/skills-catalog.mdx @@ -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 ClawHub — ou pesquise. +2. Navegue pelas fontes integradas — como o repositório de skills da Anthropic — 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. diff --git a/packages/docs/content/docs/skills-catalog.mdx b/packages/docs/content/docs/skills-catalog.mdx index 9ddcb131..e223a459 100644 --- a/packages/docs/content/docs/skills-catalog.mdx +++ b/packages/docs/content/docs/skills-catalog.mdx @@ -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 ClawHub community registry — or search. +2. Browse the built-in sources — like the Anthropic skills repo — or search. 3. Pick a skill and install it. 4. Choose where to install it: for everything you do, or just the current project. diff --git a/packages/docs/content/docs/uk/integrations.mdx b/packages/docs/content/docs/uk/integrations.mdx new file mode 100644 index 00000000..d6d075f4 --- /dev/null +++ b/packages/docs/content/docs/uk/integrations.mdx @@ -0,0 +1,54 @@ +--- +title: Інтеграції +description: Використовуйте підписки Claude або Cursor як провайдерів. +--- + +# Інтеграції + +Інтеграція — це невеликий плагін, що додає провайдера до OpenChamber на основі підписки, яка в вас уже є. Керувати ними можна в **Settings → Integrations**. + +> **Експериментальна функція.** Ми прагнемо дотримуватися політик провайдерів, але обмеження та блокування облікових записів залишаються рішенням кожного провайдера. Використовуйте інтеграції на власний ризик. + +Доступні інтеграції: + +- **Claude Code** — ваша підписка Claude Pro або Max, без API-ключів +- **Cursor** — ліміти моделей вашої підписки Cursor + +## Встановлення інтеграції + +1. Відкрийте **Settings → Integrations**. +2. Знайдіть інтеграцію та натисніть **Install**. +3. Перезапустіть OpenCode, коли про це попросять, — провайдер з'явиться після перезапуску. +4. Натисніть **Set up** і увійдіть. Після цього моделі з'являться в перемикачі моделей у чаті. + +Інтеграції встановлюються для вашого користувача, тож працюють у всіх проєктах. Оновити або видалити їх можна в будь-який момент з тієї ж карточки. + +## Claude Code + +Claude Code використовує вашу підписку Claude Pro або Max — без API-ключів і без окремого застосунку Claude. + +1. Встановіть інтеграцію (вище). +2. Натисніть **Set up** і увійдіть. Якщо у вас ще немає Claude Code CLI, програма встановлення спершу запропонує його встановити, а потім виконає вхід. + +Claude Code — єдина інтеграція тут, яка вимагає встановленого та залогіненого CLI свого провайдера. Для Cursor CLI не потрібен. + +**Як захищається ваш обліковий запис Claude:** ця інтеграція використовує офіційний Claude Agent SDK від Anthropic і ваш встановлений Claude Code CLI. Вона не перехоплює OAuth, не витягує й не відтворює браузерні токени, не видає себе за непідтримуваний клієнт і не обходить процес автентифікації Anthropic. Усе працює через підтримуваний Anthropic шлях доступу, тож інтеграція не несе ризику блокування облікового запису, пов'язаного з перехопленням токенів або несанкціонованими способами автентифікації. + +## Cursor + +Cursor робить доступними в OpenChamber моделі, що входять у вашу підписку Cursor. + +1. Встановіть інтеграцію (вище). +2. Натисніть **Set up**, відкрийте посилання та підтвердьте доступ у браузері. API-ключ не потрібен. Список моделей завантажиться автоматично після входу. + +## Оновлення та видалення + +- **Update** встановлює найновішу опубліковану версію плагіна. +- **Remove** видаляє плагін із конфігурації OpenCode. Провайдер перестане завантажуватися після оновлення OpenCode. + +Якщо карточка повідомляє, що записи потребують ручного керування, натисніть **Manage plugins** і приберіть дублікати там. + +## Пов'язане + +- [Провайдери, моделі та агенти](/uk/providers/) — підключення інших провайдерів і вибір моделей +- [Використання та квоти](/uk/usage/) — відстежуйте, скільки ви витратили diff --git a/packages/docs/content/docs/uk/providers.mdx b/packages/docs/content/docs/uk/providers.mdx index 693e15a7..9f2777e5 100644 --- a/packages/docs/content/docs/uk/providers.mdx +++ b/packages/docs/content/docs/uk/providers.mdx @@ -45,5 +45,6 @@ description: Підключайте AI-провайдерів, обирайте ## Пов'язане +- [Інтеграції](/uk/integrations/) — використовуйте підписки Claude або Cursor як провайдерів - [MCP Servers](/uk/mcp/) — додайте агентам додаткові інструменти - [Використання та квоти](/uk/usage/) — відстежуйте, скільки ви витратили diff --git a/packages/docs/content/docs/uk/skills-catalog.mdx b/packages/docs/content/docs/uk/skills-catalog.mdx index 78c0c1d6..585ad77a 100644 --- a/packages/docs/content/docs/uk/skills-catalog.mdx +++ b/packages/docs/content/docs/uk/skills-catalog.mdx @@ -12,7 +12,7 @@ description: Переглядайте та встановлюйте готові ## Встановлення навички 1. Відкрийте каталог. -2. Перегляньте вбудовані джерела — репозиторій навичок Anthropic та спільнотний реєстр ClawHub — або скористайтеся пошуком. +2. Перегляньте вбудовані джерела — наприклад репозиторій навичок Anthropic — або скористайтеся пошуком. 3. Оберіть навичку й установіть її. 4. Виберіть, куди встановити: для всього, що ви робите, чи лише для поточного проєкту. diff --git a/packages/docs/content/docs/zh-cn/integrations.mdx b/packages/docs/content/docs/zh-cn/integrations.mdx new file mode 100644 index 00000000..a5686e3c --- /dev/null +++ b/packages/docs/content/docs/zh-cn/integrations.mdx @@ -0,0 +1,54 @@ +--- +title: 集成 +description: 将你的 Claude 或 Cursor 订阅用作提供商。 +--- + +# 集成 + +集成是一个小型插件,它使用你已有的订阅为 OpenChamber 添加一个提供商。你可以在 **Settings → Integrations** 中管理它们。 + +> **实验性功能。**我们力求遵守提供商的政策,但帐户限制和暂停仍由各提供商决定。请自行承担使用集成的风险。 + +可用的集成: + +- **Claude Code** — 你的 Claude Pro 或 Max 套餐,无需 API 密钥 +- **Cursor** — 你的 Cursor 套餐的模型额度 + +## 安装集成 + +1. 打开 **Settings → Integrations**。 +2. 找到所需的集成并选择 **Install**。 +3. 在提示时重启 OpenCode — 重启后提供商就会出现。 +4. 选择 **Set up** 并登录。之后模型会出现在聊天室的模型选择器中。 + +集成按用户安装,因此在所有项目中都可用。你可以随时在同一张卡片上更新或移除它们。 + +## Claude Code + +Claude Code 使用你的 Claude Pro 或 Max 套餐 — 无需 API 密钥,也无需单独的 Claude 应用。 + +1. 安装集成(见上文)。 +2. 选择 **Set up** 并登录。如果你还没有 Claude Code CLI,安装向导会先提供安装,然后再登录。 + +Claude Code 是这里唯一要求安装并登录其提供商 CLI 的集成。Cursor 不需要其 CLI。 + +**你的 Claude 账户如何受到保护:** 此集成使用 Anthropic 官方的 Claude Agent SDK 和你已安装的 Claude Code CLI。它不会劫持 OAuth,不会提取或重放浏览器令牌,不会冒充不受支持的客户端,也不会绕过 Anthropic 的身份验证。它始终运行在 Anthropic 支持的访问路径上,因此不会带来与令牌劫持或未授权身份验证变通手段相关的封号风险。 + +## Cursor + +Cursor 让你的 Cursor 套餐中包含的模型可以在 OpenChamber 中使用。 + +1. 安装集成(见上文)。 +2. 选择 **Set up**,打开链接并在浏览器中授权访问。无需 API 密钥。登录后模型列表会自动加载。 + +## 更新或移除 + +- **Update** 安装插件的最新发布版本。 +- **Remove** 从你的 OpenCode 配置中删除该插件。OpenCode 重新加载后,该提供商将不再加载。 + +如果卡片提示条目需要手动管理,请选择 **Manage plugins** 并在那里清理重复项。 + +## 相关内容 + +- [提供商、模型与智能体](/zh-cn/providers/) — 连接其他提供商并选择模型 +- [用量与配额](/zh-cn/usage/) — 跟踪你的使用量 diff --git a/packages/docs/content/docs/zh-cn/providers.mdx b/packages/docs/content/docs/zh-cn/providers.mdx index 8e92afd0..5e81e7d3 100644 --- a/packages/docs/content/docs/zh-cn/providers.mdx +++ b/packages/docs/content/docs/zh-cn/providers.mdx @@ -45,5 +45,6 @@ description: 连接 AI 提供商、选择模型并设置智能体。 ## 相关内容 +- [集成](/zh-cn/integrations/) — 将 Claude 或 Cursor 订阅用作提供商 - [MCP Servers](/zh-cn/mcp/) — 为智能体添加额外工具 - [用量与配额](/zh-cn/usage/) — 跟踪你已使用的量 diff --git a/packages/docs/content/docs/zh-cn/skills-catalog.mdx b/packages/docs/content/docs/zh-cn/skills-catalog.mdx index f393944e..a3d982b1 100644 --- a/packages/docs/content/docs/zh-cn/skills-catalog.mdx +++ b/packages/docs/content/docs/zh-cn/skills-catalog.mdx @@ -12,7 +12,7 @@ Skills 目录让你能够安装其他人发布的 skill,而不必自己编写 ## 安装 skill 1. 打开目录。 -2. 浏览内置来源 — Anthropic skills 仓库和 ClawHub 社区注册表 — 或进行搜索。 +2. 浏览内置来源 — 例如 Anthropic skills 仓库 — 或进行搜索。 3. 选择一个 skill 并安装它。 4. 选择安装位置:用于你的所有工作,或仅用于当前项目。 diff --git a/packages/docs/sidebar.config.json b/packages/docs/sidebar.config.json index 336241f8..137bbd5b 100644 --- a/packages/docs/sidebar.config.json +++ b/packages/docs/sidebar.config.json @@ -349,6 +349,21 @@ "de": "Anbieter, Modelle und Agenten" } }, + { + "label": "Integrations", + "link": "/integrations/", + "translations": { + "uk": "Інтеграції", + "zh-CN": "集成", + "es": "Integraciones", + "pt-BR": "Integrações", + "ko": "통합 기능", + "pl": "Integracje", + "fr": "Intégrations", + "ja": "統合機能", + "de": "Integrationen" + } + }, { "label": "MCP Servers", "link": "/mcp/", diff --git a/packages/electron/README.md b/packages/electron/README.md index abda712a..7a306894 100644 --- a/packages/electron/README.md +++ b/packages/electron/README.md @@ -143,6 +143,7 @@ Use an explicit override when testing a different OpenCode CLI build or when a u ## Native Features Owned Here - Floating Mini Chat windows. +- New Mini Chat windows default to the managed Chats target. Explicit project/worktree drafts retain their target, existing managed chat sessions reopen in their own directory, and the compact header omits project/branch metadata for Chats. Opening a managed draft back in the main window preserves that target. - Multiple native windows. - Native notifications. - User-confirmed local folder selection. The shared UI supplies the requested directory as the picker `defaultPath`; confirmation is required before filesystem access is retried. @@ -159,7 +160,9 @@ Use an explicit override when testing a different OpenCode CLI build or when a u 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. + never requests anything from a third-party host. Self-signed loopback HTTPS + pages may use an untrusted certificate authority; certificate failures for + external hosts and all other certificate errors remain blocked. ## IPC Pattern diff --git a/packages/electron/browser-panel-security.mjs b/packages/electron/browser-panel-security.mjs new file mode 100644 index 00000000..532148d2 --- /dev/null +++ b/packages/electron/browser-panel-security.mjs @@ -0,0 +1,12 @@ +const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]']); + +export const shouldAllowBrowserPanelCertificateError = ({ url, error }) => { + if (error !== 'net::ERR_CERT_AUTHORITY_INVALID') return false; + + try { + const parsed = new URL(url); + return parsed.protocol === 'https:' && LOOPBACK_HOSTNAMES.has(parsed.hostname.toLowerCase()); + } catch { + return false; + } +}; diff --git a/packages/electron/browser-panel-security.test.mjs b/packages/electron/browser-panel-security.test.mjs new file mode 100644 index 00000000..a81a9e87 --- /dev/null +++ b/packages/electron/browser-panel-security.test.mjs @@ -0,0 +1,41 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { shouldAllowBrowserPanelCertificateError } from './browser-panel-security.mjs'; + +test('allows untrusted certificate authorities for loopback HTTPS pages', () => { + for (const url of [ + 'https://localhost:58580/', + 'https://127.0.0.1:58580/', + 'https://[::1]:58580/', + ]) { + assert.equal(shouldAllowBrowserPanelCertificateError({ + url, + error: 'net::ERR_CERT_AUTHORITY_INVALID', + }), true); + } +}); + +test('keeps certificate validation for non-loopback pages', () => { + for (const url of [ + 'https://example.com/', + 'https://localhost.example.com/', + 'https://0.0.0.0:58580/', + ]) { + assert.equal(shouldAllowBrowserPanelCertificateError({ + url, + error: 'net::ERR_CERT_AUTHORITY_INVALID', + }), false); + } +}); + +test('does not bypass other certificate failures or malformed URLs', () => { + assert.equal(shouldAllowBrowserPanelCertificateError({ + url: 'https://localhost:58580/', + error: 'net::ERR_CERT_DATE_INVALID', + }), false); + assert.equal(shouldAllowBrowserPanelCertificateError({ + url: 'not a url', + error: 'net::ERR_CERT_AUTHORITY_INVALID', + }), false); +}); diff --git a/packages/electron/main.mjs b/packages/electron/main.mjs index 0bcc1de9..0442f814 100644 --- a/packages/electron/main.mjs +++ b/packages/electron/main.mjs @@ -11,6 +11,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; import { promisify } from 'node:util'; import updaterPkg from 'electron-updater'; import { ElectronSshManager } from './ssh-manager.mjs'; +import { replaceFileWithRetry } from './windows-file-replace.mjs'; import { createTrayController } from './tray.mjs'; import { resolveManagedOpenCodeCwd } from './opencode-cwd.mjs'; import { resolveStartupUrlProbePlan, shouldIgnoreLoopbackConnectionLimit } from './startup-url-selection.mjs'; @@ -31,6 +32,7 @@ import { setLinuxAutostartEnabled, } from './linux-autostart.mjs'; import { unsupportedAppSpecificOpenError, validateLocalPath } from './path-open-utils.mjs'; +import { shouldAllowBrowserPanelCertificateError } from './browser-panel-security.mjs'; import { mintOutsideFileGrant } from '@openchamber/web/server/lib/fs/routes.js'; const execFileAsync = promisify(execFile); @@ -559,10 +561,15 @@ const writeJsonFile = async (filePath, data) => { // Atomic: write to a temp file then rename. Readers never see a partial // JSON file that could parse-error and get coerced to {}. const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; - await fsp.writeFile(tmp, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 }); - if (process.platform !== 'win32') await fsp.chmod(tmp, 0o600); - await fsp.rename(tmp, filePath); - if (process.platform !== 'win32') await fsp.chmod(filePath, 0o600); + try { + await fsp.writeFile(tmp, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 }); + if (process.platform !== 'win32') await fsp.chmod(tmp, 0o600); + await replaceFileWithRetry(tmp, filePath); + if (process.platform !== 'win32') await fsp.chmod(filePath, 0o600); + } catch (error) { + await fsp.rm(tmp, { force: true }).catch(() => {}); + throw error; + } }; const readSettingsRoot = () => { @@ -1186,6 +1193,15 @@ const resolveBrowserPanelContents = (rawId) => { const hardenBrowserPanelSession = () => { const panelSession = session.fromPartition(BROWSER_PANEL_PARTITION); + app.on('certificate-error', (event, contents, url, error, _certificate, callback) => { + if (contents.session === panelSession && shouldAllowBrowserPanelCertificateError({ url, error })) { + event.preventDefault(); + callback(true); + return; + } + callback(false); + }); + panelSession.setPermissionRequestHandler((_contents, permission, callback, details) => { log.info('[electron] browser panel denied a permission request', { permission, @@ -1224,7 +1240,15 @@ const registerPackagedUiProtocol = () => { if (filePath.endsWith('.html')) { const html = await fsp.readFile(filePath, 'utf8'); const body = injectRuntimeConfigIntoHtml(html); - return new Response(body, { headers: { 'Content-Type': 'text/html; charset=utf-8' } }); + // index.html must never be cached: it names the hashed asset + // bundles, and a cached copy keeps a freshly installed build + // loading the previous version's UI from the renderer disk cache. + return new Response(body, { + headers: { + 'Content-Type': 'text/html; charset=utf-8', + 'Cache-Control': 'no-store', + }, + }); } return electronNet.fetch(pathToFileURL(filePath).toString()); } @@ -2455,6 +2479,9 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} } browserWindow.__ocRuntimeConfig = { apiBaseUrl: desktopApiBaseUrl, clientToken: desktopClientToken, requestHeaders: desktopRequestHeaders }; browserWindow.__ocInitScript = buildInitScript(desktopLocalOrigin, state.bootOutcome, desktopApiBaseUrl, desktopClientToken, desktopRequestHeaders); browserWindow.__ocTitleBarOverlayEnabled = titleBarOverlayEnabled; + browserWindow.on('app-command', (event, command) => { + if (command === 'browser-backward') event.preventDefault(); + }); if (useSaved && saved.maximized) { browserWindow.maximize(); diff --git a/packages/electron/package.json b/packages/electron/package.json index 5473fc3b..b4d03bec 100644 --- a/packages/electron/package.json +++ b/packages/electron/package.json @@ -1,6 +1,6 @@ { "name": "@openchamber/electron", - "version": "1.18.4", + "version": "1.20.0", "private": true, "description": "Electron desktop runtime for OpenChamber", "author": "OpenChamber", diff --git a/packages/electron/scripts/electron-dev.mjs b/packages/electron/scripts/electron-dev.mjs index a4ee60e4..c8ef57a6 100644 --- a/packages/electron/scripts/electron-dev.mjs +++ b/packages/electron/scripts/electron-dev.mjs @@ -220,7 +220,7 @@ async function main() { }); } - const electron = spawnProcess('npx', ['electron', './main.mjs'], { + const electron = spawnProcess('bun', ['x', 'electron', './main.mjs'], { cwd: electronDir, env: { ...process.env, diff --git a/packages/electron/ssh-manager.mjs b/packages/electron/ssh-manager.mjs index 5c0b52ba..390c2a38 100644 --- a/packages/electron/ssh-manager.mjs +++ b/packages/electron/ssh-manager.mjs @@ -5,9 +5,30 @@ import os from 'node:os'; import path from 'node:path'; import { spawn } from 'node:child_process'; +import { replaceFileWithRetry } from './windows-file-replace.mjs'; + const LOCAL_HOST_ID = 'local'; const DEFAULT_CONNECTION_TIMEOUT_SEC = 60; const DEFAULT_LOCAL_BIND_HOST = '127.0.0.1'; +// Global npm prefixes are root-owned on most distributions, so `npm install -g` +// fails with EACCES for a normal SSH user. Everything we install goes to a +// prefix inside the user's home instead. +const REMOTE_USER_PREFIX = '$HOME/.openchamber/npm-global'; +const REMOTE_BUN_CANDIDATE = '"${BUN_INSTALL:-$HOME/.bun}/bin/bun"'; +// The opencode CLI usually installs into the user's home, which an SSH login +// shell does not have on PATH. The remote server only looks at OPENCODE_BINARY +// and PATH, so resolve the CLI here and hand it over explicitly. +const REMOTE_OPENCODE_CANDIDATES = [ + '"$HOME/.opencode/bin/opencode"', + '"${BUN_INSTALL:-$HOME/.bun}/bin/opencode"', + '"$HOME/.local/bin/opencode"', + '"$HOME/.openchamber/npm-global/bin/opencode"', +]; +const REMOTE_PATH_PREFIX = '$HOME/.opencode/bin:${BUN_INSTALL:-$HOME/.bun}/bin:$HOME/.local/bin:$HOME/.openchamber/npm-global/bin'; +const REMOTE_BIN_CANDIDATES = [ + '"$HOME/.openchamber/npm-global/bin/openchamber"', + '"${BUN_INSTALL:-$HOME/.bun}/bin/openchamber"', +]; const DEFAULT_CONTROL_PERSIST_SEC = 300; const DEFAULT_READY_TIMEOUT_SEC = 30; const DEFAULT_RECONNECT_MAX_ATTEMPTS = 5; @@ -77,10 +98,15 @@ const writeJsonRoot = async (settingsFilePath, root) => { await fsp.mkdir(path.dirname(settingsFilePath), { recursive: true }); // Atomic write: concurrent readers (main.mjs, web server) would otherwise // see partial JSON and readJsonRoot()'s catch would silently coerce to {}, - // causing the next read-modify-write to wipe the entire settings file. + // causing the next read-modify-write wipe the entire settings file. const tmp = `${settingsFilePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; - await fsp.writeFile(tmp, JSON.stringify(root, null, 2)); - await fsp.rename(tmp, settingsFilePath); + try { + await fsp.writeFile(tmp, JSON.stringify(root, null, 2)); + await replaceFileWithRetry(tmp, settingsFilePath); + } catch (error) { + await fsp.rm(tmp, { force: true }).catch(() => {}); + throw error; + } }; const defaultTrue = () => true; @@ -578,6 +604,7 @@ export class ElectronSshManager { localUrl: null, localPort: null, remotePort: null, + remoteBinPath: null, startedByUs: false, retryAttempt: 0, requiresUserAction: false, @@ -804,9 +831,10 @@ export class ElectronSshManager { mode: instance?.remoteOpenchamber?.mode === 'external' ? 'external' : 'managed', keepRunning: instance?.remoteOpenchamber?.keepRunning !== false, ...(Number.isFinite(instance?.remoteOpenchamber?.preferredPort) ? { preferredPort: Number(instance.remoteOpenchamber.preferredPort) } : {}), - installMethod: ['npm', 'bun', 'download_release', 'upload_bundle'].includes(instance?.remoteOpenchamber?.installMethod) + installMethod: ['auto', 'npm', 'bun'].includes(instance?.remoteOpenchamber?.installMethod) ? instance.remoteOpenchamber.installMethod - : 'bun', + : 'auto', + bindHost: instance?.remoteOpenchamber?.bindHost === '0.0.0.0' ? '0.0.0.0' : '127.0.0.1', uploadBundleOverSsh: Boolean(instance?.remoteOpenchamber?.uploadBundleOverSsh), }, localForward: { @@ -981,38 +1009,77 @@ export class ElectronSshManager { return secret?.enabled && typeof secret.value === 'string' && secret.value.trim() ? secret.value.trim() : null; } - async remoteCommandExists(parsed, controlPath, commandName) { - try { - const output = await this.runRemoteCommand(parsed, controlPath, `command -v ${commandName} >/dev/null 2>&1 && echo yes || echo no`); - return output.trim() === 'yes'; - } catch { - return false; - } - } + // A login shell over SSH does not source the user's interactive rc files, so + // tools installed into a home directory (bun above all) are missing from PATH + // even when they exist. Look at their known install locations too. + async resolveRemoteTool(parsed, controlPath, commandName, extraCandidates = []) { + const candidateList = [...extraCandidates, `"$(command -v ${commandName} 2>/dev/null)"`].join(' '); + const script = [ + `for candidate in ${candidateList}; do`, + ' [ -n "$candidate" ] || continue;', + ' [ -x "$candidate" ] || continue;', + ` printf '%s' "$candidate";`, + ' exit 0;', + 'done', + ].join(' '); - async currentRemoteOpenChamberVersion(parsed, controlPath) { try { - const output = await this.runRemoteCommand(parsed, controlPath, 'openchamber --version 2>/dev/null || true'); - return parseVersionToken(output); + const output = await this.runRemoteCommand(parsed, controlPath, script); + return output.trim() || null; } catch { return null; } } - async installOpenChamberManaged(parsed, controlPath, version, preferred) { - const hasBun = await this.remoteCommandExists(parsed, controlPath, 'bun'); - const hasNpm = await this.remoteCommandExists(parsed, controlPath, 'npm'); - const commands = []; + // Every place OpenChamber may live on the remote host, with the version each + // one reports. Installs land in the user prefix while an older copy can still + // sit on PATH, so the caller picks by version instead of trusting PATH order. + async remoteOpenChamberCandidates(parsed, controlPath) { + const script = [ + `for candidate in ${REMOTE_BIN_CANDIDATES.join(' ')} "$(command -v openchamber 2>/dev/null)"; do`, + ' [ -n "$candidate" ] || continue;', + ' [ -x "$candidate" ] || continue;', + ` printf '%s\t%s\n' "$candidate" "$("$candidate" --version 2>/dev/null | head -n 1)";`, + 'done', + ].join(' '); - if (preferred === 'bun') { - if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`); - if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`); - } else if (preferred === 'npm') { - if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`); - if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`); + let output = ''; + try { + output = await this.runRemoteCommand(parsed, controlPath, script); + } catch { + return []; + } + + const candidates = []; + const seen = new Set(); + for (const line of output.split(/\r?\n/)) { + const [binPath, versionRaw] = line.split('\t'); + const trimmed = (binPath || '').trim(); + if (!trimmed || seen.has(trimmed)) continue; + seen.add(trimmed); + candidates.push({ binPath: trimmed, version: parseVersionToken(versionRaw || '') }); + } + return candidates; + } + + async installOpenChamberManaged(parsed, controlPath, version, preferred) { + const bunPath = await this.resolveRemoteTool(parsed, controlPath, 'bun', [REMOTE_BUN_CANDIDATE]); + const npmPath = await this.resolveRemoteTool(parsed, controlPath, 'npm'); + + // bun's global install already targets ~/.bun; npm is pinned to a prefix in + // the user's home so it never touches the root-owned global directory. + const bunCommand = bunPath ? `${shellQuote(bunPath)} add -g @openchamber/web@${version}` : null; + const npmCommand = npmPath + ? `mkdir -p "${REMOTE_USER_PREFIX}" && ${shellQuote(npmPath)} install -g --prefix "${REMOTE_USER_PREFIX}" @openchamber/web@${version}` + : null; + + const commands = []; + if (preferred === 'npm') { + if (npmCommand) commands.push(npmCommand); + if (bunCommand) commands.push(bunCommand); } else { - if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`); - if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`); + if (bunCommand) commands.push(bunCommand); + if (npmCommand) commands.push(npmCommand); } if (commands.length === 0) { @@ -1072,24 +1139,35 @@ export class ElectronSshManager { } } - async startRemoteServerManaged(parsed, controlPath, instance, desiredPort) { - let envPrefix = 'OPENCHAMBER_RUNTIME=ssh-remote'; + async startRemoteServerManaged(parsed, controlPath, instance, desiredPort, binPath) { + const opencodePath = await this.resolveRemoteTool(parsed, controlPath, 'opencode', REMOTE_OPENCODE_CANDIDATES); + if (!opencodePath) { + throw new Error('The opencode CLI is not installed on the remote machine. Install it there, then connect again'); + } + const secret = this.configuredOpenChamberPassword(instance); + const remoteBindHost = instance.remoteOpenchamber?.bindHost === '0.0.0.0' ? '0.0.0.0' : '127.0.0.1'; + // Binding the remote server to every interface publishes its UI to the + // remote machine's whole network, so it may not run without a password. + if (remoteBindHost === '0.0.0.0' && !secret) { + throw new Error('Exposing the remote server to its network requires a UI password'); + } + + let envPrefix = `PATH="${REMOTE_PATH_PREFIX}:$PATH" OPENCODE_BINARY=${shellQuote(opencodePath)} OPENCHAMBER_RUNTIME=ssh-remote`; if (secret) { envPrefix += ` OPENCHAMBER_UI_PASSWORD=${shellQuote(secret)}`; } - const output = await this.runRemoteCommand(parsed, controlPath, `${envPrefix} openchamber serve --hostname 127.0.0.1 --port ${desiredPort}`); + const output = await this.runRemoteCommand(parsed, controlPath, `${envPrefix} ${shellQuote(binPath)} serve --hostname ${remoteBindHost} --port ${desiredPort}`); const port = output.split(/\s+/).map((token) => Number.parseInt(token, 10)).find((value) => Number.isFinite(value)); return port || desiredPort; } - async stopRemoteServerBestEffort(parsed, controlPath, remotePort) { + // `openchamber stop` owns the daemon lifecycle. The HTTP shutdown route sits + // behind UI authentication, so it cannot stop a password-protected server. + async stopRemoteServerBestEffort(parsed, controlPath, remotePort, remoteBinPath) { + if (!remoteBinPath) return; try { - await this.runRemoteCommand( - parsed, - controlPath, - `if command -v curl >/dev/null 2>&1; then curl -fsS -X POST http://127.0.0.1:${remotePort}/api/system/shutdown >/dev/null 2>&1 || true; elif command -v wget >/dev/null 2>&1; then wget -qO- --method=POST http://127.0.0.1:${remotePort}/api/system/shutdown >/dev/null 2>&1 || true; fi`, - ); + await this.runRemoteCommand(parsed, controlPath, `${shellQuote(remoteBinPath)} stop --port ${remotePort}`); } catch { } } @@ -1143,17 +1221,27 @@ export class ElectronSshManager { const port = instance.remoteOpenchamber.preferredPort; this.setStatus(instance.id, 'server_detecting', 'Probing external OpenChamber server', null, null, port, false, 0, false); await this.probeRemoteSystemInfo(parsed, controlPath, port, this.configuredOpenChamberPassword(instance)); - return { remotePort: port, startedByUs: false }; + return { remotePort: port, startedByUs: false, remoteBinPath: null }; } this.setStatus(instance.id, 'remote_probe', 'Checking remote OpenChamber installation'); - const installedVersion = await this.currentRemoteOpenChamberVersion(parsed, controlPath); - if (!installedVersion) { - this.setStatus(instance.id, 'installing', 'Installing OpenChamber on remote host'); - await this.installOpenChamberManaged(parsed, controlPath, this.appVersion, instance.remoteOpenchamber.installMethod); - } else if (installedVersion !== this.appVersion) { - this.setStatus(instance.id, 'updating', `Updating remote OpenChamber from ${installedVersion} to ${this.appVersion}`); + const installed = await this.remoteOpenChamberCandidates(parsed, controlPath); + let binary = installed.find((candidate) => candidate.version === this.appVersion) || null; + + if (!binary) { + const existing = installed[0] || null; + if (existing) { + this.setStatus(instance.id, 'updating', `Updating remote OpenChamber from ${existing.version || 'unknown'} to ${this.appVersion}`); + } else { + this.setStatus(instance.id, 'installing', 'Installing OpenChamber on remote host'); + } await this.installOpenChamberManaged(parsed, controlPath, this.appVersion, instance.remoteOpenchamber.installMethod); + + const afterInstall = await this.remoteOpenChamberCandidates(parsed, controlPath); + binary = afterInstall.find((candidate) => candidate.version === this.appVersion) || afterInstall[0] || existing; + if (!binary) { + throw new Error('OpenChamber was installed on the remote host but no openchamber binary could be found'); + } } this.setStatus(instance.id, 'server_detecting', 'Detecting managed OpenChamber server'); @@ -1165,13 +1253,13 @@ export class ElectronSshManager { if (!remotePort) { this.setStatus(instance.id, 'server_starting', 'Starting managed OpenChamber server'); const desiredPort = instance.remoteOpenchamber.preferredPort || randomPortCandidate(instance.id); - remotePort = await this.startRemoteServerManaged(parsed, controlPath, instance, desiredPort); + remotePort = await this.startRemoteServerManaged(parsed, controlPath, instance, desiredPort, binary.binPath); startedByUs = true; } if (!(await this.remoteServerRunning(parsed, controlPath, remotePort, this.configuredOpenChamberPassword(instance)))) { throw new Error('Managed OpenChamber server failed to become reachable'); } - return { remotePort, startedByUs }; + return { remotePort, startedByUs, remoteBinPath: binary.binPath }; } async disconnectInternal(id, reportIdle) { @@ -1186,7 +1274,7 @@ export class ElectronSshManager { if (session) { if (session.startedByUs && session.remotePort && session.instance.remoteOpenchamber.mode === 'managed' && !session.instance.remoteOpenchamber.keepRunning) { - await this.stopRemoteServerBestEffort(session.parsed, session.controlPath, session.remotePort); + await this.stopRemoteServerBestEffort(session.parsed, session.controlPath, session.remotePort, session.remoteBinPath); } await this.stopControlMasterBestEffort(session.parsed, session.controlPath); const auth = this.sshAuth.get(session.parsed); @@ -1262,9 +1350,10 @@ export class ElectronSshManager { throw new Error(`Unsupported remote OS: ${remoteOs}`); } - const { remotePort, startedByUs } = await this.ensureRemoteServer(instance, parsed, controlPath); + const { remotePort, startedByUs, remoteBinPath } = await this.ensureRemoteServer(instance, parsed, controlPath); session.remotePort = remotePort; session.startedByUs = startedByUs; + session.remoteBinPath = remoteBinPath; this.setStatus(id, 'forwarding', 'Setting up port forwards', null, null, remotePort, startedByUs, 0, false); const bindHost = sanitizeBindHost(instance.localForward?.bindHost); diff --git a/packages/electron/ssh-manager.test.mjs b/packages/electron/ssh-manager.test.mjs index eb5d1af7..fdbf84ea 100644 --- a/packages/electron/ssh-manager.test.mjs +++ b/packages/electron/ssh-manager.test.mjs @@ -289,4 +289,161 @@ describe('ElectronSshManager', () => { }); expect(settings.desktopHosts).toEqual([{ id: 'ssh-1', label: 'SSH Host', url: localUrl, apiUrl: localUrl, clientToken: 'ssh-client-token' }]); }); + test('installs OpenChamber into a home-owned npm prefix instead of the root-owned global one', async () => { + const commands = []; + const manager = new ElectronSshManager({ + settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'), + appVersion: '1.2.3', + emit: () => undefined, + }); + manager.resolveRemoteTool = async (_parsed, _controlPath, name) => (name === 'npm' ? '/usr/bin/npm' : null); + manager.runRemoteCommand = async (_parsed, _controlPath, script) => { + commands.push(script); + return ''; + }; + + await manager.installOpenChamberManaged({ destination: 'user@example.test', args: [] }, '/tmp/control.sock', '1.2.3', 'auto'); + + expect(commands).toHaveLength(1); + expect(commands[0]).toContain('--prefix "$HOME/.openchamber/npm-global"'); + expect(commands[0]).not.toMatch(/npm install -g @openchamber/); + }); + + test('lists every remote OpenChamber binary with its reported version', async () => { + const manager = new ElectronSshManager({ + settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'), + appVersion: '1.2.3', + emit: () => undefined, + }); + manager.runRemoteCommand = async () => [ + '/home/pi/.openchamber/npm-global/bin/openchamber\t1.2.3', + '/usr/bin/openchamber\t0.9.0', + '', + ].join('\n'); + + const candidates = await manager.remoteOpenChamberCandidates({ destination: 'user@example.test', args: [] }, '/tmp/control.sock'); + + expect(candidates).toEqual([ + { binPath: '/home/pi/.openchamber/npm-global/bin/openchamber', version: '1.2.3' }, + { binPath: '/usr/bin/openchamber', version: '0.9.0' }, + ]); + }); + + test('starts the resolved OpenChamber binary rather than whatever PATH exposes', async () => { + let started = ''; + const manager = new ElectronSshManager({ + settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'), + appVersion: '1.2.3', + emit: () => undefined, + }); + manager.resolveRemoteTool = async () => '/home/pi/.opencode/bin/opencode'; + manager.runRemoteCommand = async (_parsed, _controlPath, script) => { + started = script; + return '4321\n'; + }; + + const instance = { id: 'ssh-1', auth: {}, remoteOpenchamber: { mode: 'managed' } }; + const port = await manager.startRemoteServerManaged( + { destination: 'user@example.test', args: [] }, + '/tmp/control.sock', + instance, + 4321, + '/home/pi/.openchamber/npm-global/bin/openchamber', + ); + + expect(port).toBe(4321); + expect(started).toContain("'/home/pi/.openchamber/npm-global/bin/openchamber' serve"); + expect(started).toContain("OPENCODE_BINARY='/home/pi/.opencode/bin/opencode'"); + expect(started).toContain('$HOME/.opencode/bin:'); + }); + + test('refuses to start when the remote machine has no opencode CLI', async () => { + const manager = new ElectronSshManager({ + settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'), + appVersion: '1.2.3', + emit: () => undefined, + }); + manager.resolveRemoteTool = async () => null; + manager.runRemoteCommand = async () => { + throw new Error('should not start the server without a CLI'); + }; + + await expect(manager.startRemoteServerManaged( + { destination: 'user@example.test', args: [] }, + '/tmp/control.sock', + { id: 'ssh-1', auth: {}, remoteOpenchamber: { mode: 'managed' } }, + 4321, + '/home/pi/.bun/bin/openchamber', + )).rejects.toThrow(/opencode CLI is not installed/); + }); + test('prefers a bun that only exists in the home directory over npm', async () => { + const commands = []; + const manager = new ElectronSshManager({ + settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'), + appVersion: '1.2.3', + emit: () => undefined, + }); + // A login shell over SSH does not put ~/.bun/bin on PATH. + manager.resolveRemoteTool = async (_parsed, _controlPath, name) => + (name === 'bun' ? '/home/pi/.bun/bin/bun' : '/usr/bin/npm'); + manager.runRemoteCommand = async (_parsed, _controlPath, script) => { + commands.push(script); + return ''; + }; + + await manager.installOpenChamberManaged({ destination: 'user@example.test', args: [] }, '/tmp/control.sock', '1.2.3', 'auto'); + + expect(commands).toEqual(["'/home/pi/.bun/bin/bun' add -g @openchamber/web@1.2.3"]); + }); + test('stops a remote server it started through the CLI, not the authenticated HTTP route', async () => { + const scripts = []; + const manager = new ElectronSshManager({ + settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'), + appVersion: '1.2.3', + emit: () => undefined, + }); + manager.runRemoteCommand = async (_parsed, _controlPath, script) => { + scripts.push(script); + return ''; + }; + + await manager.stopRemoteServerBestEffort( + { destination: 'user@example.test', args: [] }, + '/tmp/control.sock', + 41777, + '/home/pi/.bun/bin/openchamber', + ); + + expect(scripts).toEqual(["'/home/pi/.bun/bin/openchamber' stop --port 41777"]); + }); + test('publishes the remote server to its network only with a UI password', async () => { + const manager = new ElectronSshManager({ + settingsFilePath: path.join(os.tmpdir(), 'unused-settings.json'), + appVersion: '1.2.3', + emit: () => undefined, + }); + manager.resolveRemoteTool = async () => '/home/pi/.opencode/bin/opencode'; + let started = ''; + manager.runRemoteCommand = async (_parsed, _controlPath, script) => { + started = script; + return '4321\n'; + }; + + const parsed = { destination: 'user@example.test', args: [] }; + const exposed = { + id: 'ssh-1', + auth: {}, + remoteOpenchamber: { mode: 'managed', bindHost: '0.0.0.0' }, + }; + + await expect(manager.startRemoteServerManaged(parsed, '/tmp/control.sock', exposed, 4321, '/bin/openchamber')) + .rejects.toThrow(/requires a UI password/); + + const secured = { + ...exposed, + auth: { openchamberPassword: { enabled: true, value: 'remote-secret', store: 'settings' } }, + }; + await manager.startRemoteServerManaged(parsed, '/tmp/control.sock', secured, 4321, '/bin/openchamber'); + expect(started).toContain('--hostname 0.0.0.0'); + }); }); diff --git a/packages/electron/windows-file-replace.mjs b/packages/electron/windows-file-replace.mjs new file mode 100644 index 00000000..b43ad904 --- /dev/null +++ b/packages/electron/windows-file-replace.mjs @@ -0,0 +1,28 @@ +import fsp from 'node:fs/promises'; + +const WINDOWS_RETRY_DELAYS_MS = [50, 100, 200, 400, 800, 1_000, 1_000]; + +const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)); + +const isTransientWindowsFileError = (error, platform) => { + if (platform !== 'win32') return false; + const code = error?.code; + return code === 'EPERM' || code === 'EACCES' || code === 'EBUSY'; +}; + +export const replaceFileWithRetry = async (source, target, options = {}) => { + const platform = options.platform ?? process.platform; + const rename = options.rename ?? fsp.rename; + const wait = options.wait ?? sleep; + + for (let attempt = 0; ; attempt += 1) { + try { + await rename(source, target); + return; + } catch (error) { + const delay = WINDOWS_RETRY_DELAYS_MS[attempt]; + if (delay === undefined || !isTransientWindowsFileError(error, platform)) throw error; + await wait(delay); + } + } +}; diff --git a/packages/electron/windows-file-replace.test.mjs b/packages/electron/windows-file-replace.test.mjs new file mode 100644 index 00000000..a860037d --- /dev/null +++ b/packages/electron/windows-file-replace.test.mjs @@ -0,0 +1,81 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { replaceFileWithRetry } from './windows-file-replace.mjs'; + +const fileError = (code = 'EPERM') => Object.assign(new Error(code), { code }); + +test('retries transient Windows rename failures until replacement succeeds', async () => { + const delays = []; + let attempts = 0; + + await replaceFileWithRetry('settings.tmp', 'settings.json', { + platform: 'win32', + rename: async () => { + attempts += 1; + if (attempts < 4) throw fileError(); + }, + wait: async (delay) => delays.push(delay), + }); + + assert.equal(attempts, 4); + assert.deepEqual(delays, [50, 100, 200]); +}); + +test('does not retry rename errors that are not transient Windows locks', async () => { + let attempts = 0; + const error = fileError('ENOENT'); + + await assert.rejects( + replaceFileWithRetry('settings.tmp', 'settings.json', { + platform: 'win32', + rename: async () => { + attempts += 1; + throw error; + }, + wait: async () => assert.fail('unexpected wait'), + }), + error, + ); + + assert.equal(attempts, 1); +}); + +test('does not retry transient error codes outside Windows', async () => { + let attempts = 0; + const error = fileError(); + + await assert.rejects( + replaceFileWithRetry('settings.tmp', 'settings.json', { + platform: 'linux', + rename: async () => { + attempts += 1; + throw error; + }, + wait: async () => assert.fail('unexpected wait'), + }), + error, + ); + + assert.equal(attempts, 1); +}); + +test('returns the final Windows lock error after the retry window', async () => { + const delays = []; + let attempts = 0; + + await assert.rejects( + replaceFileWithRetry('settings.tmp', 'settings.json', { + platform: 'win32', + rename: async () => { + attempts += 1; + throw fileError(); + }, + wait: async (delay) => delays.push(delay), + }), + { code: 'EPERM' }, + ); + + assert.equal(attempts, 8); + assert.deepEqual(delays, [50, 100, 200, 400, 800, 1_000, 1_000]); +}); diff --git a/packages/ui/package.json b/packages/ui/package.json index 9dc25467..c9648636 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@openchamber/ui", - "version": "1.18.4", + "version": "1.20.0", "private": true, "type": "module", "main": "src/main.tsx", @@ -19,34 +19,35 @@ "@capacitor/keyboard": "^8.0.0", "@capacitor/push-notifications": "^8.1.1", "@capacitor/status-bar": "^8.0.0", - "@codemirror/autocomplete": "^6.20.0", - "@codemirror/commands": "^6.10.1", + "@codemirror/autocomplete": "^6.20.3", + "@codemirror/commands": "^6.11.0", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", - "@codemirror/lang-html": "^6.4.11", - "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-html": "^6.4.12", + "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-json": "^6.0.2", - "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/lang-markdown": "^6.5.2", "@codemirror/lang-python": "^6.2.1", "@codemirror/lang-rust": "^6.0.2", "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-xml": "^6.1.0", - "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "6.12.2", + "@codemirror/lang-yaml": "^6.1.3", + "@codemirror/language": "6.12.4", "@codemirror/language-data": "^6.5.2", - "@codemirror/legacy-modes": "^6.5.2", - "@codemirror/lint": "^6.9.2", - "@codemirror/search": "^6.6.0", - "@codemirror/state": "^6.5.4", - "@codemirror/view": "6.39.13", + "@codemirror/legacy-modes": "^6.5.3", + "@codemirror/lint": "^6.9.7", + "@codemirror/search": "^6.7.1", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "6.43.9", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@legendapp/list": "3.3.8", "@lezer/highlight": "^1.2.3", - "@opencode-ai/sdk": "1.18.18", + "@opencode-ai/sdk": "1.18.21", "@pierre/diffs": "1.3.0-beta.6", - "@replit/codemirror-vim": "^6.3.0", + "@replit/codemirror-vim": "^6.4.0", "@simplewebauthn/browser": "13.3.0", "@tanstack/react-virtual": "3.14.5", "@xenova/transformers": "^2.17.2", @@ -103,6 +104,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.5.0", "globals": "^16.3.0", + "happy-dom": "^18.0.1", "nodemon": "^3.1.7", "tailwindcss": "^4.0.0", "tsx": "^4.20.6", diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index ad4bc2f4..f213c11f 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { MainLayout } from '@/components/layout/MainLayout'; import { ChatView } from '@/components/views/ChatView'; +import { AppLinkConfirmDialog } from '@/components/chat/AppLinkConfirmDialog'; import { FireworksProvider } from '@/contexts/FireworksContext'; import { Toaster } from '@/components/ui/sonner'; import { Button } from '@/components/ui/button'; @@ -11,9 +12,11 @@ import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; import { useMenuActions } from '@/hooks/useMenuActions'; import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap'; import { useTraySync } from '@/hooks/useTraySync'; +import { useGlobalSessionsPolling } from '@/hooks/useGlobalSessionsPolling'; import { useRouter } from '@/hooks/useRouter'; import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon'; import { useWebNotificationStream } from '@/hooks/useWebNotificationStream'; +import { useAgentMemorySync } from '@/hooks/useAgentMemorySync'; import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt'; import { useWindowTitle } from '@/hooks/useWindowTitle'; import { useConfigStore } from '@/stores/useConfigStore'; @@ -32,7 +35,6 @@ import type { RecoveryVariant } from '@/components/onboarding/DesktopConnectionR import { useSessionUIStore } from '@/sync/session-ui-store'; import { markSessionViewed } from '@/sync/notification-store'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; -import { useProjectsStore } from '@/stores/useProjectsStore'; import { opencodeClient } from '@/lib/opencode/client'; import { runtimeFetch } from '@/lib/runtime-fetch'; import { getRuntimeKey, subscribeRuntimeEndpointChanged } from '@/lib/runtime-switch'; @@ -624,7 +626,6 @@ function App({ apis }: AppProps) { const directory = typeof detail?.directory === 'string' && detail.directory.trim().length > 0 ? detail.directory.trim() : null; - useUIStore.getState().setActiveMainTab('chat'); void useSessionUIStore.getState().setCurrentSession(sessionId, directory); }; @@ -638,12 +639,9 @@ function App({ apis }: AppProps) { React.useEffect(() => { if (typeof window === 'undefined') return; const onOpenMiniChat = () => { - const currentDir = useDirectoryStore.getState().currentDirectory; - const { activeProjectId, projects } = useProjectsStore.getState(); - const activeProject = projects.find((p) => p.id === activeProjectId) ?? null; void invokeDesktop('desktop_open_draft_mini_chat_window', { - directory: currentDir || activeProject?.path || '', - projectId: activeProject?.id ?? null, + directory: '', + projectId: null, }); }; window.addEventListener('openchamber:open-mini-chat', onOpenMiniChat); @@ -675,11 +673,12 @@ function App({ apis }: AppProps) { const projectId = typeof detail?.projectId === 'string' && detail.projectId.trim().length > 0 ? detail.projectId.trim() : null; - useUIStore.getState().setActiveMainTab('chat'); + const hasProjectTarget = Boolean(directory || projectId); useUIStore.getState().setSessionSwitcherOpen(false); useSessionUIStore.getState().openNewSessionDraft({ - selectedProjectId: projectId, - directoryOverride: directory, + target: hasProjectTarget ? 'project' : 'chat', + selectedProjectId: hasProjectTarget ? projectId : null, + directoryOverride: hasProjectTarget ? directory : null, preserveDirectoryOverride: Boolean(directory), }); }; @@ -703,6 +702,10 @@ function App({ apis }: AppProps) { usePushVisibilityBeacon({ enabled: embeddedBackgroundWorkEnabled }); useWebNotificationStream({ enabled: embeddedBackgroundWorkEnabled }); + // Loaded here rather than by the Memory tab: the session index is built from + // this snapshot, so leaving it to the panel meant a user who never opened + // Project notes sent every message with no memory index at all. + useAgentMemorySync(currentDirectory || null); usePwaInstallPrompt(); useWindowTitle(); @@ -716,6 +719,7 @@ function App({ apis }: AppProps) { useMenuActions(handleToggleMemoryDebug); useTraySync(); + useGlobalSessionsPolling(!embeddedSessionChat); useSessionStatusBootstrap({ enabled: embeddedBackgroundWorkEnabled }); @@ -903,6 +907,7 @@ function App({ apis }: AppProps) { isVSCodeRuntime={isVSCodeRuntime} embeddedBackgroundWorkEnabled={embeddedBackgroundWorkEnabled} /> + @@ -946,6 +951,7 @@ function App({ apis }: AppProps) { + {!isBootShell && ( <> diff --git a/packages/ui/src/apps/ElectronMiniChatApp.tsx b/packages/ui/src/apps/ElectronMiniChatApp.tsx index 10a993d8..7aed5ba0 100644 --- a/packages/ui/src/apps/ElectronMiniChatApp.tsx +++ b/packages/ui/src/apps/ElectronMiniChatApp.tsx @@ -5,6 +5,7 @@ import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { TooltipProvider } from '@/components/ui/tooltip'; import { Toaster } from '@/components/ui/sonner'; import { MiniChatLayout } from '@/components/mini-chat/MiniChatLayout'; +import { AppLinkConfirmDialog } from '@/components/chat/AppLinkConfirmDialog'; import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon'; import { useWindowTitle } from '@/hooks/useWindowTitle'; import { opencodeClient } from '@/lib/opencode/client'; @@ -25,6 +26,7 @@ import { worktreeMapsEqual, } from '@/lib/worktrees/worktreeManager'; import type { WorktreeMetadata } from '@/types/worktree'; +import { CHAT_DRAFT_PROJECT_ID } from '@/lib/chatDirectories'; const MINI_CHAT_PRESENCE_CHANNEL = 'openchamber:mini-chat-presence'; @@ -153,9 +155,9 @@ const MiniChatBootstrap: React.FC<{ config: MiniChatConfig }> = ({ config }) => const sessionId = typeof detail?.sessionId === 'string' ? detail.sessionId.trim() : ''; if (!sessionId) return; if (useSessionUIStore.getState().currentSessionId === sessionId) return; - const directory = typeof detail?.directory === 'string' && detail.directory.trim().length > 0 - ? detail.directory.trim() - : (sessions.find((entry) => entry.id === sessionId) as { directory?: string | null } | undefined)?.directory ?? null; + const sessionDirectory = (sessions.find((entry) => entry.id === sessionId) as { directory?: string | null } | undefined)?.directory?.trim(); + const directory = sessionDirectory + || (typeof detail?.directory === 'string' && detail.directory.trim().length > 0 ? detail.directory.trim() : null); void sync.ensureSessionRenderable(sessionId); setCurrentSession(sessionId, directory); sessionBootstrappedRef.current = true; @@ -166,9 +168,11 @@ const MiniChatBootstrap: React.FC<{ config: MiniChatConfig }> = ({ config }) => React.useEffect(() => { if (config.mode !== 'draft' || draftOpen || currentSessionId) return; + const hasProjectTarget = Boolean(config.projectId || config.directory); openNewSessionDraft({ - selectedProjectId: config.projectId, - directoryOverride: config.directory, + target: hasProjectTarget ? 'project' : 'chat', + selectedProjectId: hasProjectTarget ? config.projectId : CHAT_DRAFT_PROJECT_ID, + directoryOverride: hasProjectTarget ? config.directory : null, preserveDirectoryOverride: Boolean(config.directory), }); }, [config, currentSessionId, draftOpen, openNewSessionDraft]); @@ -278,10 +282,11 @@ const MiniChatPresencePublisher: React.FC = () => { const useSessionUnavailable = (config: MiniChatConfig): boolean => { const sessions = useSessions(); const currentSessionId = useSessionUIStore((state) => state.currentSessionId); + const draftOpen = useSessionUIStore((state) => state.newSessionDraft.open); const [timedOut, setTimedOut] = React.useState(false); React.useEffect(() => { - if (config.mode !== 'session' || !config.sessionId || currentSessionId === config.sessionId) { + if (draftOpen || config.mode !== 'session' || !config.sessionId || currentSessionId) { setTimedOut(false); return; } @@ -291,7 +296,7 @@ const useSessionUnavailable = (config: MiniChatConfig): boolean => { } const timeout = window.setTimeout(() => setTimedOut(true), 5000); return () => window.clearTimeout(timeout); - }, [config.mode, config.sessionId, currentSessionId, sessions]); + }, [config.mode, config.sessionId, currentSessionId, draftOpen, sessions]); return timedOut; }; @@ -321,6 +326,7 @@ export function ElectronMiniChatApp({ apis }: ElectronMiniChatAppProps) {
+
diff --git a/packages/ui/src/apps/MobileApp.tsx b/packages/ui/src/apps/MobileApp.tsx index 42d419c8..4d30dc00 100644 --- a/packages/ui/src/apps/MobileApp.tsx +++ b/packages/ui/src/apps/MobileApp.tsx @@ -9,6 +9,7 @@ import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo'; import { ChatView } from '@/components/views/ChatView'; import { PlanView } from '@/components/views/PlanView'; import { SettingsView } from '@/components/views/SettingsView'; +import { AppLinkConfirmDialog } from '@/components/chat/AppLinkConfirmDialog'; import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider'; import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; @@ -109,7 +110,7 @@ const MobileShell: React.FC<{ onActiveConnectionDeleted: () => void }> = ({ onAc const [workspaceTab, setWorkspaceTab] = React.useState('changes'); // A plan opened from the workspace drawer's Notes tab, shown as a fullscreen // layer on top of it (back returns to the notes). - const [openPlan, setOpenPlan] = React.useState<{ path: string; title: string } | null>(null); + const [openPlan, setOpenPlan] = React.useState<{ id: string; title: string } | null>(null); const [settingsInitialMobileStage, setSettingsInitialMobileStage] = React.useState<'nav' | 'page-content'>('nav'); // When set, the Changes surface opens directly into the per-file diff for this path. const [pendingChangesDiff, setPendingChangesDiff] = React.useState<{ path: string; staged: boolean } | null>(null); @@ -540,7 +541,7 @@ const MobileShell: React.FC<{ onActiveConnectionDeleted: () => void }> = ({ onAc > { closeSurface(); closeWorkspace(); @@ -1258,6 +1259,7 @@ export function MobileApp({ apis }: MobileAppProps) { switchRuntimeEndpoint({ apiBaseUrl: '', clientToken: null, runtimeKey: 'mobile-disconnected' }); setConnectionEpoch((value) => value + 1); }} /> + {isInitialized ? : null} diff --git a/packages/ui/src/apps/MobileSessionMetadata.tsx b/packages/ui/src/apps/MobileSessionMetadata.tsx index 9ffe0d66..3d239d1f 100644 --- a/packages/ui/src/apps/MobileSessionMetadata.tsx +++ b/packages/ui/src/apps/MobileSessionMetadata.tsx @@ -388,6 +388,7 @@ export const MobileSessionMetadataButton = React.memo(function MobileSessionMeta for (let i = activeSessionMessages.length - 1; i >= 0; i -= 1) { const message = activeSessionMessages[i] as typeof activeSessionMessages[number] & { tokens?: { + total?: unknown; input?: unknown; output?: unknown; reasoning?: unknown; @@ -395,6 +396,11 @@ export const MobileSessionMetadataButton = React.memo(function MobileSessionMeta }; }; if (message.role !== 'assistant' || !message.tokens) continue; + // Multi-step turns accumulate the fields across API round-trips, so + // summing them overstates the window. The server-reported total is the + // final round-trip's window; sum only when the server did not send it. + const reportedTotal = getTokenCount(message.tokens.total); + if (reportedTotal > 0) return reportedTotal; const total = getTokenCount(message.tokens.input) + getTokenCount(message.tokens.output) + getTokenCount(message.tokens.reasoning) diff --git a/packages/ui/src/apps/MobileSessionSwitcher.tsx b/packages/ui/src/apps/MobileSessionSwitcher.tsx index c05b9ecf..fdaf436f 100644 --- a/packages/ui/src/apps/MobileSessionSwitcher.tsx +++ b/packages/ui/src/apps/MobileSessionSwitcher.tsx @@ -3,7 +3,7 @@ import type { Session } from '@opencode-ai/sdk/v2'; import { SessionActivityDuration } from '@/components/session/SessionActivityDuration'; import { formatSessionCompactDateLabel } from '@/components/session/sidebar/utils'; -import { useSwitcherItems } from '@/components/session/sidebar/hooks/useSwitcherItems'; +import { useSwitcherItems } from '@/components/session/sidebar/shell/useSwitcherItems'; import { useTabletLayout } from '@/lib/device'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; diff --git a/packages/ui/src/apps/MobileSessionsSheet.tsx b/packages/ui/src/apps/MobileSessionsSheet.tsx index 43a28f32..a64d94f0 100644 --- a/packages/ui/src/apps/MobileSessionsSheet.tsx +++ b/packages/ui/src/apps/MobileSessionsSheet.tsx @@ -43,6 +43,7 @@ import { useThemeSystem } from '@/contexts/useThemeSystem'; import { getProjectLabel, normalizePath } from './mobilePaths'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useI18n } from '@/lib/i18n'; +import { matchesRankQuery, rankByQuery } from '@/lib/search/fuzzySearch'; import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, ProjectIconImage } from '@/lib/projectMeta'; import { cn } from '@/lib/utils'; import { @@ -188,11 +189,8 @@ const findExactProjectMatch = (projects: ProjectMeta[], directory: string): Proj return projects.find((project) => projectMatchesExactDirectory(project, normalizedDirectory)) ?? null; }; -const sessionMatchesQuery = (session: Session, projectLabel: string, query: string): boolean => { - if (!query) return true; - const haystack = `${session.title ?? ''} ${session.id} ${getSessionDirectory(session)} ${projectLabel}`.toLowerCase(); - return haystack.includes(query); -}; +const sessionMatchesQuery = (session: Session, projectLabel: string, query: string): boolean => + matchesRankQuery([session.title, session.id, getSessionDirectory(session), projectLabel], query); const MobileProjectIcon: React.FC<{ project: Pick; @@ -1355,7 +1353,7 @@ export const MobileSessionsSheet: React.FC = ({ open, const filteredNodes = React.useMemo(() => { if (!normalizedQuery) return projectNodes; return projectNodes.filter((node) => { - if (`${node.project.label} ${node.project.path}`.toLowerCase().includes(normalizedQuery)) return true; + if (matchesRankQuery([node.project.label, node.project.path], normalizedQuery)) return true; return node.buckets.some((bucket) => bucket.sessions.some((session) => sessionMatchesQuery(session, node.project.label, normalizedQuery)), ); @@ -1385,8 +1383,7 @@ export const MobileSessionsSheet: React.FC = ({ open, const searchProjectMatches = React.useMemo(() => { if (!normalizedQuery) return [] as Array; - return projectsMeta - .filter((project) => `${project.label} ${project.path}`.toLowerCase().includes(normalizedQuery)) + return rankByQuery(projectsMeta, normalizedQuery, (project) => [project.label, project.path]) .map((project) => ({ ...project, sessionCount: sessions.filter((session) => { diff --git a/packages/ui/src/apps/MobileWorkspaceDrawer.tsx b/packages/ui/src/apps/MobileWorkspaceDrawer.tsx index b3c94c24..f0847aa1 100644 --- a/packages/ui/src/apps/MobileWorkspaceDrawer.tsx +++ b/packages/ui/src/apps/MobileWorkspaceDrawer.tsx @@ -105,7 +105,7 @@ export const MobileWorkspaceDrawer: React.FC<{ /** When set, the Changes tab opens directly into the per-file diff. */ pendingChangesDiff: { path: string; staged: boolean } | null; /** Notes tab: opens a plan fullscreen (layered above the drawer). */ - onOpenPlan: (plan: { path: string; title: string }) => void; + onOpenPlan: (plan: { id: string; title: string }) => void; /** MCP tab: jump to the MCP settings page pre-seeded with a new server draft. */ onOpenMcpSettings: () => void; variant?: 'drawer' | 'panel'; diff --git a/packages/ui/src/apps/VSCodeApp.tsx b/packages/ui/src/apps/VSCodeApp.tsx index 47b8086c..737a0239 100644 --- a/packages/ui/src/apps/VSCodeApp.tsx +++ b/packages/ui/src/apps/VSCodeApp.tsx @@ -8,8 +8,10 @@ import { Toaster } from '@/components/ui/sonner'; import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay'; import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; import { OpenCodeUpdateToast } from '@/components/update/OpenCodeUpdateToast'; +import { AppLinkConfirmDialog } from '@/components/chat/AppLinkConfirmDialog'; import { VSCodeLayout } from '@/components/layout/VSCodeLayout'; import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon'; +import { useGlobalSessionsPolling } from '@/hooks/useGlobalSessionsPolling'; import { useRouter } from '@/hooks/useRouter'; import { useWindowTitle } from '@/hooks/useWindowTitle'; import { opencodeClient } from '@/lib/opencode/client'; @@ -56,6 +58,7 @@ export function VSCodeApp({ apis }: VSCodeAppProps) { usePushVisibilityBeacon({ enabled: true }); useWindowTitle(); useRouter(); + useGlobalSessionsPolling(panelType !== 'agentManager'); React.useEffect(() => { document.documentElement.classList.toggle('wide-chat-layout', wideChatLayoutEnabled); @@ -108,6 +111,7 @@ export function VSCodeApp({ apis }: VSCodeAppProps) {
+
@@ -127,6 +131,7 @@ export function VSCodeApp({ apis }: VSCodeAppProps) {
+ diff --git a/packages/ui/src/apps/mobileConnections.ts b/packages/ui/src/apps/mobileConnections.ts index 77c83c24..78b5851b 100644 --- a/packages/ui/src/apps/mobileConnections.ts +++ b/packages/ui/src/apps/mobileConnections.ts @@ -23,7 +23,7 @@ import type { PairingConnectionPayload, PairingEndpointCandidate } from '@/lib/c import { isCapacitorApp } from '@/lib/platform'; import { adoptRelayTunnel, isRelayModeActive } from '@/lib/relay/runtime-tunnel'; import { createRelayTunnelClient } from '@/lib/relay/tunnel-client'; -import { runtimeFetch } from '@/lib/runtime-fetch'; +import { addRuntimeProxyHeaders, runtimeFetch } from '@/lib/runtime-fetch'; import { getRuntimeApiBaseUrl, getRuntimeKey, switchRuntimeEndpoint } from '@/lib/runtime-switch'; import { recordMobileConnectDebug } from './mobileConnectionDebug'; @@ -346,11 +346,11 @@ const nativeHttpRequest = async (url: string, init?: RequestInit): Promise { export const resetAppForRuntimeEndpointChange = (detail: RuntimeEndpointChangedDetail): void => { useSessionUIStore.getState().prepareForRuntimeSwitch(detail.previousRuntimeKey); - useUIStore.getState().prepareForRuntimeSwitch(detail.previousRuntimeKey); if (detail.previousRuntimeKey) { useAutoReviewStore.getState().stopRunningRunsForRuntime(detail.previousRuntimeKey); } @@ -52,6 +51,9 @@ export const resetAppForRuntimeEndpointChange = (detail: RuntimeEndpointChangedD lastDisconnectReason: null, }); useProjectsStore.getState().resetForRuntimeSwitch(); + // Notes, todos, plans and the pinned-context bookkeeping are keyed by a + // path-derived project id, which two runtimes can collide on. + useProjectContextStore.getState().reset(); // Cross-project session list (mobile sessions sheet & co) belongs to the // previous instance — drop it so stale sessions can't linger after a switch. useGlobalSessionsStore.getState().resetForRuntimeSwitch(); @@ -67,7 +69,6 @@ export const resetAppForRuntimeEndpointChange = (detail: RuntimeEndpointChangedD useSessionFoldersStore.getState().resetForRuntimeSwitch(detail.runtimeKey); useFilesViewTabsStore.getState().resetForRuntimeSwitch(detail.runtimeKey); useSessionUIStore.getState().restoreForRuntimeSwitch(detail.runtimeKey); - useUIStore.getState().restoreForRuntimeSwitch(detail.runtimeKey); resetStreamingState(); queueMicrotask(() => void syncDesktopSettings()); }; diff --git a/packages/ui/src/assets/provider-logos/command-code.svg b/packages/ui/src/assets/provider-logos/command-code.svg new file mode 100644 index 00000000..786e90bc --- /dev/null +++ b/packages/ui/src/assets/provider-logos/command-code.svg @@ -0,0 +1,15 @@ + + Command Code + + + diff --git a/packages/ui/src/components/chat/AppLinkConfirmDialog.test.tsx b/packages/ui/src/components/chat/AppLinkConfirmDialog.test.tsx new file mode 100644 index 00000000..ceb2624e --- /dev/null +++ b/packages/ui/src/components/chat/AppLinkConfirmDialog.test.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { beforeEach, describe, expect, mock, test } from 'bun:test'; +import { renderToStaticMarkup } from 'react-dom/server'; + +import { I18nProvider } from '@/lib/i18n'; + +mock.module('@/components/ui/dialog', () => ({ + Dialog: ({ children }: React.PropsWithChildren) => <>{children}, + DialogContent: ({ children }: React.PropsWithChildren) =>
{children}
, + DialogDescription: ({ children }: React.PropsWithChildren) =>

{children}

, + DialogFooter: ({ children }: React.PropsWithChildren) =>
{children}
, + DialogHeader: ({ children }: React.PropsWithChildren) =>
{children}
, + DialogTitle: ({ children }: React.PropsWithChildren) =>

{children}

, +})); + +const { AppLinkConfirmDialog } = await import('./AppLinkConfirmDialog'); +const { + getAppLinkConfirmationSnapshot, + openAppLinkWithConfirmation, + settleAppLinkConfirmation, +} = await import('./appLinkConfirmation'); + +describe('AppLinkConfirmDialog', () => { + beforeEach(() => { + if (getAppLinkConfirmationSnapshot()) { + settleAppLinkConfirmation('cancel'); + } + }); + + test('keeps cancel visible and focused beside both open choices', () => { + void openAppLinkWithConfirmation('obsidian://open?vault=Notebook'); + + const markup = renderToStaticMarkup( + + + , + ); + + expect(markup).toContain('>Cancel'); + expect(markup).toContain('autofocus=""'); + expect(markup).toContain('>Open once'); + expect(markup).toContain('>Trust and open'); + + settleAppLinkConfirmation('cancel'); + }); +}); diff --git a/packages/ui/src/components/chat/AppLinkConfirmDialog.tsx b/packages/ui/src/components/chat/AppLinkConfirmDialog.tsx new file mode 100644 index 00000000..73960a9e --- /dev/null +++ b/packages/ui/src/components/chat/AppLinkConfirmDialog.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; + +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { useI18n } from '@/lib/i18n'; +import { getUrlScheme } from '@/lib/url'; + +import { + getAppLinkConfirmationSnapshot, + settleAppLinkConfirmation, + subscribeAppLinkConfirmation, + type AppLinkConfirmationChoice, +} from './appLinkConfirmation'; + +/** + * App-level dialog confirming application deep links (obsidian://, vscode://, + * ...) rendered in chat markdown before the OS is asked to open them. + * Dismissing via the close button, Escape, or the backdrop cancels the open. + */ +export const AppLinkConfirmDialog = () => { + const { t } = useI18n(); + const request = React.useSyncExternalStore( + subscribeAppLinkConfirmation, + getAppLinkConfirmationSnapshot, + getAppLinkConfirmationSnapshot, + ); + + const url = request?.url ?? ''; + const scheme = getUrlScheme(url) ?? ''; + + const settle = React.useCallback((choice: AppLinkConfirmationChoice) => { + settleAppLinkConfirmation(choice); + }, []); + + return ( + { + if (!open) { + settle('cancel'); + } + }} + > + + + {t('chat.appLink.confirm.title')} + + {scheme + ? t('chat.appLink.confirm.description', { scheme: `${scheme}://` }) + : t('chat.appLink.confirm.descriptionPlain')} + + +
+ {url} +
+ + + + + +
+
+ ); +}; diff --git a/packages/ui/src/components/chat/ChatContainer.tsx b/packages/ui/src/components/chat/ChatContainer.tsx index a50ea6e7..0873eee2 100644 --- a/packages/ui/src/components/chat/ChatContainer.tsx +++ b/packages/ui/src/components/chat/ChatContainer.tsx @@ -18,8 +18,8 @@ import { StatusRowContainer } from './StatusRowContainer'; import { SessionRecapNote } from '@/components/chat/SessionRecapSpacer'; import ScrollToBottomButton from './components/ScrollToBottomButton'; import { PromptNavigatorRail } from './components/PromptNavigatorRail'; -import { ScrollShadow } from '@/components/ui/ScrollShadow'; -import { useChatAutoFollow, type AnimationHandlers, type ContentChangeReason } from '@/hooks/useChatAutoFollow'; +import { useScrollShadow } from '@/components/ui/useScrollShadow'; +import { useChatTimelineScroll, type TimelineListHandle } from '@/hooks/useChatTimelineScroll'; import { useChatTimelineController } from './hooks/useChatTimelineController'; import { TimelineDialog } from './TimelineDialog'; import { useChatTurnNavigation } from './hooks/useChatTurnNavigation'; @@ -60,11 +60,14 @@ import { findShellCommandForMessage, isUserShellMarkerMessage } from './lib/shel import { resolveChatPromptReadOnly } from './chatPromptReadOnly'; import { getRuntimeKey } from '@/lib/runtime-switch'; import { createFirstVisibleSessionPerformanceTracker } from '@/sync/session-load-performance'; +import { isChatDirectoryPath } from '@/lib/chatDirectories'; const EMPTY_MESSAGES: Array<{ info: Message; parts: Part[] }> = []; const IDLE_SESSION_STATUS = { type: 'idle' as const }; const CHAT_FORCE_SCROLL_BOTTOM_EVENT = 'openchamber:chat-force-scroll-bottom'; const DEFAULT_RETRY_MESSAGE = 'Quota limit reached. Retrying automatically.'; +const DRAFT_EXIT_DURATION_MS = 120; +const COMPOSER_MOVE_DURATION_MS = 180; const CHAT_SCROLL_STYLE = { overflowAnchor: 'none', overscrollBehavior: 'contain', @@ -148,11 +151,15 @@ type ChatViewportProps = { currentSessionKey: string; isDesktopExpandedInput: boolean; isMobile: boolean; - stickyUserHeader: boolean; directory?: string; scrollRef: React.RefObject; messageListRef: React.RefObject; - pendingRevealWork: boolean; + registerList: (list: TimelineListHandle | null) => void; + anchorMessageId: string | null; + onAnchorReady: (messageId: string, anchorIndex: number) => void; + onAnchorSizeChanged: (messageId: string) => void; + onIsAtEndChange: (isAtEnd: boolean) => void; + onTimelineDataChange: () => void; renderedMessages: SessionMessageRecord[]; isLoadingOlder: boolean; sessionIsWorking: boolean; @@ -164,10 +171,11 @@ type ChatViewportProps = { confirmedAt?: number; fallbackTimestamp?: number; } | null; - handleMessageContentChange: (reason?: ContentChangeReason) => void; - getAnimationHandlers: (messageId: string) => AnimationHandlers; - handleHistoryScroll: () => void; scrollToBottom: () => void; + endPinningReleased: boolean; + // One-shot fade for content that replaced the hydration skeleton; + // cached sessions render instantly without it. + revealContent: boolean; sessionQuestions: QuestionRequest[]; sessionPermissions: PermissionRequest[]; isProgrammaticFollowActive: boolean; @@ -187,21 +195,24 @@ const ChatViewport = React.memo(({ currentSessionKey, isDesktopExpandedInput, isMobile, - stickyUserHeader, directory, scrollRef, messageListRef, - pendingRevealWork, + registerList, + anchorMessageId, + onAnchorReady, + onAnchorSizeChanged, + onIsAtEndChange, + onTimelineDataChange, renderedMessages, isLoadingOlder, sessionIsWorking, streamingMessageId, activeStreamingPhase, retryOverlay, - handleMessageContentChange, - getAnimationHandlers, - handleHistoryScroll, scrollToBottom, + endPinningReleased, + revealContent, sessionQuestions, sessionPermissions, isProgrammaticFollowActive, @@ -312,83 +323,96 @@ const ChatViewport = React.memo(({ scrollRef.current?.focus({ preventScroll: true }); }, [scrollRef]); + // Everything that used to sit beside the list inside the scroll container + // now renders as the list's header/footer, so it keeps scrolling with the + // rows exactly as before. + const listHeader = React.useMemo(() => ( + showLoadOlderButton ? ( +
+ +
+ ) : null + ), [isLoadingOlder, onLoadOlder, showLoadOlderButton, t]); + + const listFooter = React.useMemo(() => ( + <> + {(sessionQuestions.length > 0 || sessionPermissions.length > 0) && ( +
+ {sessionQuestions.map((question) => ( + + ))} + {sessionPermissions.map((permission) => ( + + ))} +
+ )} + + + +