diff --git a/.agents/skills/issue-fix/SKILL.md b/.agents/skills/issue-fix/SKILL.md new file mode 100644 index 00000000..00c20c21 --- /dev/null +++ b/.agents/skills/issue-fix/SKILL.md @@ -0,0 +1,91 @@ +--- +name: issue-fix +description: Triage and fix a GitHub issue (or a user-reported bug) end to end — reproduce, find the root cause, generalise it to every sibling case, fix the class rather than the instance, verify, and open one PR. Use when given an issue URL/number or asked to fix a reported bug and open a PR ("看看这个 issue", "修一下这个 bug", "提 pr"). +--- + +# Fix an issue: from one case to the whole class + +The reported case is one sample of a defect class. A fix that only makes the +screenshot go away is not done: the next question is always "does this break for other +languages / platforms / entry points too?". Answer that before anyone asks. + +## 1. Read and reproduce + +- `gh issue view --comments` (repo `l0ng-ai/openidea`). Read attached logs/screenshots + closely: OS, language, toolchain versions, exact error text. +- Locate the code path and confirm the claim in code, with `file:line` evidence. + Say plainly whether it is a real bug, a missing feature, or user environment. +- Reproduce locally when possible (`fixtures/` projects, `/verify` for runtime). If the + reporter's platform (often Windows/Linux) can't be reproduced here, say so up front. +- Do not ask for a generic environment/log checklist before trying the flow and inspecting the code. + Record the attempt and result; ask only for a fact that actually blocks the next diagnostic step. + Missing fields, limited time, or unavailable automation are not by themselves reasons to ask the reporter. +- Treat a maintainer-confirmed bug as an investigation task even if local reproduction fails. Distinguish + “not reproduced here” from “not a bug”, and do not require the reporter to prove its existence again. + +## 2. Name the root cause as a class + +Write one sentence of the form " is missing/wrong, so any hits it", +not " is broken". Typical classes in this codebase: + +- Per-language config handled for one language only (`languages/*.toml`, `ide-sdk`). +- Platform gaps: paths, executables, shells, `.cmd`/`.exe`, separators — macOS-only code. +- Toolchain/version discovery: one probe location, no version check, no rescan. +- Two features each implementing half a flow and never joined (feature isolation → + should go through an action/service). +- State not reset on project switch / window reuse; events not scoped per window. +- Failure surfaced as raw stderr/stack trace or retried until "crashed" instead of a + clear status with an action. +- UI: same primitive styled differently in N places. + +## 3. Sweep for siblings + +Search systematically, not by memory: + +- Same pattern elsewhere: `git grep` the function, the config key, the error string, + the hard-coded path/flag. Check every `languages/*.toml` entry and every platform branch. +- Same flow for other languages/runtimes (Java ↔ Kotlin/Gradle, Python ↔ venv/uv, + Node ↔ Deno/Bun, Go, Rust …): would they fail the same way? +- Other callers/entry points of the same flow (menu, context menu, gutter, shortcut, CLI arg). +- Neighbouring failure modes of the same mechanism (not installed / wrong version / + installed but crashes / slow start / installed mid-session). + For broad sweeps spawn one Explore agent with the class description, not the instance. + +## 4. Decide the fix at the right altitude + +- Fix the mechanism once (shared helper, action, config hook) so siblings are fixed + by construction; don't patch each call site. +- Respect `AGENTS.md` rules (crate boundaries, no cross-feature imports, `IdeResult`, + UTF-16). If the right fix breaks a rule, pick the rule-respecting design and say why. +- Compare with how established IDEs solve it when relevant; prefer the established answer. +- Present: root cause, sibling table, proposed fix, what's deferred. When the user leaves + the choice to you, go ahead without asking again. + +## 5. Implement and test the class + +- Tests cover the class: table-driven over languages/platforms/states, not just the + reported input. Include the reporter's case verbatim. +- Run the scoped checks (`pnpm vitest run `, `cargo nextest run -p `), then + `/verify` in the app for UI or runtime behaviour. + +## 6. PR + +- One branch `fix/`, one PR unless the user asks otherwise; `Fixes #` in the body. +- Don't stage untracked IDE-state dirs (`.ideal/`, `.openidea/`) or screenshots. +- PR body follows `.github/pull_request_template.md`: `Closes #`, then + **What and why** (user-visible change + root cause at class level, 2–4 sentences), + **How it was verified** (commands run and results, siblings fixed/deferred, what could break and how + it was checked, what was not verified, e.g. "Windows not tested"; for UI changes the before/after + screenshot paths from `/verify`), **Review focus** (optional). Short and plain. + +## Report format + +Reply in the user's language: + +``` +Root cause: +Sibling sweep: +| Location / case | Affected? | Action | +Fixed / not covered (why) / not verified (platform) +PR: +``` diff --git a/.agents/skills/verify/SKILL.md b/.agents/skills/verify/SKILL.md new file mode 100644 index 00000000..2ca0d5a0 --- /dev/null +++ b/.agents/skills/verify/SKILL.md @@ -0,0 +1,57 @@ +--- +name: verify +description: Build, launch and drive the OpenIDEA desktop app through the Tauri MCP bridge to verify a change at runtime. +--- + +# Verify OpenIDEA in the running app + +## Launch + +- Run the app from the worktree that holds the change: `pnpm tauri dev` in the background, logging + into your task's scratch subdirectory. Use `pnpm dev:app` instead when your checks call + `window.__TAURI__` from `execute_js`. The wrapper gives each worktree its own app identity, data, + settings and Vite port, so other worktrees and the installed app don't interfere. If it reports a + session already running for this worktree, reuse that one. +- Once the app is up, `pnpm dev:status` lists this worktree's MCP bridges. Pass the reported + `appIdentifier` (the port) on every `mcp__tauri__*` call. The MCP server is shared between + sessions: never `driver_session stop` without an `appIdentifier`, and re-check `document.title` / + `location.href` before acting if anything looks off. +- Make sure the binary contains the change: a session already running for this worktree may be an + older build. Restart it after Rust changes; frontend changes reload through Vite. +- After the app restarts, `driver_session start` again for your port (a stale session says "Already connected"). +- If the `pnpm tauri dev` process exits while the app window stays open, Vite is gone and any webview + reload shows an empty `#root`. Stop that app and start the session again. +- Stop everything you started when done and nothing else; never use process-name kills. + +## Driving + +- `manage_window focus` before screenshots / timing-sensitive steps: a background window throttles timers + (debounced LSP / VCS refreshes stall, `setTimeout` promises in `execute_js` time out) and native + screenshots come back black. +- Settings is a separate native window `settings-main`; pass `windowId`, focus it before screenshotting. +- CodeMirror / xterm ignore MCP `type`. Focus the element, then `document.execCommand('insertText', false, text)`. + For xterm Enter, dispatch a `keydown` (`key: 'Enter', keyCode: 13`) on `.xterm-helper-textarea`. +- MCP clicks arrive with `event.detail === 0`; CodeMirror treats them as line-select clicks. Set the caret + through the view instead: `document.querySelector('.cm-content').cmTile.view`. +- MCP `text` strategy can resolve to a hidden node; prefer coordinates (CSS px = image px × 0.9) or CSS selectors. +- CSS-module edits often trigger a full page reload in dev: dialogs close, Run sessions and terminal + scrollback reset, and each reload spawns a fresh shell. Re-open what you were testing. + +## Evidence + +- For UI or interaction changes, capture `before.png` (on `main` or with the fix reverted) and + `after.png` at the same state and window size, into your task's scratch subdirectory. Multi-step + flows: a numbered series (`after-1.png`, `after-2.png`, ...). +- Report the absolute paths; they go into the PR's **How it was verified** section and the triage + acceptance entry. Never commit screenshots to the feature branch. + +## Fixture + +`fixtures/` has sample projects (go-app, rust-app, ts-app, kotlin-gradle, polyglot). Copy one into +your scratch directory before editing it, and don't commit the IDE state it creates (`.openidea/`). + +## Flows worth driving + +⌘⇧O Goto File · ⌘⇧F Find in Files (preview scroll + highlight) · ⌘⇧A Find Action · editor edit/undo/autosave · +Terminal (Unicode, venv) · Commit view at the default narrow width · Problems · Run → Stop · Project view +New / Delete (nested `a/b.txt` names) · ⌘, Settings search (+ Keymap filter) · Git Log on a repo without commits. diff --git a/.claude/skills/issue-fix b/.claude/skills/issue-fix new file mode 120000 index 00000000..e632043f --- /dev/null +++ b/.claude/skills/issue-fix @@ -0,0 +1 @@ +../../.agents/skills/issue-fix \ No newline at end of file diff --git a/.claude/skills/verify b/.claude/skills/verify new file mode 120000 index 00000000..326bc3f8 --- /dev/null +++ b/.claude/skills/verify @@ -0,0 +1 @@ +../../.agents/skills/verify \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index b7008354..35e26f55 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -42,6 +42,12 @@ body: 3. ... validations: required: true + - type: textarea + id: reproduction + attributes: + label: Minimal reproduction + description: A link to a small public repository, or the smallest files/snippet that trigger the problem. Optional, but it's the fastest way to get a fix. + placeholder: https://github.com/you/repro-project - type: textarea id: expected attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index bc9ad784..ac1c9037 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -27,6 +27,16 @@ body: description: How you'd like it to work. Mockups or examples from other tools are welcome. validations: required: true + - type: textarea + id: acceptance + attributes: + label: Acceptance criteria + description: How we'll know it's done. One observable behavior per line. + placeholder: | + - [ ] Right-clicking a test class shows "Run with Coverage" + - [ ] Coverage results open in the Coverage tool window + validations: + required: true - type: textarea id: alternatives attributes: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..836b8710 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,17 @@ +Closes # + +## What and why + + + +## How it was verified + + + +## Review focus + + diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 00000000..d1a74b7e --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,69 @@ +name: PR check +on: + # pull_request_target so the check can label PRs from forks. It never checks out + # or runs PR code: it only reads the PR title and description. + pull_request_target: + types: [opened, edited, reopened, ready_for_review] + +permissions: + issues: write + pull-requests: write + +concurrency: + group: pr-check-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + check: + if: github.event.pull_request.user.type != 'Bot' && !github.event.pull_request.draft + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + const LABEL = 'needs-info'; + const MARKER = ''; + // Docs, CI and pure refactors may skip the linked issue. + const NO_ISSUE_TYPES = /^(docs|ci|build|chore|refactor|style|test)(\(.+\))?!?:/; + + const body = (pr.body || '').replace(//g, ''); + const problems = []; + + const linksIssue = + /\b(close[sd]?|fix(e[sd])?|resolve[sd]?)\s*:?\s+(#\d+|https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/issues\/\d+)/i.test(body); + if (!linksIssue && !NO_ISSUE_TYPES.test(pr.title)) { + problems.push('Link the issue this PR resolves, e.g. `Closes #123`.'); + } + + const verified = body.match(/^##+\s*How it was verified\s*$([\s\S]*?)(?=^##+\s|(?![\s\S]))/im); + if (!verified || !verified[1].trim()) { + problems.push('Fill in **How it was verified**: the commands you ran and their results. UI changes need before/after screenshots.'); + } + + const { owner, repo } = context.repo; + const issue_number = pr.number; + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }); + const existing = comments.find(c => c.user.type === 'Bot' && c.body.includes(MARKER)); + const labeled = pr.labels.some(l => l.name === LABEL); + + if (problems.length) { + const text = `${MARKER}\nThanks for the PR. Before review, please update the description (see [CONTRIBUTING.md](https://github.com/${owner}/${repo}/blob/main/CONTRIBUTING.md)):\n\n${problems.map(p => `- ${p}`).join('\n')}\n\nThis check reruns when you edit the description.`; + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body: text }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number, body: text }); + } + if (!labeled) { + await github.rest.issues.addLabels({ owner, repo, issue_number, labels: [LABEL] }); + } + return; + } + + // Only undo what this check did: the label is cleared only when our comment exists. + if (existing) { + await github.rest.issues.deleteComment({ owner, repo, comment_id: existing.id }); + if (labeled) { + await github.rest.issues.removeLabel({ owner, repo, issue_number, name: LABEL }).catch(() => {}); + } + } diff --git a/.gitignore b/.gitignore index a3a6a71d..88c7d40b 100644 --- a/.gitignore +++ b/.gitignore @@ -30,9 +30,13 @@ target/ test-results/ playwright-report/ -# Local AI agent files -CLAUDE.md -.claude/ +# Local AI agent files. Shared skills live in .agents/skills; .claude/skills links to them. +CLAUDE.local.md +.claude/* +!.claude/skills/ +.claude/skills/* +!.claude/skills/verify +!.claude/skills/issue-fix # Local reference screenshots pycharm-screenshots/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..35f9cdf5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,118 @@ +# OpenIDEA — contributor guide + +Rust + Tauri 2 desktop IDE with full-featured, multi-language code intelligence. +Visual style: dark, low-contrast greys, one cool accent +(`src/styles/tokens.css` is the source of truth for colours and metrics). + +## Layout + +- `crates/ide-*` — backend crates; one domain each. Depend on `ide-core` for shared + types; never reach into another crate's internals. +- `src-tauri/` — glue only: `state.rs` (AppState / Project), `commands/.rs` + (thin typed IPC adapters), `events.rs`. No domain logic here. +- `src/bindings.ts` — generated by tauri-specta (`pnpm gen:bindings`). Never edit. +- `src/platform/` — ipc (`call`, `onIdeEvent`, `nextRequestId`), actions registry, + keymap + dispatcher, context keys / `when` clauses, project store. +- `src/workbench/contributions.ts` — extension points (tool windows, status items, + overlays, editor kinds) and cross-feature services (editor, tool windows, notifications). +- `src/ui/` — design-system primitives. `src/features//` — one feature each, + exporting `activate()` registered in `src/features/index.ts`. +- `languages/.toml` — language definitions (detection, comments, LSP, DAP). +- `crates/ide-host` — the `openidea-server` binary for remote development. +- `wit/` — plugin interface definitions; `plugins/` — sample and bundled plugins; + `templates/` — plugin starter templates. +- `e2e/` — Playwright UI tests (`pnpm e2e`). `fixtures/` — sample projects per language + for manual and automated testing. + +## Rules + +- IPC coordinates: 0-based lines, **UTF-16** columns/offsets everywhere (`ide-core::text`). +- Every command returns `IdeResult`; convert internal errors at the crate edge. +- Streaming → `tauri::ipc::Channel`; cancellable → UI-allocated `RequestId` + + `cancel_request`. Backend → UI notifications → `IdeEvent` via `EventSink`. +- Long work never blocks the UI thread or a Tauri command thread: use + `spawn_blocking`/rayon, honour `CancellationToken`. +- Frontend features never import each other; communicate via actions, contributions + and services. All user-invocable behaviour is an action with a stable id; default + shortcuts live in `src/platform/keymaps/`. +- Styling: CSS Modules + tokens only (no hex literals in components). Inline SVG + icons from `src/ui/icons.tsx`, never emoji. Real `