From f9e47fc95c761ca7e163c784bb2b4c0d96d83c7e Mon Sep 17 00:00:00 2001 From: Sergii Demianchuk Date: Wed, 8 Jul 2026 13:15:47 -0400 Subject: [PATCH 1/7] rename extensions from atom-ai to levelcode-ai --- .github/workflows/release.yml | 2 +- CLAUDE.md | 26 +- blog/atom-style-package-hot-reload.md | 2 +- branding/product.overlay.json | 2 +- docs/CORE-PATCHES.md | 2 +- docs/M1-SPEC.md | 6 +- docs/RELEASING.md | 2 +- docs/levelcode-agent-sketch-design.md | 2 +- docs/levelcode-multiprovider-design.md | 4 +- extensions/atom-npp-pack/package.json | 404 ------------------ .../{atom-ai => levelcode-ai}/README.md | 0 extensions/{atom-ai => levelcode-ai}/agent.js | 0 .../{atom-ai => levelcode-ai}/aiEdit.js | 0 .../{atom-ai => levelcode-ai}/customize.js | 0 .../{atom-ai => levelcode-ai}/extension.js | 0 .../{atom-ai => levelcode-ai}/importVscode.js | 0 .../inlineComplete.js | 0 .../{atom-ai => levelcode-ai}/lmProvider.js | 0 .../{atom-ai => levelcode-ai}/media/chat.html | 0 .../media/levelcode-ai.svg | 0 .../media/sketch.html | 0 .../media/walkthrough/ai.md | 0 .../media/walkthrough/look.md | 0 .../media/walkthrough/updates.md | 0 .../{atom-ai => levelcode-ai}/package.json | 0 .../{atom-ai => levelcode-ai}/providers.js | 0 .../providers/anthropic.js | 0 .../providers/catalog.js | 0 .../providers/gateway.js | 0 .../providers/index.js | 0 .../providers/openaiCompat.js | 0 .../providers/sse.js | 0 .../providers/translate.js | 0 .../reviewSession.js | 0 .../{atom-ai => levelcode-ai}/sketch.js | 0 .../sketch/agentCatalog.js | 0 .../{atom-ai => levelcode-ai}/sketch/graph.js | 0 .../sketch/pricing.js | 0 .../sketch/templates.js | 0 .../{atom-ai => levelcode-ai}/skills.js | 0 .../skills/code-review/SKILL.md | 0 .../skills/focused-fix/SKILL.md | 0 .../skills/test-writing/SKILL.md | 0 .../test/catalog.test.js | 0 .../test/gateway.test.js | 0 .../test/importVscode.test.js | 0 .../test/providers.test.js | 0 .../test/sketch.test.js | 0 .../test/translate.test.js | 0 .../{atom-ai => levelcode-ai}/verify.js | Bin .../extension.js | 0 .../keymaps.js | 0 .../package.json | 2 +- .../packages.js | 0 .../.vscodeignore | 0 .../README.md | 0 .../bigFile.js | 10 +- .../columnOps.js | 14 +- .../encodingEol.js | 24 +- .../extension.js | 58 +-- .../fileOps.js | 10 +- .../lineOps.js | 38 +- extensions/levelcode-npp-pack/package.json | 404 ++++++++++++++++++ .../package.json | 2 +- .../themes/atom-one-dark.json | 0 .../themes/atom-one-light.json | 0 .../extension.js | 0 .../package.json | 2 +- .../test/update.test.js | 2 +- .../update.js | 0 scripts/strip-unreleased.mjs | 18 +- tools/sync-server/README.md | 2 +- tools/update-server/README.md | 4 +- tools/update-server/server.js | 2 +- 74 files changed, 522 insertions(+), 522 deletions(-) delete mode 100644 extensions/atom-npp-pack/package.json rename extensions/{atom-ai => levelcode-ai}/README.md (100%) rename extensions/{atom-ai => levelcode-ai}/agent.js (100%) rename extensions/{atom-ai => levelcode-ai}/aiEdit.js (100%) rename extensions/{atom-ai => levelcode-ai}/customize.js (100%) rename extensions/{atom-ai => levelcode-ai}/extension.js (100%) rename extensions/{atom-ai => levelcode-ai}/importVscode.js (100%) rename extensions/{atom-ai => levelcode-ai}/inlineComplete.js (100%) rename extensions/{atom-ai => levelcode-ai}/lmProvider.js (100%) rename extensions/{atom-ai => levelcode-ai}/media/chat.html (100%) rename extensions/{atom-ai => levelcode-ai}/media/levelcode-ai.svg (100%) rename extensions/{atom-ai => levelcode-ai}/media/sketch.html (100%) rename extensions/{atom-ai => levelcode-ai}/media/walkthrough/ai.md (100%) rename extensions/{atom-ai => levelcode-ai}/media/walkthrough/look.md (100%) rename extensions/{atom-ai => levelcode-ai}/media/walkthrough/updates.md (100%) rename extensions/{atom-ai => levelcode-ai}/package.json (100%) rename extensions/{atom-ai => levelcode-ai}/providers.js (100%) rename extensions/{atom-ai => levelcode-ai}/providers/anthropic.js (100%) rename extensions/{atom-ai => levelcode-ai}/providers/catalog.js (100%) rename extensions/{atom-ai => levelcode-ai}/providers/gateway.js (100%) rename extensions/{atom-ai => levelcode-ai}/providers/index.js (100%) rename extensions/{atom-ai => levelcode-ai}/providers/openaiCompat.js (100%) rename extensions/{atom-ai => levelcode-ai}/providers/sse.js (100%) rename extensions/{atom-ai => levelcode-ai}/providers/translate.js (100%) rename extensions/{atom-ai => levelcode-ai}/reviewSession.js (100%) rename extensions/{atom-ai => levelcode-ai}/sketch.js (100%) rename extensions/{atom-ai => levelcode-ai}/sketch/agentCatalog.js (100%) rename extensions/{atom-ai => levelcode-ai}/sketch/graph.js (100%) rename extensions/{atom-ai => levelcode-ai}/sketch/pricing.js (100%) rename extensions/{atom-ai => levelcode-ai}/sketch/templates.js (100%) rename extensions/{atom-ai => levelcode-ai}/skills.js (100%) rename extensions/{atom-ai => levelcode-ai}/skills/code-review/SKILL.md (100%) rename extensions/{atom-ai => levelcode-ai}/skills/focused-fix/SKILL.md (100%) rename extensions/{atom-ai => levelcode-ai}/skills/test-writing/SKILL.md (100%) rename extensions/{atom-ai => levelcode-ai}/test/catalog.test.js (100%) rename extensions/{atom-ai => levelcode-ai}/test/gateway.test.js (100%) rename extensions/{atom-ai => levelcode-ai}/test/importVscode.test.js (100%) rename extensions/{atom-ai => levelcode-ai}/test/providers.test.js (100%) rename extensions/{atom-ai => levelcode-ai}/test/sketch.test.js (100%) rename extensions/{atom-ai => levelcode-ai}/test/translate.test.js (100%) rename extensions/{atom-ai => levelcode-ai}/verify.js (100%) rename extensions/{atom-hackability => levelcode-hackability}/extension.js (100%) rename extensions/{atom-hackability => levelcode-hackability}/keymaps.js (100%) rename extensions/{atom-hackability => levelcode-hackability}/package.json (97%) rename extensions/{atom-hackability => levelcode-hackability}/packages.js (100%) rename extensions/{atom-npp-pack => levelcode-npp-pack}/.vscodeignore (100%) rename extensions/{atom-npp-pack => levelcode-npp-pack}/README.md (100%) rename extensions/{atom-npp-pack => levelcode-npp-pack}/bigFile.js (84%) rename extensions/{atom-npp-pack => levelcode-npp-pack}/columnOps.js (85%) rename extensions/{atom-npp-pack => levelcode-npp-pack}/encodingEol.js (70%) rename extensions/{atom-npp-pack => levelcode-npp-pack}/extension.js (79%) rename extensions/{atom-npp-pack => levelcode-npp-pack}/fileOps.js (85%) rename extensions/{atom-npp-pack => levelcode-npp-pack}/lineOps.js (71%) create mode 100644 extensions/levelcode-npp-pack/package.json rename extensions/{atom-themes => levelcode-themes}/package.json (95%) rename extensions/{atom-themes => levelcode-themes}/themes/atom-one-dark.json (100%) rename extensions/{atom-themes => levelcode-themes}/themes/atom-one-light.json (100%) rename extensions/{atom-updater => levelcode-updater}/extension.js (100%) rename extensions/{atom-updater => levelcode-updater}/package.json (98%) rename extensions/{atom-updater => levelcode-updater}/test/update.test.js (96%) rename extensions/{atom-updater => levelcode-updater}/update.js (100%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6a071d2..58d1879 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: node-version: "24" - name: Extension unit tests run: | - cd extensions/atom-ai + cd extensions/levelcode-ai for t in test/*.test.js; do node "$t"; done build: diff --git a/CLAUDE.md b/CLAUDE.md index be0820c..4cdc4cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ lives in the tracked repo and is re-applied onto a clean clone: | Ours (edit here, tracked) | Gets installed into (generated) | By | | --- | --- | --- | -| `extensions/atom-npp-pack/`, `extensions/atom-ai/` | `vscode/extensions/*` | `apply-branding.mjs` (copy) | +| `extensions/levelcode-npp-pack/`, `extensions/levelcode-ai/` | `vscode/extensions/*` | `apply-branding.mjs` (copy) | | `branding/product.overlay.json`, `branding/icons/` | `vscode/product.json`, `vscode/resources/...` | `apply-branding.mjs` (merge/copy) | | `patches/levelcode-core.patch` | edits to `vscode/src/**`, `vscode/build/**` | `bootstrap.sh` (`git apply`) | @@ -33,12 +33,12 @@ docs/CORE-PATCHES.md log of every edit inside vscode/ (tagged // [LevelCo docs/M0-RUNBOOK.md, EXIT-TEST.md, M1-SPEC.md branding/product.overlay.json LevelCode identity + Open VSX gallery (deep-merged onto product.json) branding/icons/ app icon source PNG, generated .icns, 1024 png -extensions/atom-npp-pack/ Notepad++ power-editing pack (plain JS, no build step) -extensions/atom-ai/ native Claude AI (chat, inline completion, providers, edit-with-diff, LM provider) -extensions/atom-themes/ signature One Dark / One Light themes (JSON, default via configurationDefaults) -extensions/atom-hackability/ user init script + Atom/NPP keymap presets + package generator & hot-reload dev loader -extensions/atom-sync/ 'levelcode' auth provider that lights up the built-in Settings Sync (LevelCode Sync, S0) -extensions/atom-updater/ notify-only update checker (polls the update feed; never auto-applies) +extensions/levelcode-npp-pack/ Notepad++ power-editing pack (plain JS, no build step) +extensions/levelcode-ai/ native Claude AI (chat, inline completion, providers, edit-with-diff, LM provider) +extensions/levelcode-themes/ signature One Dark / One Light themes (JSON, default via configurationDefaults) +extensions/levelcode-hackability/ user init script + Atom/NPP keymap presets + package generator & hot-reload dev loader +extensions/levelcode-sync/ 'levelcode' auth provider that lights up the built-in Settings Sync (LevelCode Sync, S0) +extensions/levelcode-updater/ notify-only update checker (polls the update feed; never auto-applies) patches/levelcode-core.patch our core source edits, applied on bootstrap scripts/ bootstrap.sh, apply-branding.mjs, run-dev.sh, build-macos.sh, make-dmg.sh, make-icon.sh; atom (CLI launcher) + install-level.sh tools/ dependency-free reference servers: sync-server (/v1 Settings-Sync), update-server (/api/update feed) @@ -83,17 +83,17 @@ Tracked in `patches/levelcode-core.patch`, tagged with `// [LevelCode]`. Find th ## Settings shipped as defaults (via extension `configurationDefaults` or core patch) - `files.hotExit = onExitAndWindowClose` (core patch — application-scoped). -- `workbench.editorLargeFileConfirmation = 2048`, `chat.commandCenter.enabled = false`, `chat.disableAIFeatures = true` (atom-npp-pack). +- `workbench.editorLargeFileConfirmation = 2048`, `chat.commandCenter.enabled = false`, `chat.disableAIFeatures = true` (levelcode-npp-pack). - Extensions can only override **machine-overridable / window / resource / language-overridable** scoped settings — NOT application/machine. (That's why hotExit needed a core patch.) ## Feature status -**M1 — Notepad++ pack (`extensions/atom-npp-pack/`, all done, plain JS):** +**M1 — Notepad++ pack (`extensions/levelcode-npp-pack/`, all done, plain JS):** macros (`Cmd+Shift+R`/`Cmd+Alt+P`), Duplicate file (`Cmd+D` / Explorer menu), Sublime hot-exit, line operations (sort/dedup/case/…), column incrementing numbers, encoding/EOL status-bar toggle, big-file mode badge. Files: extension.js + fileOps/lineOps/columnOps/encodingEol/bigFile.js. -**M2 — native AI (`extensions/atom-ai/`, working):** +**M2 — native AI (`extensions/levelcode-ai/`, working):** - `providers/` — **multi-provider BYOK (P1)**. A registry data table (`providers/index.js` `PROVIDERS` + `getProvider`/`normId`/`secretStorageKey`/`isInsecureCustomUrl`) dispatches `streamChat()`/`complete()` on the row's `kind`: **Anthropic** keeps its native adapter (`providers/anthropic.js` — prompt caching + full tool-use, @@ -147,13 +147,13 @@ big-file mode badge. Files: extension.js + fileOps/lineOps/columnOps/encodingEol multi-session core effort. We use the diff-tab review instead. - Built since M2 shipped: inline tab-completion (`inlineComplete.js`), multi-file + auto-retrieval chat context (`gatherAutoContext`), chat moved to the right side bar, typewriter streaming, `make-dmg.sh` packaging. -- M3 so far: One Dark/Light themes (`atom-themes`); user init script + Atom/Notepad++ keymap presets + - package generator with live hot-reload (`atom-hackability`). The Atom/NPP keymap also clears the old M1 leftover. +- M3 so far: One Dark/Light themes (`levelcode-themes`); user init script + Atom/Notepad++ keymap presets + + package generator with live hot-reload (`levelcode-hackability`). The Atom/NPP keymap also clears the old M1 leftover. - M4 agentic multi-file tasks: DONE. `agent.js` has the full tool loop — list_files/read_file/search, update_plan, edit_file/write_file/**delete_file** (apply-then-review with Keep/Undo + per-turn checkpoint restore via `reviewSession.js`), run_command(+background)/read_command_output, ask_user, use_skill — plus the M5 auto-verify loop. `delete_file` also enables rename/move (write new path → delete old). -- LevelCode Sync (S0) + notify-only updater (U0): see `extensions/atom-sync` / `extensions/atom-updater` + `tools/`. +- LevelCode Sync (S0) + notify-only updater (U0): see `extensions/levelcode-sync` / `extensions/levelcode-updater` + `tools/`. - Not yet built: remaining M3 hackability — settings UI, theme studio, VS Code settings import. ## Conventions diff --git a/blog/atom-style-package-hot-reload.md b/blog/atom-style-package-hot-reload.md index 6cf9ea6..17378d5 100644 --- a/blog/atom-style-package-hot-reload.md +++ b/blog/atom-style-package-hot-reload.md @@ -169,5 +169,5 @@ Write, save, see it. Welcome back. --- *LevelCode is an AI-native, hackable, Notepad++-powered editor for macOS, built on the MIT -Code-OSS core. The package generator and hot-reload loader live in the `atom-hackability` +Code-OSS core. The package generator and hot-reload loader live in the `levelcode-hackability` extension, alongside the user init script and the Atom/Notepad++ keymap presets.* diff --git a/branding/product.overlay.json b/branding/product.overlay.json index 1921acc..aaa11bf 100644 --- a/branding/product.overlay.json +++ b/branding/product.overlay.json @@ -23,7 +23,7 @@ "urlProtocol": "levelcode", "quality": "stable", - "_syncComment": "LevelCode Sync (built-in Settings Sync + the `levelcode` auth provider from extensions/atom-sync) was REMOVED for now — the managed sync host isn't built and it duplicated the LevelCode Cloud sign-in. Removed at source: the atom-sync extension, this configurationSync.store block, and the Welcome 'Sync' step. Re-add when the managed sync host ships.", + "_syncComment": "LevelCode Sync (built-in Settings Sync + the `levelcode` auth provider from extensions/levelcode-sync) was REMOVED for now — the managed sync host isn't built and it duplicated the LevelCode Cloud sign-in. Removed at source: the levelcode-sync extension, this configurationSync.store block, and the Welcome 'Sync' step. Re-add when the managed sync host ships.", "licenseName": "MIT", "licenseUrl": "https://github.com/levelcode/levelcode/blob/main/LICENSE", diff --git a/docs/CORE-PATCHES.md b/docs/CORE-PATCHES.md index 8fe0c0e..cd6e247 100644 --- a/docs/CORE-PATCHES.md +++ b/docs/CORE-PATCHES.md @@ -10,7 +10,7 @@ comment in the code, so a rebase onto a newer Code-OSS tag is easy to re-apply/v that is *ours* lives in the tracked repo and is re-installed onto a clean clone: - **Branding** → `branding/product.overlay.json` (+ `branding/icons/`), applied by `apply-branding.mjs`. -- **Extensions** → `extensions/atom-npp-pack/`, `extensions/atom-ai/` are the **canonical source**; +- **Extensions** → `extensions/levelcode-npp-pack/`, `extensions/levelcode-ai/` are the **canonical source**; `apply-branding.mjs` copies them into `vscode/extensions/`. Edit them *here*, not in `vscode/`. - **Core source edits** → captured as `patches/levelcode-core.patch`; `bootstrap.sh` applies it (`git apply --3way`) onto the fresh checkout. diff --git a/docs/M1-SPEC.md b/docs/M1-SPEC.md index 76b65c0..4c91112 100644 --- a/docs/M1-SPEC.md +++ b/docs/M1-SPEC.md @@ -4,11 +4,11 @@ Goal of M1: make LevelCode feel like Notepad++ for the editing muscle-memory Ser ## Design principle: isolate, don't scatter -Everything that **can** live in a first-party bundled extension **does** — shipped enabled by default as the "Notepad++ Pack" (`extensions/atom-npp-pack/`). This keeps our code out of upstream core files so rebasing onto new Code-OSS stays cheap (PLAN.md §8). We only touch core where the extension API genuinely can't reach: large-file rendering and the encoding/EOL status-bar affordances. Those core touches are kept in clearly marked, self-contained modules. +Everything that **can** live in a first-party bundled extension **does** — shipped enabled by default as the "Notepad++ Pack" (`extensions/levelcode-npp-pack/`). This keeps our code out of upstream core files so rebasing onto new Code-OSS stays cheap (PLAN.md §8). We only touch core where the extension API genuinely can't reach: large-file rendering and the encoding/EOL status-bar affordances. Those core touches are kept in clearly marked, self-contained modules. Two buckets: -- **Bucket A — bundled extension** (`atom-npp-pack`): macros, line operations, column-mode extras, NPP keymap. No core edits. +- **Bucket A — bundled extension** (`levelcode-npp-pack`): macros, line operations, column-mode extras, NPP keymap. No core edits. - **Bucket B — core touches** (small, isolated): big-file mode, always-visible encoding/EOL status bar items. Each guarded behind an `_atomPlusPlus` product.json flag so it's easy to diff and toggle. --- @@ -73,7 +73,7 @@ Ship a selectable **"Notepad++"** keymap (alongside an "Atom" keymap from M3) so ## Build order & milestone gates -1. **Scaffold `atom-npp-pack`** bundled extension + "Notepad++ Pack" settings namespace (`levelcode.*`). Wire it into the build's built-in extensions list. +1. **Scaffold `levelcode-npp-pack`** bundled extension + "Notepad++ Pack" settings namespace (`levelcode.*`). Wire it into the build's built-in extensions list. 2. **Macros** (headline) → demo + exit test. 3. **Line ops + column extras** (fast wins, pure extension) → exit tests. 4. **Encoding/EOL status bar** (Bucket B, small core touch) → exit test. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index b480272..350cff1 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -88,7 +88,7 @@ from **levelcode.ai/download**. ## 5. Point the update feed at the release -The notify-only updater (`extensions/atom-updater`) polls a feed from `tools/update-server`. After +The notify-only updater (`extensions/levelcode-updater`) polls a feed from `tools/update-server`. After publishing, update the feed's release entry (version + the release-asset URLs above) so running installs see the new version. Keep the feed version in lockstep with the tag. diff --git a/docs/levelcode-agent-sketch-design.md b/docs/levelcode-agent-sketch-design.md index 190e458..094f97b 100644 --- a/docs/levelcode-agent-sketch-design.md +++ b/docs/levelcode-agent-sketch-design.md @@ -22,7 +22,7 @@ Everything below sits on the multi-provider layer already shipped: ## 1. The SK1 skeleton (shipped) ``` -extensions/atom-ai/ +extensions/levelcode-ai/ sketch.js controller: panel, persistence, THE RUNNER (reentrancy-guarded, abortable) sketch/agentCatalog.js 96 agent archetypes in 13 palette groups (derived from ruflo, MIT © ruvnet — names + one-line roles only) sketch/templates.js ready-made topologies — incl. the "Design a Key-Value Store" hero template [unit-tested] diff --git a/docs/levelcode-multiprovider-design.md b/docs/levelcode-multiprovider-design.md index 62e661c..50e68b1 100644 --- a/docs/levelcode-multiprovider-design.md +++ b/docs/levelcode-multiprovider-design.md @@ -39,12 +39,12 @@ Everything reduces to **OpenAI Chat Completions**, **Anthropic Messages** (block ## 2. Recommended architecture for LevelCode -### 2.1 Module layout — `extensions/atom-ai/providers/` +### 2.1 Module layout — `extensions/levelcode-ai/providers/` Today everything is one flat `providers.js` with eight provider-named exports and hard-coded Anthropic assumptions in 4 places. Evolve it into a directory (keep `providers.js` as a thin re-export shim during migration so nothing breaks mid-refactor): ``` -extensions/atom-ai/providers/ +extensions/levelcode-ai/providers/ index.js # registry (PROVIDERS data table) + getProvider(id) + resolveProvider(id) + shared readLines() anthropic.js # native Anthropic Messages adapter (streamClaude/completeClaude/streamClaudeAgentTurn moved here) openaiCompat.js # THE universal adapter — /v1/chat/completions, param'd by {baseURL, apiKey, headers} diff --git a/extensions/atom-npp-pack/package.json b/extensions/atom-npp-pack/package.json deleted file mode 100644 index cbb8ba1..0000000 --- a/extensions/atom-npp-pack/package.json +++ /dev/null @@ -1,404 +0,0 @@ -{ - "name": "atom-npp-pack", - "displayName": "Atom++ Notepad++ Pack", - "description": "Notepad++-style power editing for Atom++: macros, line operations, column extras.", - "publisher": "atompp", - "version": "0.1.0", - "license": "MIT", - "engines": { - "vscode": "^1.126.0" - }, - "categories": [ - "Other" - ], - "capabilities": { - "virtualWorkspaces": true, - "untrustedWorkspaces": { - "supported": true - } - }, - "activationEvents": [ - "onStartupFinished" - ], - "main": "./extension.js", - "contributes": { - "commands": [ - { - "command": "atompp.macro.toggleRecording", - "title": "Macro: Start/Stop Recording", - "category": "Atom++" - }, - { - "command": "atompp.macro.play", - "title": "Macro: Play Last", - "category": "Atom++" - }, - { - "command": "atompp.macro.playNTimes", - "title": "Macro: Play Last N Times…", - "category": "Atom++" - }, - { - "command": "atompp.macro.saveLast", - "title": "Macro: Save Last Macro…", - "category": "Atom++" - }, - { - "command": "atompp.macro.runSaved", - "title": "Macro: Run Saved Macro…", - "category": "Atom++" - }, - { - "command": "atompp.macro.deleteSaved", - "title": "Macro: Delete Saved Macro…", - "category": "Atom++" - }, - { - "command": "atompp.file.duplicate", - "title": "Duplicate", - "category": "Atom++" - }, - { - "command": "atompp.lines.sortAsc", - "title": "Lines: Sort Ascending", - "category": "Atom++" - }, - { - "command": "atompp.lines.sortDesc", - "title": "Lines: Sort Descending", - "category": "Atom++" - }, - { - "command": "atompp.lines.sortCaseInsensitive", - "title": "Lines: Sort (Case-Insensitive)", - "category": "Atom++" - }, - { - "command": "atompp.lines.sortNumeric", - "title": "Lines: Sort Numerically", - "category": "Atom++" - }, - { - "command": "atompp.lines.removeDuplicates", - "title": "Lines: Remove Duplicate Lines", - "category": "Atom++" - }, - { - "command": "atompp.lines.removeDuplicatesAdjacent", - "title": "Lines: Remove Consecutive Duplicate Lines", - "category": "Atom++" - }, - { - "command": "atompp.lines.removeEmpty", - "title": "Lines: Remove Empty Lines", - "category": "Atom++" - }, - { - "command": "atompp.lines.reverse", - "title": "Lines: Reverse", - "category": "Atom++" - }, - { - "command": "atompp.lines.join", - "title": "Lines: Join", - "category": "Atom++" - }, - { - "command": "atompp.lines.trimTrailing", - "title": "Lines: Trim Trailing Whitespace", - "category": "Atom++" - }, - { - "command": "atompp.case.upper", - "title": "Convert to UPPERCASE", - "category": "Atom++" - }, - { - "command": "atompp.case.lower", - "title": "Convert to lowercase", - "category": "Atom++" - }, - { - "command": "atompp.case.title", - "title": "Convert to Title Case", - "category": "Atom++" - }, - { - "command": "atompp.case.toggle", - "title": "Toggle Case", - "category": "Atom++" - }, - { - "command": "atompp.column.insertNumbers", - "title": "Column: Insert Incrementing Numbers…", - "category": "Atom++" - }, - { - "command": "atompp.column.fill", - "title": "Column: Fill With Text…", - "category": "Atom++" - }, - { - "command": "atompp.eol.toLF", - "title": "Convert Line Endings to macOS/Unix (LF)", - "category": "Atom++" - }, - { - "command": "atompp.eol.toCRLF", - "title": "Convert Line Endings to Windows (CRLF)", - "category": "Atom++" - }, - { - "command": "atompp.eol.toggle", - "title": "Toggle Line Endings (Windows ⇄ macOS/Unix)", - "category": "Atom++" - }, - { - "command": "atompp.encoding.change", - "title": "Change File Encoding…", - "category": "Atom++" - } - ], - "submenus": [ - { - "id": "atompp.lineOps", - "label": "Line Operations" - }, - { - "id": "atompp.columnOps", - "label": "Column Editor" - }, - { - "id": "atompp.encodingOps", - "label": "Encoding / Line Endings" - } - ], - "menus": { - "explorer/context": [ - { - "command": "atompp.file.duplicate", - "group": "5_cutcopypaste@11", - "when": "!explorerResourceIsRoot" - } - ], - "editor/context": [ - { - "submenu": "atompp.lineOps", - "group": "1_modification@100", - "when": "editorTextFocus" - }, - { - "submenu": "atompp.columnOps", - "group": "1_modification@101", - "when": "editorTextFocus" - }, - { - "submenu": "atompp.encodingOps", - "group": "1_modification@102", - "when": "editorTextFocus" - } - ], - "atompp.columnOps": [ - { - "command": "atompp.column.insertNumbers", - "group": "1_column@1" - }, - { - "command": "atompp.column.fill", - "group": "1_column@2" - } - ], - "atompp.encodingOps": [ - { - "command": "atompp.eol.toLF", - "group": "1_eol@1" - }, - { - "command": "atompp.eol.toCRLF", - "group": "1_eol@2" - }, - { - "command": "atompp.eol.toggle", - "group": "1_eol@3" - }, - { - "command": "atompp.encoding.change", - "group": "2_encoding@1" - } - ], - "atompp.lineOps": [ - { - "command": "atompp.lines.sortAsc", - "group": "1_sort@1" - }, - { - "command": "atompp.lines.sortDesc", - "group": "1_sort@2" - }, - { - "command": "atompp.lines.sortCaseInsensitive", - "group": "1_sort@3" - }, - { - "command": "atompp.lines.sortNumeric", - "group": "1_sort@4" - }, - { - "command": "atompp.lines.removeDuplicates", - "group": "2_clean@1" - }, - { - "command": "atompp.lines.removeDuplicatesAdjacent", - "group": "2_clean@2" - }, - { - "command": "atompp.lines.removeEmpty", - "group": "2_clean@3" - }, - { - "command": "atompp.lines.trimTrailing", - "group": "2_clean@4" - }, - { - "command": "atompp.lines.reverse", - "group": "3_order@1" - }, - { - "command": "atompp.lines.join", - "group": "3_order@2" - }, - { - "command": "atompp.case.upper", - "group": "4_case@1" - }, - { - "command": "atompp.case.lower", - "group": "4_case@2" - }, - { - "command": "atompp.case.title", - "group": "4_case@3" - }, - { - "command": "atompp.case.toggle", - "group": "4_case@4" - } - ] - }, - "keybindings": [ - { - "command": "atompp.macro.toggleRecording", - "key": "cmd+shift+r", - "when": "editorTextFocus" - }, - { - "command": "atompp.macro.play", - "key": "cmd+alt+p", - "when": "editorTextFocus && !atompp.macroRecording" - }, - { - "command": "atompp.macro.rec.cursorDown", - "key": "down", - "when": "atompp.macroRecording && editorTextFocus && !suggestWidgetVisible" - }, - { - "command": "atompp.macro.rec.cursorUp", - "key": "up", - "when": "atompp.macroRecording && editorTextFocus && !suggestWidgetVisible" - }, - { - "command": "atompp.macro.rec.cursorLeft", - "key": "left", - "when": "atompp.macroRecording && editorTextFocus" - }, - { - "command": "atompp.macro.rec.cursorRight", - "key": "right", - "when": "atompp.macroRecording && editorTextFocus" - }, - { - "command": "atompp.macro.rec.cursorHome", - "key": "home", - "when": "atompp.macroRecording && editorTextFocus" - }, - { - "command": "atompp.macro.rec.cursorEnd", - "key": "end", - "when": "atompp.macroRecording && editorTextFocus" - }, - { - "command": "atompp.macro.rec.cursorWordLeft", - "key": "alt+left", - "when": "atompp.macroRecording && editorTextFocus" - }, - { - "command": "atompp.macro.rec.cursorWordRight", - "key": "alt+right", - "when": "atompp.macroRecording && editorTextFocus" - }, - { - "command": "atompp.macro.rec.deleteLeft", - "key": "backspace", - "when": "atompp.macroRecording && editorTextFocus" - }, - { - "command": "atompp.macro.rec.deleteRight", - "key": "delete", - "when": "atompp.macroRecording && editorTextFocus" - }, - { - "command": "atompp.macro.rec.deleteWordLeft", - "key": "alt+backspace", - "when": "atompp.macroRecording && editorTextFocus" - }, - { - "command": "atompp.macro.rec.insertNewline", - "key": "enter", - "when": "atompp.macroRecording && editorTextFocus && !suggestWidgetVisible && !inSnippetMode" - }, - { - "command": "atompp.macro.rec.insertTab", - "key": "tab", - "when": "atompp.macroRecording && editorTextFocus && !suggestWidgetVisible && !inSnippetMode" - }, - { - "command": "atompp.file.duplicate", - "key": "cmd+d", - "when": "filesExplorerFocus && !inputFocus && !explorerResourceIsRoot" - } - ], - "configurationDefaults": { - "workbench.editorLargeFileConfirmation": 2048, - "chat.commandCenter.enabled": false, - "chat.disableAIFeatures": true - }, - "configuration": { - "title": "Atom++ Notepad++ Pack", - "properties": { - "atompp.macros.maxSteps": { - "type": "number", - "default": 5000, - "minimum": 100, - "description": "Safety cap on the number of steps recorded in a single macro." - }, - "atompp.macros.maxReplayCount": { - "type": "number", - "default": 100000, - "minimum": 1, - "description": "Maximum repeat count allowed when playing a macro N times." - }, - "atompp.bigFile.thresholdMB": { - "type": "number", - "default": 256, - "minimum": 1, - "description": "Show the Big-file mode indicator when the active file is at least this many megabytes." - }, - "atompp.bigFile.notify": { - "type": "boolean", - "default": true, - "description": "Show a one-time notification when a file opens in Big-file mode." - } - } - } - } -} diff --git a/extensions/atom-ai/README.md b/extensions/levelcode-ai/README.md similarity index 100% rename from extensions/atom-ai/README.md rename to extensions/levelcode-ai/README.md diff --git a/extensions/atom-ai/agent.js b/extensions/levelcode-ai/agent.js similarity index 100% rename from extensions/atom-ai/agent.js rename to extensions/levelcode-ai/agent.js diff --git a/extensions/atom-ai/aiEdit.js b/extensions/levelcode-ai/aiEdit.js similarity index 100% rename from extensions/atom-ai/aiEdit.js rename to extensions/levelcode-ai/aiEdit.js diff --git a/extensions/atom-ai/customize.js b/extensions/levelcode-ai/customize.js similarity index 100% rename from extensions/atom-ai/customize.js rename to extensions/levelcode-ai/customize.js diff --git a/extensions/atom-ai/extension.js b/extensions/levelcode-ai/extension.js similarity index 100% rename from extensions/atom-ai/extension.js rename to extensions/levelcode-ai/extension.js diff --git a/extensions/atom-ai/importVscode.js b/extensions/levelcode-ai/importVscode.js similarity index 100% rename from extensions/atom-ai/importVscode.js rename to extensions/levelcode-ai/importVscode.js diff --git a/extensions/atom-ai/inlineComplete.js b/extensions/levelcode-ai/inlineComplete.js similarity index 100% rename from extensions/atom-ai/inlineComplete.js rename to extensions/levelcode-ai/inlineComplete.js diff --git a/extensions/atom-ai/lmProvider.js b/extensions/levelcode-ai/lmProvider.js similarity index 100% rename from extensions/atom-ai/lmProvider.js rename to extensions/levelcode-ai/lmProvider.js diff --git a/extensions/atom-ai/media/chat.html b/extensions/levelcode-ai/media/chat.html similarity index 100% rename from extensions/atom-ai/media/chat.html rename to extensions/levelcode-ai/media/chat.html diff --git a/extensions/atom-ai/media/levelcode-ai.svg b/extensions/levelcode-ai/media/levelcode-ai.svg similarity index 100% rename from extensions/atom-ai/media/levelcode-ai.svg rename to extensions/levelcode-ai/media/levelcode-ai.svg diff --git a/extensions/atom-ai/media/sketch.html b/extensions/levelcode-ai/media/sketch.html similarity index 100% rename from extensions/atom-ai/media/sketch.html rename to extensions/levelcode-ai/media/sketch.html diff --git a/extensions/atom-ai/media/walkthrough/ai.md b/extensions/levelcode-ai/media/walkthrough/ai.md similarity index 100% rename from extensions/atom-ai/media/walkthrough/ai.md rename to extensions/levelcode-ai/media/walkthrough/ai.md diff --git a/extensions/atom-ai/media/walkthrough/look.md b/extensions/levelcode-ai/media/walkthrough/look.md similarity index 100% rename from extensions/atom-ai/media/walkthrough/look.md rename to extensions/levelcode-ai/media/walkthrough/look.md diff --git a/extensions/atom-ai/media/walkthrough/updates.md b/extensions/levelcode-ai/media/walkthrough/updates.md similarity index 100% rename from extensions/atom-ai/media/walkthrough/updates.md rename to extensions/levelcode-ai/media/walkthrough/updates.md diff --git a/extensions/atom-ai/package.json b/extensions/levelcode-ai/package.json similarity index 100% rename from extensions/atom-ai/package.json rename to extensions/levelcode-ai/package.json diff --git a/extensions/atom-ai/providers.js b/extensions/levelcode-ai/providers.js similarity index 100% rename from extensions/atom-ai/providers.js rename to extensions/levelcode-ai/providers.js diff --git a/extensions/atom-ai/providers/anthropic.js b/extensions/levelcode-ai/providers/anthropic.js similarity index 100% rename from extensions/atom-ai/providers/anthropic.js rename to extensions/levelcode-ai/providers/anthropic.js diff --git a/extensions/atom-ai/providers/catalog.js b/extensions/levelcode-ai/providers/catalog.js similarity index 100% rename from extensions/atom-ai/providers/catalog.js rename to extensions/levelcode-ai/providers/catalog.js diff --git a/extensions/atom-ai/providers/gateway.js b/extensions/levelcode-ai/providers/gateway.js similarity index 100% rename from extensions/atom-ai/providers/gateway.js rename to extensions/levelcode-ai/providers/gateway.js diff --git a/extensions/atom-ai/providers/index.js b/extensions/levelcode-ai/providers/index.js similarity index 100% rename from extensions/atom-ai/providers/index.js rename to extensions/levelcode-ai/providers/index.js diff --git a/extensions/atom-ai/providers/openaiCompat.js b/extensions/levelcode-ai/providers/openaiCompat.js similarity index 100% rename from extensions/atom-ai/providers/openaiCompat.js rename to extensions/levelcode-ai/providers/openaiCompat.js diff --git a/extensions/atom-ai/providers/sse.js b/extensions/levelcode-ai/providers/sse.js similarity index 100% rename from extensions/atom-ai/providers/sse.js rename to extensions/levelcode-ai/providers/sse.js diff --git a/extensions/atom-ai/providers/translate.js b/extensions/levelcode-ai/providers/translate.js similarity index 100% rename from extensions/atom-ai/providers/translate.js rename to extensions/levelcode-ai/providers/translate.js diff --git a/extensions/atom-ai/reviewSession.js b/extensions/levelcode-ai/reviewSession.js similarity index 100% rename from extensions/atom-ai/reviewSession.js rename to extensions/levelcode-ai/reviewSession.js diff --git a/extensions/atom-ai/sketch.js b/extensions/levelcode-ai/sketch.js similarity index 100% rename from extensions/atom-ai/sketch.js rename to extensions/levelcode-ai/sketch.js diff --git a/extensions/atom-ai/sketch/agentCatalog.js b/extensions/levelcode-ai/sketch/agentCatalog.js similarity index 100% rename from extensions/atom-ai/sketch/agentCatalog.js rename to extensions/levelcode-ai/sketch/agentCatalog.js diff --git a/extensions/atom-ai/sketch/graph.js b/extensions/levelcode-ai/sketch/graph.js similarity index 100% rename from extensions/atom-ai/sketch/graph.js rename to extensions/levelcode-ai/sketch/graph.js diff --git a/extensions/atom-ai/sketch/pricing.js b/extensions/levelcode-ai/sketch/pricing.js similarity index 100% rename from extensions/atom-ai/sketch/pricing.js rename to extensions/levelcode-ai/sketch/pricing.js diff --git a/extensions/atom-ai/sketch/templates.js b/extensions/levelcode-ai/sketch/templates.js similarity index 100% rename from extensions/atom-ai/sketch/templates.js rename to extensions/levelcode-ai/sketch/templates.js diff --git a/extensions/atom-ai/skills.js b/extensions/levelcode-ai/skills.js similarity index 100% rename from extensions/atom-ai/skills.js rename to extensions/levelcode-ai/skills.js diff --git a/extensions/atom-ai/skills/code-review/SKILL.md b/extensions/levelcode-ai/skills/code-review/SKILL.md similarity index 100% rename from extensions/atom-ai/skills/code-review/SKILL.md rename to extensions/levelcode-ai/skills/code-review/SKILL.md diff --git a/extensions/atom-ai/skills/focused-fix/SKILL.md b/extensions/levelcode-ai/skills/focused-fix/SKILL.md similarity index 100% rename from extensions/atom-ai/skills/focused-fix/SKILL.md rename to extensions/levelcode-ai/skills/focused-fix/SKILL.md diff --git a/extensions/atom-ai/skills/test-writing/SKILL.md b/extensions/levelcode-ai/skills/test-writing/SKILL.md similarity index 100% rename from extensions/atom-ai/skills/test-writing/SKILL.md rename to extensions/levelcode-ai/skills/test-writing/SKILL.md diff --git a/extensions/atom-ai/test/catalog.test.js b/extensions/levelcode-ai/test/catalog.test.js similarity index 100% rename from extensions/atom-ai/test/catalog.test.js rename to extensions/levelcode-ai/test/catalog.test.js diff --git a/extensions/atom-ai/test/gateway.test.js b/extensions/levelcode-ai/test/gateway.test.js similarity index 100% rename from extensions/atom-ai/test/gateway.test.js rename to extensions/levelcode-ai/test/gateway.test.js diff --git a/extensions/atom-ai/test/importVscode.test.js b/extensions/levelcode-ai/test/importVscode.test.js similarity index 100% rename from extensions/atom-ai/test/importVscode.test.js rename to extensions/levelcode-ai/test/importVscode.test.js diff --git a/extensions/atom-ai/test/providers.test.js b/extensions/levelcode-ai/test/providers.test.js similarity index 100% rename from extensions/atom-ai/test/providers.test.js rename to extensions/levelcode-ai/test/providers.test.js diff --git a/extensions/atom-ai/test/sketch.test.js b/extensions/levelcode-ai/test/sketch.test.js similarity index 100% rename from extensions/atom-ai/test/sketch.test.js rename to extensions/levelcode-ai/test/sketch.test.js diff --git a/extensions/atom-ai/test/translate.test.js b/extensions/levelcode-ai/test/translate.test.js similarity index 100% rename from extensions/atom-ai/test/translate.test.js rename to extensions/levelcode-ai/test/translate.test.js diff --git a/extensions/atom-ai/verify.js b/extensions/levelcode-ai/verify.js similarity index 100% rename from extensions/atom-ai/verify.js rename to extensions/levelcode-ai/verify.js diff --git a/extensions/atom-hackability/extension.js b/extensions/levelcode-hackability/extension.js similarity index 100% rename from extensions/atom-hackability/extension.js rename to extensions/levelcode-hackability/extension.js diff --git a/extensions/atom-hackability/keymaps.js b/extensions/levelcode-hackability/keymaps.js similarity index 100% rename from extensions/atom-hackability/keymaps.js rename to extensions/levelcode-hackability/keymaps.js diff --git a/extensions/atom-hackability/package.json b/extensions/levelcode-hackability/package.json similarity index 97% rename from extensions/atom-hackability/package.json rename to extensions/levelcode-hackability/package.json index efbf202..a31560d 100644 --- a/extensions/atom-hackability/package.json +++ b/extensions/levelcode-hackability/package.json @@ -1,5 +1,5 @@ { - "name": "atom-hackability", + "name": "levelcode-hackability", "displayName": "LevelCode Hackability", "description": "Atom-style hackability for LevelCode — a user init script that runs your own code at startup.", "publisher": "levelcode", diff --git a/extensions/atom-hackability/packages.js b/extensions/levelcode-hackability/packages.js similarity index 100% rename from extensions/atom-hackability/packages.js rename to extensions/levelcode-hackability/packages.js diff --git a/extensions/atom-npp-pack/.vscodeignore b/extensions/levelcode-npp-pack/.vscodeignore similarity index 100% rename from extensions/atom-npp-pack/.vscodeignore rename to extensions/levelcode-npp-pack/.vscodeignore diff --git a/extensions/atom-npp-pack/README.md b/extensions/levelcode-npp-pack/README.md similarity index 100% rename from extensions/atom-npp-pack/README.md rename to extensions/levelcode-npp-pack/README.md diff --git a/extensions/atom-npp-pack/bigFile.js b/extensions/levelcode-npp-pack/bigFile.js similarity index 84% rename from extensions/atom-npp-pack/bigFile.js rename to extensions/levelcode-npp-pack/bigFile.js index 68fd284..84768db 100644 --- a/extensions/atom-npp-pack/bigFile.js +++ b/extensions/levelcode-npp-pack/bigFile.js @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Atom++ — Notepad++ Pack + * LevelCode — Notepad++ Pack * Feature: Big-file mode (M1, feature 2 — headline). * * The editor engine already handles large files well: it streams them into a piece-tree @@ -22,7 +22,7 @@ let statusItem; const notified = new Set(); function cfg() { - return vscode.workspace.getConfiguration('atompp.bigFile'); + return vscode.workspace.getConfiguration('levelcode.bigFile'); } async function fileSizeBytes(uri) { @@ -47,14 +47,14 @@ async function refresh() { const mbLabel = mb >= 1024 ? `${(mb / 1024).toFixed(2)} GB` : `${mb.toFixed(0)} MB`; statusItem.text = '$(zap) Big-file mode'; - statusItem.tooltip = `Atom++: large file (${mbLabel}). Syntax highlighting and other heavy features are reduced for speed — editing, scrolling and find still work.`; + statusItem.tooltip = `LevelCode: large file (${mbLabel}). Syntax highlighting and other heavy features are reduced for speed — editing, scrolling and find still work.`; statusItem.show(); const key = ed.document.uri.toString(); if (cfg().get('notify', true) && !notified.has(key)) { notified.add(key); vscode.window.showInformationMessage( - `Atom++: opened ${path.basename(ed.document.uri.fsPath)} (${mbLabel}) in Big-file mode — some features reduced for performance.` + `LevelCode: opened ${path.basename(ed.document.uri.fsPath)} (${mbLabel}) in Big-file mode — some features reduced for performance.` ); } } @@ -69,7 +69,7 @@ function registerBigFile(context) { vscode.window.onDidChangeVisibleTextEditors(() => { refresh(); }), vscode.workspace.onDidOpenTextDocument(() => { refresh(); }), vscode.workspace.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration('atompp.bigFile')) { refresh(); } + if (e.affectsConfiguration('levelcode.bigFile')) { refresh(); } }) ); refresh(); diff --git a/extensions/atom-npp-pack/columnOps.js b/extensions/levelcode-npp-pack/columnOps.js similarity index 85% rename from extensions/atom-npp-pack/columnOps.js rename to extensions/levelcode-npp-pack/columnOps.js index 3264dbf..58c820b 100644 --- a/extensions/atom-npp-pack/columnOps.js +++ b/extensions/levelcode-npp-pack/columnOps.js @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Atom++ — Notepad++ Pack + * LevelCode — Notepad++ Pack * Feature: Column editor (M1, feature 4) — insert incrementing numbers + column fill. * * Use a box selection (Shift+Alt+drag) or multiple cursors, then "Insert Incrementing @@ -61,7 +61,7 @@ function formatNum(n, spec) { async function insertIncrementingNumbers() { const editor = vscode.window.activeTextEditor; - if (!editor) { vscode.window.showWarningMessage('Atom++: open a text editor first.'); return; } + if (!editor) { vscode.window.showWarningMessage('LevelCode: open a text editor first.'); return; } const input = await vscode.window.showInputBox({ title: 'Insert Incrementing Numbers', @@ -82,25 +82,25 @@ async function insertIncrementingNumbers() { eb.replace(sel, formatNum(spec.start + i * spec.step, spec)); }); }); - vscode.window.setStatusBarMessage(`$(list-ordered) Atom++: inserted ${selections.length} numbers`, 2000); + vscode.window.setStatusBarMessage(`$(list-ordered) LevelCode: inserted ${selections.length} numbers`, 2000); } async function columnFill() { const editor = vscode.window.activeTextEditor; - if (!editor) { vscode.window.showWarningMessage('Atom++: open a text editor first.'); return; } + if (!editor) { vscode.window.showWarningMessage('LevelCode: open a text editor first.'); return; } const text = await vscode.window.showInputBox({ title: 'Column Fill', prompt: 'Text to insert at every cursor / selection' }); if (text === undefined) { return; } await editor.edit((eb) => { for (const sel of editor.selections) { eb.replace(sel, text); } }); - vscode.window.setStatusBarMessage(`$(symbol-string) Atom++: filled ${editor.selections.length} positions`, 2000); + vscode.window.setStatusBarMessage(`$(symbol-string) LevelCode: filled ${editor.selections.length} positions`, 2000); } /** @param {vscode.ExtensionContext} context */ function registerColumnOps(context) { context.subscriptions.push( - vscode.commands.registerCommand('atompp.column.insertNumbers', insertIncrementingNumbers), - vscode.commands.registerCommand('atompp.column.fill', columnFill) + vscode.commands.registerCommand('levelcode.column.insertNumbers', insertIncrementingNumbers), + vscode.commands.registerCommand('levelcode.column.fill', columnFill) ); } diff --git a/extensions/atom-npp-pack/encodingEol.js b/extensions/levelcode-npp-pack/encodingEol.js similarity index 70% rename from extensions/atom-npp-pack/encodingEol.js rename to extensions/levelcode-npp-pack/encodingEol.js index d9167ae..81e8ec2 100644 --- a/extensions/atom-npp-pack/encodingEol.js +++ b/extensions/levelcode-npp-pack/encodingEol.js @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Atom++ — Notepad++ Pack + * LevelCode — Notepad++ Pack * Feature: Encoding & line-ending controls (M1, feature 3). * * VS Code already shows encoding/EOL in the status bar, but converting line endings there @@ -25,29 +25,29 @@ function refreshEol() { if (!ed) { eolStatus.hide(); return; } eolStatus.text = isCRLF(ed.document) ? '$(arrow-swap) CRLF · Windows' : '$(arrow-swap) LF · macOS/Unix'; eolStatus.tooltip = isCRLF(ed.document) - ? 'Atom++: Windows line endings (CRLF). Click to switch to macOS/Unix (LF).' - : 'Atom++: macOS/Unix line endings (LF). Click to switch to Windows (CRLF).'; - eolStatus.command = 'atompp.eol.toggle'; + ? 'LevelCode: Windows line endings (CRLF). Click to switch to macOS/Unix (LF).' + : 'LevelCode: macOS/Unix line endings (LF). Click to switch to Windows (CRLF).'; + eolStatus.command = 'levelcode.eol.toggle'; eolStatus.show(); } /** @param {vscode.EndOfLine} target */ async function setEol(target) { const ed = vscode.window.activeTextEditor; - if (!ed) { vscode.window.showWarningMessage('Atom++: open a text editor first.'); return; } + if (!ed) { vscode.window.showWarningMessage('LevelCode: open a text editor first.'); return; } if (ed.document.eol === target) { - vscode.window.setStatusBarMessage('Atom++: line endings already set', 1500); + vscode.window.setStatusBarMessage('LevelCode: line endings already set', 1500); return; } await ed.edit((eb) => eb.setEndOfLine(target)); refreshEol(); vscode.window.setStatusBarMessage( - `$(check) Atom++: converted line endings to ${target === vscode.EndOfLine.CRLF ? 'CRLF' : 'LF'}`, 2000); + `$(check) LevelCode: converted line endings to ${target === vscode.EndOfLine.CRLF ? 'CRLF' : 'LF'}`, 2000); } function toggleEol() { const ed = vscode.window.activeTextEditor; - if (!ed) { vscode.window.showWarningMessage('Atom++: open a text editor first.'); return; } + if (!ed) { vscode.window.showWarningMessage('LevelCode: open a text editor first.'); return; } return setEol(isCRLF(ed.document) ? vscode.EndOfLine.LF : vscode.EndOfLine.CRLF); } @@ -57,11 +57,11 @@ function registerEncodingEol(context) { context.subscriptions.push(eolStatus); context.subscriptions.push( - vscode.commands.registerCommand('atompp.eol.toLF', () => setEol(vscode.EndOfLine.LF)), - vscode.commands.registerCommand('atompp.eol.toCRLF', () => setEol(vscode.EndOfLine.CRLF)), - vscode.commands.registerCommand('atompp.eol.toggle', toggleEol), + vscode.commands.registerCommand('levelcode.eol.toLF', () => setEol(vscode.EndOfLine.LF)), + vscode.commands.registerCommand('levelcode.eol.toCRLF', () => setEol(vscode.EndOfLine.CRLF)), + vscode.commands.registerCommand('levelcode.eol.toggle', toggleEol), // Built-in encoding picker (Reopen with Encoding / Save with Encoding). - vscode.commands.registerCommand('atompp.encoding.change', + vscode.commands.registerCommand('levelcode.encoding.change', () => vscode.commands.executeCommand('workbench.action.editor.changeEncoding')) ); diff --git a/extensions/atom-npp-pack/extension.js b/extensions/levelcode-npp-pack/extension.js similarity index 79% rename from extensions/atom-npp-pack/extension.js rename to extensions/levelcode-npp-pack/extension.js index 9c2e011..cd626c2 100644 --- a/extensions/atom-npp-pack/extension.js +++ b/extensions/levelcode-npp-pack/extension.js @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Atom++ — Notepad++ Pack + * LevelCode — Notepad++ Pack * Feature: Macro record & replay (M1, feature 1). * * Recording model (reliable within the extension API): @@ -7,7 +7,7 @@ * recording, and forwarding to `default:type` so insertion still happens. * - Cursor moves / deletions / newline / tab are captured via thin "recordable" * commands that are bound to the normal keys ONLY while the - * `atompp.macroRecording` context is true (see keybindings in package.json), + * `levelcode.macroRecording` context is true (see keybindings in package.json), * so normal editing is never disturbed when not recording. * * Replay re-dispatches the recorded steps at the current cursor. Inserts use @@ -24,9 +24,9 @@ const { registerColumnOps } = require('./columnOps'); const { registerEncodingEol } = require('./encodingEol'); const { registerBigFile } = require('./bigFile'); -const CTX_RECORDING = 'atompp.macroRecording'; -const KEY_LAST = 'atompp.macros.last'; -const KEY_SAVED = 'atompp.macros.saved'; +const CTX_RECORDING = 'levelcode.macroRecording'; +const KEY_LAST = 'levelcode.macros.last'; +const KEY_SAVED = 'levelcode.macros.saved'; /** Recordable cursor/delete commands: our command id suffix -> built-in command. */ const RECORDABLE_COMMANDS = { @@ -57,7 +57,7 @@ let statusItem; let ctx; function cfg() { - return vscode.workspace.getConfiguration('atompp.macros'); + return vscode.workspace.getConfiguration('levelcode.macros'); } function setRecordingContext(on) { @@ -67,7 +67,7 @@ function setRecordingContext(on) { function updateStatus() { if (recording) { statusItem.text = `$(record) Recording macro (${currentSteps.length})`; - statusItem.tooltip = 'Atom++: recording a macro — press Cmd+Shift+R to stop'; + statusItem.tooltip = 'LevelCode: recording a macro — press Cmd+Shift+R to stop'; statusItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); statusItem.show(); } else { @@ -79,7 +79,7 @@ function updateStatus() { function pushStep(step) { const max = cfg().get('maxSteps', 5000); if (currentSteps.length >= max) { - vscode.window.showWarningMessage(`Atom++: macro hit the ${max}-step cap; recording stopped.`); + vscode.window.showWarningMessage(`LevelCode: macro hit the ${max}-step cap; recording stopped.`); stopRecording(); return; } @@ -104,7 +104,7 @@ function startRecording() { } catch (e) { typeOverride = undefined; vscode.window.showWarningMessage( - 'Atom++: could not capture typed text (another extension owns `type`). Cursor moves and deletes will still record.' + 'LevelCode: could not capture typed text (another extension owns `type`). Cursor moves and deletes will still record.' ); } @@ -121,9 +121,9 @@ function stopRecording() { if (currentSteps.length > 0) { ctx.globalState.update(KEY_LAST, currentSteps); - vscode.window.setStatusBarMessage(`$(check) Atom++: macro recorded (${currentSteps.length} steps)`, 2500); + vscode.window.setStatusBarMessage(`$(check) LevelCode: macro recorded (${currentSteps.length} steps)`, 2500); } else { - vscode.window.setStatusBarMessage('Atom++: empty macro (nothing recorded)', 2000); + vscode.window.setStatusBarMessage('LevelCode: empty macro (nothing recorded)', 2000); } } @@ -142,12 +142,12 @@ async function applyInsert(editor, text) { /** @param {Step[]} steps @param {number} times */ async function replay(steps, times) { if (!steps || steps.length === 0) { - vscode.window.showInformationMessage('Atom++: no macro to play. Record one with Cmd+Shift+R.'); + vscode.window.showInformationMessage('LevelCode: no macro to play. Record one with Cmd+Shift+R.'); return; } const editor = vscode.window.activeTextEditor; if (!editor) { - vscode.window.showWarningMessage('Atom++: open a text editor to play a macro.'); + vscode.window.showWarningMessage('LevelCode: open a text editor to play a macro.'); return; } if (recording) { stopRecording(); } @@ -165,7 +165,7 @@ async function replay(steps, times) { } } } catch (e) { - vscode.window.showErrorMessage('Atom++: macro playback failed: ' + (e && e.message ? e.message : String(e))); + vscode.window.showErrorMessage('LevelCode: macro playback failed: ' + (e && e.message ? e.message : String(e))); } finally { replaying = false; } @@ -185,7 +185,7 @@ async function playLast() { async function playNTimes() { const last = getLast(); - if (!last) { vscode.window.showInformationMessage('Atom++: no macro recorded yet.'); return; } + if (!last) { vscode.window.showInformationMessage('LevelCode: no macro recorded yet.'); return; } const maxN = cfg().get('maxReplayCount', 100000); const input = await vscode.window.showInputBox({ prompt: 'Play the last macro how many times?', @@ -203,19 +203,19 @@ async function playNTimes() { async function saveLast() { const last = getLast(); - if (!last) { vscode.window.showInformationMessage('Atom++: no macro to save yet.'); return; } + if (!last) { vscode.window.showInformationMessage('LevelCode: no macro to save yet.'); return; } const name = await vscode.window.showInputBox({ prompt: 'Name this macro', placeHolder: 'e.g. comment-line' }); if (!name) { return; } const saved = getSaved(); saved[name] = last; await ctx.globalState.update(KEY_SAVED, saved); - vscode.window.setStatusBarMessage(`$(save) Atom++: saved macro “${name}”`, 2500); + vscode.window.setStatusBarMessage(`$(save) LevelCode: saved macro “${name}”`, 2500); } async function runSaved() { const saved = getSaved(); const names = Object.keys(saved); - if (names.length === 0) { vscode.window.showInformationMessage('Atom++: no saved macros yet.'); return; } + if (names.length === 0) { vscode.window.showInformationMessage('LevelCode: no saved macros yet.'); return; } const pick = await vscode.window.showQuickPick(names, { placeHolder: 'Run which saved macro?' }); if (!pick) { return; } await ctx.globalState.update(KEY_LAST, saved[pick]); // make it the "last" too @@ -225,12 +225,12 @@ async function runSaved() { async function deleteSaved() { const saved = getSaved(); const names = Object.keys(saved); - if (names.length === 0) { vscode.window.showInformationMessage('Atom++: no saved macros to delete.'); return; } + if (names.length === 0) { vscode.window.showInformationMessage('LevelCode: no saved macros to delete.'); return; } const pick = await vscode.window.showQuickPick(names, { placeHolder: 'Delete which saved macro?' }); if (!pick) { return; } delete saved[pick]; await ctx.globalState.update(KEY_SAVED, saved); - vscode.window.setStatusBarMessage(`Atom++: deleted macro “${pick}”`, 2000); + vscode.window.setStatusBarMessage(`LevelCode: deleted macro “${pick}”`, 2000); } function activate(context) { @@ -240,16 +240,16 @@ function activate(context) { const reg = (id, fn) => context.subscriptions.push(vscode.commands.registerCommand(id, fn)); - reg('atompp.macro.toggleRecording', toggleRecording); - reg('atompp.macro.play', playLast); - reg('atompp.macro.playNTimes', playNTimes); - reg('atompp.macro.saveLast', saveLast); - reg('atompp.macro.runSaved', runSaved); - reg('atompp.macro.deleteSaved', deleteSaved); + reg('levelcode.macro.toggleRecording', toggleRecording); + reg('levelcode.macro.play', playLast); + reg('levelcode.macro.playNTimes', playNTimes); + reg('levelcode.macro.saveLast', saveLast); + reg('levelcode.macro.runSaved', runSaved); + reg('levelcode.macro.deleteSaved', deleteSaved); // Recordable cursor/delete wrappers: record (if recording) then run the built-in. for (const [suffix, builtin] of Object.entries(RECORDABLE_COMMANDS)) { - reg('atompp.macro.rec.' + suffix, async () => { + reg('levelcode.macro.rec.' + suffix, async () => { if (recording) { pushStep({ k: 'cmd', id: builtin }); } await vscode.commands.executeCommand(builtin); }); @@ -257,11 +257,11 @@ function activate(context) { // Newline / tab while recording: record an insert and perform it via default:type // (bypasses our `type` override so it isn't double-recorded). - reg('atompp.macro.rec.insertNewline', async () => { + reg('levelcode.macro.rec.insertNewline', async () => { if (recording) { pushStep({ k: 'insert', text: '\n' }); } await vscode.commands.executeCommand('default:type', { text: '\n' }); }); - reg('atompp.macro.rec.insertTab', async () => { + reg('levelcode.macro.rec.insertTab', async () => { if (recording) { pushStep({ k: 'insert', text: '\t' }); } await vscode.commands.executeCommand('default:type', { text: '\t' }); }); diff --git a/extensions/atom-npp-pack/fileOps.js b/extensions/levelcode-npp-pack/fileOps.js similarity index 85% rename from extensions/atom-npp-pack/fileOps.js rename to extensions/levelcode-npp-pack/fileOps.js index 819f660..fe5b5e6 100644 --- a/extensions/atom-npp-pack/fileOps.js +++ b/extensions/levelcode-npp-pack/fileOps.js @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Atom++ — Notepad++ Pack + * LevelCode — Notepad++ Pack * Feature: Duplicate file/folder from the Explorer context menu (and Cmd+D in the tree). * * Mirrors Finder/Notepad++ "Duplicate": creates "name copy.ext" next to the original, @@ -53,7 +53,7 @@ async function duplicate(clickedUri, selectedUris) { } targets = targets.filter(u => u && u.scheme === 'file'); if (targets.length === 0) { - vscode.window.showInformationMessage('Atom++: nothing to duplicate (select a file or folder in the Explorer).'); + vscode.window.showInformationMessage('LevelCode: nothing to duplicate (select a file or folder in the Explorer).'); return; } @@ -66,7 +66,7 @@ async function duplicate(clickedUri, selectedUris) { lastNew = target; } catch (e) { vscode.window.showErrorMessage( - `Atom++: could not duplicate ${path.basename(src.fsPath)}: ${e && e.message ? e.message : String(e)}` + `LevelCode: could not duplicate ${path.basename(src.fsPath)}: ${e && e.message ? e.message : String(e)}` ); } } @@ -76,7 +76,7 @@ async function duplicate(clickedUri, selectedUris) { try { await vscode.commands.executeCommand('revealInExplorer', lastNew); } catch { /* noop */ } const label = path.basename(lastNew.fsPath); vscode.window.setStatusBarMessage( - `$(files) Atom++: duplicated → ${label}${targets.length > 1 ? ` (+${targets.length - 1} more)` : ''}`, + `$(files) LevelCode: duplicated → ${label}${targets.length > 1 ? ` (+${targets.length - 1} more)` : ''}`, 2500 ); } @@ -85,7 +85,7 @@ async function duplicate(clickedUri, selectedUris) { /** @param {vscode.ExtensionContext} context */ function registerFileOps(context) { context.subscriptions.push( - vscode.commands.registerCommand('atompp.file.duplicate', duplicate) + vscode.commands.registerCommand('levelcode.file.duplicate', duplicate) ); } diff --git a/extensions/atom-npp-pack/lineOps.js b/extensions/levelcode-npp-pack/lineOps.js similarity index 71% rename from extensions/atom-npp-pack/lineOps.js rename to extensions/levelcode-npp-pack/lineOps.js index ce08a5e..7397098 100644 --- a/extensions/atom-npp-pack/lineOps.js +++ b/extensions/levelcode-npp-pack/lineOps.js @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Atom++ — Notepad++ Pack + * LevelCode — Notepad++ Pack * Feature: Line operations (M1, feature 5) — the everyday Notepad++/TextFX set. * * Each line op works on the selected lines (selection expanded to whole lines), or the @@ -33,17 +33,17 @@ function eol(doc) { /** Run a lines-array transform over the target range as one edit. */ async function transformLines(editorArg, fn, opName) { const editor = editorArg || vscode.window.activeTextEditor; - if (!editor) { vscode.window.showWarningMessage('Atom++: open a text editor first.'); return; } + if (!editor) { vscode.window.showWarningMessage('LevelCode: open a text editor first.'); return; } const doc = editor.document; const range = lineRange(editor); const lines = doc.getText(range).split(/\r?\n/); const out = fn(lines); if (out.join('\n') === lines.join('\n')) { - vscode.window.setStatusBarMessage(`Atom++: ${opName} — no change`, 1500); + vscode.window.setStatusBarMessage(`LevelCode: ${opName} — no change`, 1500); return; } await editor.edit((eb) => eb.replace(range, out.join(eol(doc)))); - vscode.window.setStatusBarMessage(`$(check) Atom++: ${opName} (${lines.length}→${out.length} lines)`, 2000); + vscode.window.setStatusBarMessage(`$(check) LevelCode: ${opName} (${lines.length}→${out.length} lines)`, 2000); } // --- sorting --------------------------------------------------------------- @@ -89,7 +89,7 @@ const casers = { async function convertCase(mode) { const editor = vscode.window.activeTextEditor; - if (!editor) { vscode.window.showWarningMessage('Atom++: open a text editor first.'); return; } + if (!editor) { vscode.window.showWarningMessage('LevelCode: open a text editor first.'); return; } const fn = casers[mode]; await editor.edit((eb) => { for (const sel of editor.selections) { @@ -107,32 +107,32 @@ function registerLineOps(context) { const reg = (id, fn) => context.subscriptions.push(vscode.commands.registerCommand(id, fn)); // Sorting - reg('atompp.lines.sortAsc', () => transformLines(null, sorters.asc, 'sort ascending')); - reg('atompp.lines.sortDesc', () => transformLines(null, sorters.desc, 'sort descending')); - reg('atompp.lines.sortCaseInsensitive', () => transformLines(null, sorters.ci, 'sort (case-insensitive)')); - reg('atompp.lines.sortNumeric', () => transformLines(null, sorters.numeric, 'sort numerically')); + reg('levelcode.lines.sortAsc', () => transformLines(null, sorters.asc, 'sort ascending')); + reg('levelcode.lines.sortDesc', () => transformLines(null, sorters.desc, 'sort descending')); + reg('levelcode.lines.sortCaseInsensitive', () => transformLines(null, sorters.ci, 'sort (case-insensitive)')); + reg('levelcode.lines.sortNumeric', () => transformLines(null, sorters.numeric, 'sort numerically')); // Dedup / blank lines / order - reg('atompp.lines.removeDuplicates', () => transformLines(null, (a) => { + reg('levelcode.lines.removeDuplicates', () => transformLines(null, (a) => { const seen = new Set(); const out = []; for (const l of a) { if (!seen.has(l)) { seen.add(l); out.push(l); } } return out; }, 'remove duplicate lines')); - reg('atompp.lines.removeDuplicatesAdjacent', () => transformLines(null, (a) => + reg('levelcode.lines.removeDuplicatesAdjacent', () => transformLines(null, (a) => a.filter((l, i) => i === 0 || l !== a[i - 1]), 'remove consecutive duplicates')); - reg('atompp.lines.removeEmpty', () => transformLines(null, (a) => + reg('levelcode.lines.removeEmpty', () => transformLines(null, (a) => a.filter((l) => l.trim().length > 0), 'remove empty lines')); - reg('atompp.lines.reverse', () => transformLines(null, (a) => [...a].reverse(), 'reverse lines')); + reg('levelcode.lines.reverse', () => transformLines(null, (a) => [...a].reverse(), 'reverse lines')); // Aliases to robust built-ins (selection-aware already) - reg('atompp.lines.join', () => vscode.commands.executeCommand('editor.action.joinLines')); - reg('atompp.lines.trimTrailing', () => vscode.commands.executeCommand('editor.action.trimTrailingWhitespace')); + reg('levelcode.lines.join', () => vscode.commands.executeCommand('editor.action.joinLines')); + reg('levelcode.lines.trimTrailing', () => vscode.commands.executeCommand('editor.action.trimTrailingWhitespace')); // Case - reg('atompp.case.upper', () => convertCase('upper')); - reg('atompp.case.lower', () => convertCase('lower')); - reg('atompp.case.title', () => convertCase('title')); - reg('atompp.case.toggle', () => convertCase('toggle')); + reg('levelcode.case.upper', () => convertCase('upper')); + reg('levelcode.case.lower', () => convertCase('lower')); + reg('levelcode.case.title', () => convertCase('title')); + reg('levelcode.case.toggle', () => convertCase('toggle')); } module.exports = { registerLineOps }; diff --git a/extensions/levelcode-npp-pack/package.json b/extensions/levelcode-npp-pack/package.json new file mode 100644 index 0000000..9a8cec2 --- /dev/null +++ b/extensions/levelcode-npp-pack/package.json @@ -0,0 +1,404 @@ +{ + "name": "levelcode-npp-pack", + "displayName": "LevelCode Notepad++ Pack", + "description": "Notepad++-style power editing for LevelCode: macros, line operations, column extras.", + "publisher": "levelcode", + "version": "0.1.0", + "license": "MIT", + "engines": { + "vscode": "^1.126.0" + }, + "categories": [ + "Other" + ], + "capabilities": { + "virtualWorkspaces": true, + "untrustedWorkspaces": { + "supported": true + } + }, + "activationEvents": [ + "onStartupFinished" + ], + "main": "./extension.js", + "contributes": { + "commands": [ + { + "command": "levelcode.macro.toggleRecording", + "title": "Macro: Start/Stop Recording", + "category": "LevelCode" + }, + { + "command": "levelcode.macro.play", + "title": "Macro: Play Last", + "category": "LevelCode" + }, + { + "command": "levelcode.macro.playNTimes", + "title": "Macro: Play Last N Times…", + "category": "LevelCode" + }, + { + "command": "levelcode.macro.saveLast", + "title": "Macro: Save Last Macro…", + "category": "LevelCode" + }, + { + "command": "levelcode.macro.runSaved", + "title": "Macro: Run Saved Macro…", + "category": "LevelCode" + }, + { + "command": "levelcode.macro.deleteSaved", + "title": "Macro: Delete Saved Macro…", + "category": "LevelCode" + }, + { + "command": "levelcode.file.duplicate", + "title": "Duplicate", + "category": "LevelCode" + }, + { + "command": "levelcode.lines.sortAsc", + "title": "Lines: Sort Ascending", + "category": "LevelCode" + }, + { + "command": "levelcode.lines.sortDesc", + "title": "Lines: Sort Descending", + "category": "LevelCode" + }, + { + "command": "levelcode.lines.sortCaseInsensitive", + "title": "Lines: Sort (Case-Insensitive)", + "category": "LevelCode" + }, + { + "command": "levelcode.lines.sortNumeric", + "title": "Lines: Sort Numerically", + "category": "LevelCode" + }, + { + "command": "levelcode.lines.removeDuplicates", + "title": "Lines: Remove Duplicate Lines", + "category": "LevelCode" + }, + { + "command": "levelcode.lines.removeDuplicatesAdjacent", + "title": "Lines: Remove Consecutive Duplicate Lines", + "category": "LevelCode" + }, + { + "command": "levelcode.lines.removeEmpty", + "title": "Lines: Remove Empty Lines", + "category": "LevelCode" + }, + { + "command": "levelcode.lines.reverse", + "title": "Lines: Reverse", + "category": "LevelCode" + }, + { + "command": "levelcode.lines.join", + "title": "Lines: Join", + "category": "LevelCode" + }, + { + "command": "levelcode.lines.trimTrailing", + "title": "Lines: Trim Trailing Whitespace", + "category": "LevelCode" + }, + { + "command": "levelcode.case.upper", + "title": "Convert to UPPERCASE", + "category": "LevelCode" + }, + { + "command": "levelcode.case.lower", + "title": "Convert to lowercase", + "category": "LevelCode" + }, + { + "command": "levelcode.case.title", + "title": "Convert to Title Case", + "category": "LevelCode" + }, + { + "command": "levelcode.case.toggle", + "title": "Toggle Case", + "category": "LevelCode" + }, + { + "command": "levelcode.column.insertNumbers", + "title": "Column: Insert Incrementing Numbers…", + "category": "LevelCode" + }, + { + "command": "levelcode.column.fill", + "title": "Column: Fill With Text…", + "category": "LevelCode" + }, + { + "command": "levelcode.eol.toLF", + "title": "Convert Line Endings to macOS/Unix (LF)", + "category": "LevelCode" + }, + { + "command": "levelcode.eol.toCRLF", + "title": "Convert Line Endings to Windows (CRLF)", + "category": "LevelCode" + }, + { + "command": "levelcode.eol.toggle", + "title": "Toggle Line Endings (Windows ⇄ macOS/Unix)", + "category": "LevelCode" + }, + { + "command": "levelcode.encoding.change", + "title": "Change File Encoding…", + "category": "LevelCode" + } + ], + "submenus": [ + { + "id": "levelcode.lineOps", + "label": "Line Operations" + }, + { + "id": "levelcode.columnOps", + "label": "Column Editor" + }, + { + "id": "levelcode.encodingOps", + "label": "Encoding / Line Endings" + } + ], + "menus": { + "explorer/context": [ + { + "command": "levelcode.file.duplicate", + "group": "5_cutcopypaste@11", + "when": "!explorerResourceIsRoot" + } + ], + "editor/context": [ + { + "submenu": "levelcode.lineOps", + "group": "1_modification@100", + "when": "editorTextFocus" + }, + { + "submenu": "levelcode.columnOps", + "group": "1_modification@101", + "when": "editorTextFocus" + }, + { + "submenu": "levelcode.encodingOps", + "group": "1_modification@102", + "when": "editorTextFocus" + } + ], + "levelcode.columnOps": [ + { + "command": "levelcode.column.insertNumbers", + "group": "1_column@1" + }, + { + "command": "levelcode.column.fill", + "group": "1_column@2" + } + ], + "levelcode.encodingOps": [ + { + "command": "levelcode.eol.toLF", + "group": "1_eol@1" + }, + { + "command": "levelcode.eol.toCRLF", + "group": "1_eol@2" + }, + { + "command": "levelcode.eol.toggle", + "group": "1_eol@3" + }, + { + "command": "levelcode.encoding.change", + "group": "2_encoding@1" + } + ], + "levelcode.lineOps": [ + { + "command": "levelcode.lines.sortAsc", + "group": "1_sort@1" + }, + { + "command": "levelcode.lines.sortDesc", + "group": "1_sort@2" + }, + { + "command": "levelcode.lines.sortCaseInsensitive", + "group": "1_sort@3" + }, + { + "command": "levelcode.lines.sortNumeric", + "group": "1_sort@4" + }, + { + "command": "levelcode.lines.removeDuplicates", + "group": "2_clean@1" + }, + { + "command": "levelcode.lines.removeDuplicatesAdjacent", + "group": "2_clean@2" + }, + { + "command": "levelcode.lines.removeEmpty", + "group": "2_clean@3" + }, + { + "command": "levelcode.lines.trimTrailing", + "group": "2_clean@4" + }, + { + "command": "levelcode.lines.reverse", + "group": "3_order@1" + }, + { + "command": "levelcode.lines.join", + "group": "3_order@2" + }, + { + "command": "levelcode.case.upper", + "group": "4_case@1" + }, + { + "command": "levelcode.case.lower", + "group": "4_case@2" + }, + { + "command": "levelcode.case.title", + "group": "4_case@3" + }, + { + "command": "levelcode.case.toggle", + "group": "4_case@4" + } + ] + }, + "keybindings": [ + { + "command": "levelcode.macro.toggleRecording", + "key": "cmd+shift+r", + "when": "editorTextFocus" + }, + { + "command": "levelcode.macro.play", + "key": "cmd+alt+p", + "when": "editorTextFocus && !levelcode.macroRecording" + }, + { + "command": "levelcode.macro.rec.cursorDown", + "key": "down", + "when": "levelcode.macroRecording && editorTextFocus && !suggestWidgetVisible" + }, + { + "command": "levelcode.macro.rec.cursorUp", + "key": "up", + "when": "levelcode.macroRecording && editorTextFocus && !suggestWidgetVisible" + }, + { + "command": "levelcode.macro.rec.cursorLeft", + "key": "left", + "when": "levelcode.macroRecording && editorTextFocus" + }, + { + "command": "levelcode.macro.rec.cursorRight", + "key": "right", + "when": "levelcode.macroRecording && editorTextFocus" + }, + { + "command": "levelcode.macro.rec.cursorHome", + "key": "home", + "when": "levelcode.macroRecording && editorTextFocus" + }, + { + "command": "levelcode.macro.rec.cursorEnd", + "key": "end", + "when": "levelcode.macroRecording && editorTextFocus" + }, + { + "command": "levelcode.macro.rec.cursorWordLeft", + "key": "alt+left", + "when": "levelcode.macroRecording && editorTextFocus" + }, + { + "command": "levelcode.macro.rec.cursorWordRight", + "key": "alt+right", + "when": "levelcode.macroRecording && editorTextFocus" + }, + { + "command": "levelcode.macro.rec.deleteLeft", + "key": "backspace", + "when": "levelcode.macroRecording && editorTextFocus" + }, + { + "command": "levelcode.macro.rec.deleteRight", + "key": "delete", + "when": "levelcode.macroRecording && editorTextFocus" + }, + { + "command": "levelcode.macro.rec.deleteWordLeft", + "key": "alt+backspace", + "when": "levelcode.macroRecording && editorTextFocus" + }, + { + "command": "levelcode.macro.rec.insertNewline", + "key": "enter", + "when": "levelcode.macroRecording && editorTextFocus && !suggestWidgetVisible && !inSnippetMode" + }, + { + "command": "levelcode.macro.rec.insertTab", + "key": "tab", + "when": "levelcode.macroRecording && editorTextFocus && !suggestWidgetVisible && !inSnippetMode" + }, + { + "command": "levelcode.file.duplicate", + "key": "cmd+d", + "when": "filesExplorerFocus && !inputFocus && !explorerResourceIsRoot" + } + ], + "configurationDefaults": { + "workbench.editorLargeFileConfirmation": 2048, + "chat.commandCenter.enabled": false, + "chat.disableAIFeatures": true + }, + "configuration": { + "title": "LevelCode Notepad++ Pack", + "properties": { + "levelcode.macros.maxSteps": { + "type": "number", + "default": 5000, + "minimum": 100, + "description": "Safety cap on the number of steps recorded in a single macro." + }, + "levelcode.macros.maxReplayCount": { + "type": "number", + "default": 100000, + "minimum": 1, + "description": "Maximum repeat count allowed when playing a macro N times." + }, + "levelcode.bigFile.thresholdMB": { + "type": "number", + "default": 256, + "minimum": 1, + "description": "Show the Big-file mode indicator when the active file is at least this many megabytes." + }, + "levelcode.bigFile.notify": { + "type": "boolean", + "default": true, + "description": "Show a one-time notification when a file opens in Big-file mode." + } + } + } + } +} diff --git a/extensions/atom-themes/package.json b/extensions/levelcode-themes/package.json similarity index 95% rename from extensions/atom-themes/package.json rename to extensions/levelcode-themes/package.json index a51ca7c..63558fe 100644 --- a/extensions/atom-themes/package.json +++ b/extensions/levelcode-themes/package.json @@ -1,5 +1,5 @@ { - "name": "atom-themes", + "name": "levelcode-themes", "displayName": "LevelCode Themes", "description": "Signature LevelCode themes — One Dark and One Light, adapted from Atom's iconic MIT palette.", "publisher": "levelcode", diff --git a/extensions/atom-themes/themes/atom-one-dark.json b/extensions/levelcode-themes/themes/atom-one-dark.json similarity index 100% rename from extensions/atom-themes/themes/atom-one-dark.json rename to extensions/levelcode-themes/themes/atom-one-dark.json diff --git a/extensions/atom-themes/themes/atom-one-light.json b/extensions/levelcode-themes/themes/atom-one-light.json similarity index 100% rename from extensions/atom-themes/themes/atom-one-light.json rename to extensions/levelcode-themes/themes/atom-one-light.json diff --git a/extensions/atom-updater/extension.js b/extensions/levelcode-updater/extension.js similarity index 100% rename from extensions/atom-updater/extension.js rename to extensions/levelcode-updater/extension.js diff --git a/extensions/atom-updater/package.json b/extensions/levelcode-updater/package.json similarity index 98% rename from extensions/atom-updater/package.json rename to extensions/levelcode-updater/package.json index 9891bbe..6a06a98 100644 --- a/extensions/atom-updater/package.json +++ b/extensions/levelcode-updater/package.json @@ -1,5 +1,5 @@ { - "name": "atom-updater", + "name": "levelcode-updater", "displayName": "LevelCode Updater (notify-only)", "description": "Periodically checks for new LevelCode releases and notifies you with a download link. Notify-only — never auto-applies, so it works on any build with no code-signing.", "publisher": "levelcode", diff --git a/extensions/atom-updater/test/update.test.js b/extensions/levelcode-updater/test/update.test.js similarity index 96% rename from extensions/atom-updater/test/update.test.js rename to extensions/levelcode-updater/test/update.test.js index 11cef06..536b569 100644 --- a/extensions/atom-updater/test/update.test.js +++ b/extensions/levelcode-updater/test/update.test.js @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Unit tests for extensions/atom-updater/update.js — run: node update.test.js + * Unit tests for extensions/levelcode-updater/update.js — run: node update.test.js *--------------------------------------------------------------------------------------------*/ // @ts-check 'use strict'; diff --git a/extensions/atom-updater/update.js b/extensions/levelcode-updater/update.js similarity index 100% rename from extensions/atom-updater/update.js rename to extensions/levelcode-updater/update.js diff --git a/scripts/strip-unreleased.mjs b/scripts/strip-unreleased.mjs index 9b8b305..3e6eab4 100644 --- a/scripts/strip-unreleased.mjs +++ b/scripts/strip-unreleased.mjs @@ -6,7 +6,7 @@ * Today this hides **LevelCode Sync**: the managed cloud host isn't deployed and the store URL is a * localhost dev placeholder, so a public user clicking "Set up Sync" would just fail. We remove * its user-facing surfaces: - * 1. the atom-sync extension (its commands + the `levelcode` sync auth provider) + * 1. the levelcode-sync extension (its commands + the `levelcode` sync auth provider) * 2. product.json configurationSync.store (the dead localhost sync endpoint) * 3. the "Sync" step in the LevelCode AI Welcome walkthrough * (The Customize panel's Sync section hides itself at runtime when the sync command is absent.) @@ -25,13 +25,13 @@ if (!fs.existsSync(appDir)) { const done = []; const missing = []; -// 1. Remove the atom-sync extension from the built app. -const syncExt = path.join(appDir, "extensions", "atom-sync"); +// 1. Remove the levelcode-sync extension from the built app. +const syncExt = path.join(appDir, "extensions", "levelcode-sync"); if (fs.existsSync(syncExt)) { fs.rmSync(syncExt, { recursive: true, force: true }); - done.push("extensions/atom-sync (commands + `levelcode` sync auth provider)"); + done.push("extensions/levelcode-sync (commands + `levelcode` sync auth provider)"); } else { - missing.push("extensions/atom-sync"); + missing.push("extensions/levelcode-sync"); } // 2. Drop the configurationSync.store override from product.json (the dead localhost endpoint). @@ -59,7 +59,7 @@ try { } // 3. Remove the "Sync" step from the LevelCode AI Welcome walkthrough so it can't dangle a dead link. -const aiPkgPath = path.join(appDir, "extensions", "atom-ai", "package.json"); +const aiPkgPath = path.join(appDir, "extensions", "levelcode-ai", "package.json"); try { const pkg = JSON.parse(fs.readFileSync(aiPkgPath, "utf8")); const walkthroughs = ((pkg.contributes || {}).walkthroughs) || []; @@ -74,12 +74,12 @@ try { } if (removed) { fs.writeFileSync(aiPkgPath, JSON.stringify(pkg, null, "\t") + "\n"); - done.push(`atom-ai Welcome walkthrough: removed ${removed} Sync step`); + done.push(`levelcode-ai Welcome walkthrough: removed ${removed} Sync step`); } else { - missing.push("atom-ai walkthrough Sync step"); + missing.push("levelcode-ai walkthrough Sync step"); } } catch (e) { - missing.push("atom-ai package.json (" + ((e && e.message) || e) + ")"); + missing.push("levelcode-ai package.json (" + ((e && e.message) || e) + ")"); } console.log("[hide] removing not-yet-ready features from the public build (LevelCode Sync)"); diff --git a/tools/sync-server/README.md b/tools/sync-server/README.md index 226dcc7..f5a1c9d 100644 --- a/tools/sync-server/README.md +++ b/tools/sync-server/README.md @@ -38,7 +38,7 @@ node tools/sync-server/test.js # round-trips the REST contract (manifest, ET ## Notes - **Auth:** every `/v1` request needs `Authorization: Bearer ` (the token comes from the - `levelcode` auth provider in `extensions/atom-sync`). Storage is **isolated per token**. + `levelcode` auth provider in `extensions/levelcode-sync`). Storage is **isolated per token**. - **Opaque bodies:** the server never parses settings, so client-side **E2E encryption** can be added later with zero server changes. - **Dev only:** HTTP on localhost, file-backed storage under `tools/sync-server/.data` (gitignored). diff --git a/tools/update-server/README.md b/tools/update-server/README.md index b71ff6b..2e82ef2 100644 --- a/tools/update-server/README.md +++ b/tools/update-server/README.md @@ -1,7 +1,7 @@ # LevelCode reference update-feed server A tiny, **dependency-free** implementation of the Code-OSS update-feed contract that both the -built-in updater and the **notify-only `atom-updater` extension** speak. +built-in updater and the **notify-only `levelcode-updater` extension** speak. ``` GET /api/update/{target}/{quality}/{commit} @@ -47,7 +47,7 @@ node tools/update-server/test.js # 5 contract round-trips ## Notes -- **Notify-only:** the `atom-updater` extension only links to the download — it never installs. +- **Notify-only:** the `levelcode-updater` extension only links to the download — it never installs. Native auto-apply needs Developer-ID signing (the built-in Squirrel updater); this works on any build. - **Dev only:** HTTP on localhost. LevelCode Cloud is the managed implementation; staged rollout / channels live there. diff --git a/tools/update-server/server.js b/tools/update-server/server.js index 78c95ec..b73626c 100644 --- a/tools/update-server/server.js +++ b/tools/update-server/server.js @@ -2,7 +2,7 @@ * LevelCode — reference update-feed server * * A tiny, dependency-free implementation of the Code-OSS update-feed contract that BOTH the - * built-in updater and the notify-only atom-updater extension speak: + * built-in updater and the notify-only levelcode-updater extension speak: * * GET /api/update/{target}/{quality}/{commit} * → 204 if {commit} is already the latest build From a69e6e112c6356050d2b09670be6ad80000fa667 Mon Sep 17 00:00:00 2001 From: Sergii Demianchuk Date: Wed, 8 Jul 2026 23:09:03 -0400 Subject: [PATCH 2/7] Editor agent: refresh the LevelCode token on a mid-run 401 and retry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long/looping agent runs 401'd (Signature has expired) — only the chat path refreshed the access token on 401; the agent loop was called raw. Add isAgentAuthError + wrap the turn call: on a 401 before any content has streamed, refresh once (via a ctx.refreshAuth hook wired in extension.js), update ctx.apiKey, and retry the turn once. Mid-stream/second failures propagate; BYOK / refresh-failed rethrow. Co-Authored-By: Claude Opus 4.8 --- extensions/levelcode-ai/agent.js | 29 ++++++++++++++++++++++++++-- extensions/levelcode-ai/extension.js | 7 +++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/extensions/levelcode-ai/agent.js b/extensions/levelcode-ai/agent.js index c5bce76..4e87bb7 100644 --- a/extensions/levelcode-ai/agent.js +++ b/extensions/levelcode-ai/agent.js @@ -393,6 +393,13 @@ async function runTool(tu, ctx) { * providerId/baseURL select the model provider (Anthropic native, or any OpenAI-shaped one via the * tool-use translation layer); applyEdit applies file edits (apply-then-review); approve gates run_command only. */ +// A gateway 401 (expired LevelCode access token) surfaces as " API 401: …" or the raw +// "Signature has expired". Used to trigger ONE token-refresh + retry inside the agent loop (mirrors +// the chat path's refresh-on-401 — see extension.js isAuthError). +function isAgentAuthError(e) { + return /\bAPI 401\b|signature has expired/i.test(String((e && e.message) || e)); +} + async function runAgent(ctx) { const root = workspaceRoot(); if (!root) { ctx.post({ type: 'agentError', message: 'Open a folder first — the agent works on your workspace.' }); ctx.post({ type: 'agentDone', reason: 'error' }); return; } @@ -481,7 +488,7 @@ async function runAgent(ctx) { dbg('turn.request', { step, transcriptMsgs: messages.length }); let streamed = false; let textChars = 0; - const turn = await providers.streamAgentTurn({ + const turnOpts = { providerId: ctx.providerId, baseURL: ctx.baseURL, apiKey: ctx.apiKey, model: ctx.model, maxTokens: perTurnMax, system: system, messages, tools: TOOLS, signal: ctx.signal, @@ -493,7 +500,25 @@ async function runAgent(ctx) { : name === 'run_command' ? 'preparing command…' : name === 'update_plan' ? 'planning…' : 'running ' + name + '…'; ctx.post({ type: 'agentStatus', text: verb }); } - }); + }; + let turn; + try { + turn = await providers.streamAgentTurn(turnOpts); + } catch (e) { + // LevelCode Cloud access token expired mid-run → refresh once and retry THIS turn. A 401 is + // rejected upfront, so we only retry when nothing has streamed yet (a mid-stream failure + // would otherwise duplicate output). BYOK / no refresh hook / refresh returned null → rethrow. + if (!streamed && isAgentAuthError(e) && typeof ctx.refreshAuth === 'function') { + const fresh = await ctx.refreshAuth(); + if (!fresh) { throw e; } + dbg('turn.authRetry', { step }); + ctx.apiKey = fresh; + turnOpts.apiKey = fresh; + turn = await providers.streamAgentTurn(turnOpts); + } else { + throw e; + } + } if (streamed) { ctx.post({ type: 'agentTurnEnd' }); } dbg('turn.response', { step, stop: turn.stop_reason, blocks: turn.content.length, textChars, tools: turn.content.filter((c) => c.type === 'tool_use').map((c) => c.name + (turn.malformed && turn.malformed.has(c.id) ? '!malformed' : '')) }); // Report real context usage (input_tokens = how full the transcript is) so the UI can meter + warn. diff --git a/extensions/levelcode-ai/extension.js b/extensions/levelcode-ai/extension.js index 0a8b070..d9d13ea 100644 --- a/extensions/levelcode-ai/extension.js +++ b/extensions/levelcode-ai/extension.js @@ -865,6 +865,13 @@ async function agentFlow(text) { post, dbg, approve: requestApproval, // run_command only ask: requestQuestions, // ask_user — clickable clarifying questions + // Gateway-mode token refresh for the agent loop: on a mid-run 401 (expired access token) + // agent.js calls this to renew silently + retry the turn. Returns the fresh token, or null + // (BYOK / signed out / refresh failed → the run surfaces the auth error as before). + refreshAuth: async () => { + if (!req.gateway) { return null; } + return (await refreshGatewayToken()) ? await ctx.secrets.get(ACCOUNT_TOKEN_KEY) : null; + }, skills: skillsObj, // M6.5: implicit skills (name+desc menu in SYSTEM + use_skill resolver) contextLimit: contextLimitFor(req.providerId, capsModel(req.model)), // Auto → flagship window; the model SENT stays req.model commandStops: commandStops, // runId → stop() (process-group kill); used by Stop button / ■ From a4ad2b722ea974c706d6cc0b1cf708938b919936 Mon Sep 17 00:00:00 2001 From: Sergii Demianchuk Date: Wed, 8 Jul 2026 23:09:03 -0400 Subject: [PATCH 3/7] Editor chat: collapsible question/plan panels, plan redesign, cap upgrade card ask_user and update_plan panels get a collapse/expand header (plan collapse state persists across re-renders). Plan redesign: professional .ci status icons (check / spinning sync / circle), a progress bar + count pill, refined rows. Free-tier cap (402 cap_reached) now renders a compelling upgrade card (plan-aware: paid users get a Manage-plan button) instead of raw error JSON. Co-Authored-By: Claude Opus 4.8 --- extensions/levelcode-ai/media/chat.html | 115 ++++++++++++++++++++---- 1 file changed, 96 insertions(+), 19 deletions(-) diff --git a/extensions/levelcode-ai/media/chat.html b/extensions/levelcode-ai/media/chat.html index 5970c55..b78b60f 100644 --- a/extensions/levelcode-ai/media/chat.html +++ b/extensions/levelcode-ai/media/chat.html @@ -111,6 +111,17 @@ .msg.user .body { background: var(--field-bg); border: 1px solid var(--border); border-radius: 10px; padding: 9px 11px; } .msg.assistant .body { padding: 1px 2px; } .err { color: var(--vscode-errorForeground); } + /* free-tier cap reached → a compelling upgrade CTA instead of raw 402 error text */ + .upgradecard { border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border)); border-radius: 12px; margin: 8px 2px; padding: 13px 15px 14px; background: color-mix(in srgb, var(--accent) 9%, var(--field-bg)); } + .upgradecard .uchead { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; margin-bottom: 6px; } + .upgradecard .uchead .ci { width: 15px; height: 15px; color: var(--accent); } + .upgradecard .ucbody { font-size: 12.5px; line-height: 1.55; opacity: .92; margin-bottom: 12px; } + .upgradecard .ucbtns { display: flex; gap: 8px; flex-wrap: wrap; } + .upgradecard .ucbtn { cursor: pointer; border: none; border-radius: 7px; padding: 7px 16px; font-size: 12px; font-weight: 500; } + .upgradecard .ucbtn.primary { background: var(--accent); color: var(--vscode-button-foreground, #fff); } + .upgradecard .ucbtn.primary:hover { filter: brightness(1.1); } + .upgradecard .ucbtn.ghost { background: transparent; border: 1px solid var(--border); color: var(--vscode-foreground); } + .upgradecard .ucbtn.ghost:hover { background: var(--vscode-toolbar-hoverBackground, rgba(127,127,127,.14)); } pre { background: var(--vscode-editor-background, #282c34); border: 1px solid var(--border); padding: 9px 11px; border-radius: 8px; overflow-x: auto; margin: 8px 0; min-height: 1.3em; color: var(--vscode-editor-foreground, var(--vscode-foreground)); } pre code { background: none; padding: 0; } code { font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: .92em; } @@ -230,7 +241,14 @@ /* ---- ask_user: clickable clarifying questions ---- */ .questions { border: 1px solid var(--border); border-radius: 10px; margin: 6px 2px; padding: 10px 11px 11px; background: var(--field-bg); } - .questions .qhead { font-size: 12px; font-weight: 600; margin-bottom: 8px; } + .questions .qhead { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; margin-bottom: 8px; cursor: pointer; user-select: none; } + .questions .qhead .qtitle { flex: 1 1 auto; min-width: 0; } + .questions .qtoggle { flex: 0 0 auto; display: inline-flex; align-items: center; padding: 1px; border-radius: 4px; opacity: .7; } + .questions .qhead:hover .qtoggle { opacity: 1; } + .questions .qtoggle .ci { transition: transform .15s ease; } + .questions.collapsed .qtoggle .ci { transform: rotate(-90deg); } + .questions.collapsed .qhead { margin-bottom: 0; } + .questions.collapsed .qblock, .questions.collapsed .qnotes, .questions.collapsed .qbtns { display: none; } .questions .qblock { margin: 8px 0 10px; } .questions .qtag { display: inline-block; font-size: 10px; text-transform: uppercase; letter-spacing: .04em; opacity: .65; margin-bottom: 3px; } .questions .qtext { font-size: 12.5px; margin-bottom: 6px; } @@ -345,17 +363,32 @@ background: var(--vscode-sideBar-background, var(--vscode-editor-background, #1e1e1e)); border-top: 1px solid var(--border); box-shadow: 0 -8px 14px -8px rgba(0,0,0,.4); } #planBar .plancard { margin: 0; } - .plancard { border: 1px solid var(--border); border-radius: 10px; margin: 6px 2px; overflow: hidden; background: var(--field-bg); } - .plancard .phead { padding: 7px 11px; font-size: 12px; font-weight: 600; border-bottom: 1px solid var(--border); } - .plancard .pcount { float: right; opacity: .65; font-weight: 400; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); } - .plancard .pitems { padding: 4px 0 6px; } - .plancard .pitem { display: flex; gap: 8px; align-items: baseline; padding: 3px 11px; font-size: 12px; line-height: 1.4; } - .plancard .pitem .pic { width: 13px; flex: 0 0 auto; text-align: center; } - .plancard .pitem.done .ptitle { opacity: .55; text-decoration: line-through; } + .plancard { border: 1px solid var(--border); border-radius: 12px; margin: 6px 2px; overflow: hidden; background: var(--field-bg); } + .plancard .phead { display: flex; align-items: center; gap: 8px; padding: 9px 12px 8px; font-size: 12px; font-weight: 600; letter-spacing: .01em; cursor: pointer; user-select: none; } + .plancard .ptoggle { flex: 0 0 auto; display: inline-flex; color: var(--muted); opacity: .65; transition: transform .18s cubic-bezier(.4,0,.2,1); } + .plancard .ptoggle .ci { width: 13px; height: 13px; } + .plancard .phead:hover .ptoggle { opacity: 1; } + .plancard.collapsed .ptoggle { transform: rotate(-90deg); } + .plancard .phtitle { flex: 1 1 auto; min-width: 0; } + .plancard .pcount { flex: 0 0 auto; opacity: .75; font-weight: 500; font-variant-numeric: tabular-nums; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11px; padding: 1px 8px; border-radius: 999px; background: rgba(127,127,127,.14); } + .plancard.alldone .pcount { opacity: 1; color: var(--vscode-gitDecoration-addedResourceForeground, #4ec97a); background: color-mix(in srgb, var(--vscode-gitDecoration-addedResourceForeground, #4ec97a) 16%, transparent); } + .plancard .pprog { height: 3px; margin: 0 12px 2px; border-radius: 3px; background: rgba(127,127,127,.16); overflow: hidden; } + .plancard .pprog .pfill { height: 100%; border-radius: 3px; background: var(--accent); transition: width .4s cubic-bezier(.4,0,.2,1); } + .plancard.alldone .pprog .pfill { background: var(--vscode-gitDecoration-addedResourceForeground, #4ec97a); } + .plancard .pitems { padding: 6px 0 7px; } + .plancard.collapsed .pprog, .plancard.collapsed .pitems { display: none; } + .plancard .pitem { display: flex; gap: 9px; align-items: center; padding: 4px 12px; font-size: 12.5px; line-height: 1.45; position: relative; } + .plancard .pitem .pic { flex: 0 0 auto; width: 15px; height: 15px; display: inline-flex; align-items: center; justify-content: center; } + .plancard .pitem .pic .ci { width: 13px; height: 13px; } .plancard .pitem.done .pic { color: var(--vscode-gitDecoration-addedResourceForeground, #4ec97a); } - .plancard .pitem.doing .pic { color: var(--accent); animation: agentpulse 1.3s ease-in-out infinite; } + .plancard .pitem.done .ptitle { opacity: .5; text-decoration: line-through; text-decoration-thickness: 1px; } + .plancard .pitem.doing { background: color-mix(in srgb, var(--accent) 11%, transparent); } + .plancard .pitem.doing::before { content: ''; position: absolute; left: 0; top: 3px; bottom: 3px; width: 2px; border-radius: 2px; background: var(--accent); } + .plancard .pitem.doing .pic { color: var(--accent); } + .plancard .pitem.doing .pic .ci { animation: spin .9s linear infinite; } .plancard .pitem.doing .ptitle { font-weight: 600; } - .plancard .pitem.todo .pic { opacity: .4; } + .plancard .pitem.todo .pic { color: var(--muted); opacity: .5; } + .plancard .pitem.todo .ptitle { opacity: .82; } @keyframes agentpulse { 0%,100% { opacity: .45; } 50% { opacity: 1; } } .diff { overflow: auto; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11.5px; line-height: 1.5; padding: 4px 0; } .diff .dl { white-space: pre-wrap; overflow-wrap: anywhere; padding: 0 10px 0 22px; text-indent: -16px; } @@ -855,6 +888,8 @@ 'sparkle': { vb: '0 0 16 16', p: '' }, 'trash': { vb: '0 0 16 16', p: '' }, 'sync': { vb: '0 0 16 16', p: '' }, + 'chevron-down': { vb: '0 0 16 16', p: '' }, + 'circle': { vb: '0 0 16 16', p: '' }, }; function codicon(name, cls){ const i = IC[name]; if (!i) { return esc(name || ''); } return ''; } @@ -1284,12 +1319,37 @@ } // Blocking clarifying questions (ask_user) — clickable numbered options the agent waits on. + // A free-tier cap hit arrives as an error string like ` API 402: {"error":{"code":"cap_reached",…}}`. + // Parse the payload so we can show an upgrade CTA instead of dumping raw red JSON. + function capReachedInfo(message){ + const s = String(message || ''); + if (!/cap_reached/.test(s)) { return null; } + const i = s.indexOf('{'); if (i < 0) { return null; } + try { + const err = (JSON.parse(s.slice(i)) || {}).error || {}; + return err.code === 'cap_reached' ? { message: err.message || '', upgrade: !!err.upgrade_url } : null; + } catch (_) { return null; } + } + // Compelling cap-reached card (reuses the existing accountUpgrade action → opens /ai/pricing). Free + // tier → sell Pro; a paid user who hit their own cap → point at plan management, not "Upgrade to Pro". + function addUpgradeCard(info){ + clearStatus(); finishAgentBubble(); + const up = info.upgrade; + const card = document.createElement('div'); card.className = 'upgradecard'; + card.innerHTML = + '
' + codicon('sparkle') + '' + (up ? 'Keep building with Pro' : 'You’ve hit your usage limit') + '
' + + '
' + esc(info.message || 'Upgrade to keep building without interruptions.') + '
' + + '
'; + log.appendChild(card); scrollIfStuck(); + card.querySelectorAll('[data-act]').forEach((b) => { b.onclick = () => vscode.postMessage({ type: 'accountUpgrade' }); }); + } function addQuestions(m){ clearEmpty(); clearStatus(); agentBubble = null; const qs = m.questions || []; const state = qs.map((q) => ({ multi: !!q.multiSelect, sel: new Set() })); const card = document.createElement('div'); card.className = 'questions'; - let h = '
' + codicon('question') + ' A few quick decisions — click to choose
'; + let h = '
' + codicon('chevron-down') + '' + + '' + codicon('question') + ' A few quick decisions — click to choose
'; qs.forEach((q, qi) => { h += '
'; if (q.header){ h += '
' + esc(q.header) + (q.multiSelect ? ' · pick any' : '') + '
'; } @@ -1308,6 +1368,10 @@ card.innerHTML = h; log.appendChild(card); scrollIfStuck(); + // Collapse/expand the whole panel by clicking its header (chevron rotates). + const qhead = card.querySelector('.qhead'); + if (qhead) { qhead.onclick = () => card.classList.toggle('collapsed'); } + card.querySelectorAll('.qopt').forEach((btn) => { btn.onclick = () => { const qi = +btn.dataset.q, oi = +btn.dataset.o, st = state[qi]; @@ -1510,16 +1574,24 @@ renderBgTasks(); } const planBar = document.getElementById('planBar'); + let planCollapsed = false; // persists across the agent's repeated update_plan re-renders function renderPlan(todos){ if (!todos || !todos.length){ planBar.style.display = 'none'; planBar.innerHTML = ''; return; } const total = todos.length, done = todos.filter(t => t && t.status === 'done').length; - let h = '
Plan ' + done + '/' + total + '
'; + const pct = Math.round(done / total * 100), allDone = done === total; + let items = ''; for (const t of todos){ const st = t.status === 'done' ? 'done' : t.status === 'in_progress' ? 'doing' : 'todo'; - const ic = st === 'done' ? '✓' : st === 'doing' ? '◉' : '○'; - h += '
' + ic + '' + esc(t.title) + '
'; + const ic = st === 'done' ? codicon('check') : st === 'doing' ? codicon('sync') : codicon('circle'); + items += '
' + ic + '' + esc(t.title) + '
'; } - planBar.innerHTML = '
' + h + '
'; + const head = '
' + codicon('chevron-down') + '' + + 'Plan' + done + '/' + total + '
' + + '
'; + planBar.innerHTML = '
' + + head + '
' + items + '
'; + const phead = planBar.querySelector('.phead'); + if (phead){ phead.onclick = () => { planCollapsed = !planCollapsed; const c = planBar.querySelector('.plancard'); if (c){ c.classList.toggle('collapsed', planCollapsed); } }; } planBar.style.display = 'block'; scrollIfStuck(); // log shrank as the sticky plan appeared — keep pinned if we were at bottom } @@ -1904,9 +1976,14 @@ else if (m.type === 'assistantDone'){ doneSignaled = true; ensurePump(); } else if (m.type === 'assistantError'){ pending = ''; flushAll = false; doneSignaled = false; - const html = '' + esc(m.message) + ''; - if (current){ streamSetHtml(current, html); } else { add('assistant', html); } - current = null; setStreaming(false); + const cap = capReachedInfo(m.message); + if (cap){ current = null; addUpgradeCard(cap); } + else { + const html = '' + esc(m.message) + ''; + if (current){ streamSetHtml(current, html); } else { add('assistant', html); } + current = null; + } + setStreaming(false); } else if (m.type === 'activeFile'){ activeFileLabel = m.label; renderChips(); } else if (m.type === 'contextFiles'){ ctxFiles = m.files || []; renderChips(); } @@ -1939,7 +2016,7 @@ else if (m.type === 'debug'){ addDebug(m); } else if (m.type === 'account'){ renderAccount(m); if (m.open) openAccount(); } else if (m.type === 'fileIndex'){ setFileIndex(m.files || []); } - else if (m.type === 'agentError'){ clearStatus(); finishAgentBubble(); add('assistant', '' + esc(m.message) + ''); } + else if (m.type === 'agentError'){ clearStatus(); finishAgentBubble(); const cap = capReachedInfo(m.message); if (cap){ addUpgradeCard(cap); } else { add('assistant', '' + esc(m.message) + ''); } } else if (m.type === 'agentDone'){ clearStatus(); finishAgentBubble(); addAgentDone(m.reason, m.edits, m.credits, m.maxSteps); setStreaming(false); } else if (m.type === 'context'){ selLabel = m.label; renderChips(); } else if (m.type === 'clearContext'){ selLabel = null; renderChips(); } From 4a1d0d1b1030cc027f9eb95b31130ff2a6a532d8 Mon Sep 17 00:00:00 2001 From: Sergii Demianchuk Date: Wed, 8 Jul 2026 23:09:03 -0400 Subject: [PATCH 4/7] Editor build: prune stale extensions on branding + skip Copilot compile in dev apply-branding.mjs prunes orphaned LevelCode extensions from vscode/extensions (renamed-away atom-* copies double-registered commands). run-dev.sh compiles the client only (compile-client), skipping the proprietary Copilot build (stripped/disabled anyway, and broken under Node 24 by a glob CJS/ESM error). Co-Authored-By: Claude Opus 4.8 --- scripts/apply-branding.mjs | 29 ++++++++++++++++++++++++++--- scripts/run-dev.sh | 6 +++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/scripts/apply-branding.mjs b/scripts/apply-branding.mjs index da8fad7..0c830b0 100755 --- a/scripts/apply-branding.mjs +++ b/scripts/apply-branding.mjs @@ -93,12 +93,35 @@ if (existsSync(pngSrc) && existsSync(dirname(pngDst))) { // Install our bundled LevelCode extensions into the checkout. The canonical, version-controlled // source lives in this repo's extensions/ folder; the copies inside vscode/ are generated. +const OUR_PUBLISHERS = new Set(["levelcode", "atompp"]); // atompp = pre-rename, prune any leftovers const extSrcRoot = join(repoRoot, "extensions"); +const vscodeExtDir = join(vscodeDir, "extensions"); if (existsSync(extSrcRoot)) { - for (const name of readdirSync(extSrcRoot)) { + const current = new Set(readdirSync(extSrcRoot).filter((n) => statSync(join(extSrcRoot, n)).isDirectory())); + + // Prune stale copies of OUR extensions left in vscode/ from a previous rename (e.g. atom-ai → + // levelcode-ai). Without this, both the old and new folder load and double-register commands + // ("command 'levelcode.init.edit' already exists"). A dir is "ours" if its package.json publisher + // is one of OUR_PUBLISHERS; drop any that isn't in the current repo set. Code-OSS built-ins (other + // publishers) are never touched. + if (existsSync(vscodeExtDir)) { + for (const name of readdirSync(vscodeExtDir)) { + if (current.has(name)) { continue; } + const pkg = join(vscodeExtDir, name, "package.json"); + if (!existsSync(pkg)) { continue; } + try { + const publisher = JSON.parse(readFileSync(pkg, "utf8")).publisher; + if (OUR_PUBLISHERS.has(publisher)) { + rmSync(join(vscodeExtDir, name), { recursive: true, force: true }); + console.log(`[apply-branding] Pruned stale LevelCode extension -> ${name}`); + } + } catch { /* unreadable manifest: leave it alone */ } + } + } + + for (const name of current) { const src = join(extSrcRoot, name); - if (!statSync(src).isDirectory()) { continue; } - const dst = join(vscodeDir, "extensions", name); + const dst = join(vscodeExtDir, name); rmSync(dst, { recursive: true, force: true }); // clean overwrite (avoids stale/locked files) cpSync(src, dst, { recursive: true }); console.log(`[apply-branding] Installed extension -> extensions/${name}`); diff --git a/scripts/run-dev.sh b/scripts/run-dev.sh index 0e78454..627b97c 100755 --- a/scripts/run-dev.sh +++ b/scripts/run-dev.sh @@ -15,7 +15,11 @@ node "$SCRIPT_DIR/apply-branding.mjs" "$VSCODE_DIR" >/dev/null cd "$VSCODE_DIR" echo "[run-dev] Building core (first run takes a while)…" -npm run compile +# compile-client ONLY — deliberately NOT `npm run compile`, which also runs compile-copilot (the +# proprietary GitHub Copilot Chat extension). LevelCode disables Copilot at launch (--disable-extension +# below) and strips it from the packaged app (build-macos.sh), so compiling it is pointless here — and +# upstream's extensions/copilot/.esbuild.mts fails under Node 24 (glob CJS/ESM named-export error). +npm run compile-client echo "[run-dev] Ensuring a clean LevelCode instance…" # LevelCode dev + packaged builds share the same macOS bundle identifier. If another # instance is already running, LaunchServices can hand this launch off to that From a308e8e76acc37daebf239c818a50dd6b9840c86 Mon Sep 17 00:00:00 2001 From: Sergii Demianchuk Date: Thu, 9 Jul 2026 18:31:24 -0400 Subject: [PATCH 5/7] =?UTF-8?q?Editor=20chat:=20agent=20activity=20timelin?= =?UTF-8?q?e=20=E2=80=94=20Copilot-style=20cards,=20falling-dots,=20approv?= =?UTF-8?q?al=20redesign?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework the agent's command/tool activity into a connected timeline (rail + nodes). Executed commands render as collapsible GitHub/Copilot-style cards: a Running→Ran header with command chips + live state, the command with lightweight shell syntax highlighting, streamed output visible by default, an exit/duration status foot, and the agent's intent in italics. A Copilot-style falling-dots indicator replaces the spinner while running. run_command approval is redesigned as a quiet Cursor/Claude-style ask card (no macOS-terminal chrome) with Enter/Esc keyboard shortcuts that collapses to a one-line verdict. agent.js posts the command explanation so the card can show the why. Co-Authored-By: Claude Opus 4.8 --- extensions/levelcode-ai/agent.js | 2 +- extensions/levelcode-ai/media/chat.html | 226 ++++++++++++++++++++---- 2 files changed, 196 insertions(+), 32 deletions(-) diff --git a/extensions/levelcode-ai/agent.js b/extensions/levelcode-ai/agent.js index 4e87bb7..99843a2 100644 --- a/extensions/levelcode-ai/agent.js +++ b/extensions/levelcode-ai/agent.js @@ -305,7 +305,7 @@ async function runTool(tu, ctx) { const approved = await ctx.approve({ kind: 'command', command: cmd, explanation: input.explanation || '' }); if (!approved) { return 'User skipped this command. Do not retry it.'; } const runId = tu.id || ('run-' + Date.now()); - ctx.post({ type: 'termRun', id: runId, command: cmd, cwd: path.basename(root) || 'workspace', background: bg }); + ctx.post({ type: 'termRun', id: runId, command: cmd, cwd: path.basename(root) || 'workspace', background: bg, explanation: input.explanation || '' }); const stops = ctx.commandStops; // shared registry so the Stop button / ■ can kill the process group // Only BACKGROUND commands get a registry entry (read_command_output reads it). Foreground // one-shots keep their old behavior + don't accumulate — the model already gets their output. diff --git a/extensions/levelcode-ai/media/chat.html b/extensions/levelcode-ai/media/chat.html index b78b60f..4627598 100644 --- a/extensions/levelcode-ai/media/chat.html +++ b/extensions/levelcode-ai/media/chat.html @@ -239,6 +239,93 @@ .verifyrun .tbartitle { color: var(--vscode-terminal-ansiCyan, var(--accent)); display: inline-flex; align-items: center; gap: 5px; } .verifyrun .tbartitle .ci { width: 1em; height: 1em; } + /* ── Agent activity timeline — tool + command actions thread one rail (GitHub-commit style) ── + Scope: only the tool/command nodes are threaded; free reasoning bubbles break the rail (intended). */ + .tl { display: flex; gap: 11px; margin: 0 4px; position: relative; } + .tl-rail { flex: 0 0 22px; position: relative; } + .tl-rail::before { content: ''; position: absolute; left: 11px; top: 0; bottom: 0; width: 2px; transform: translateX(-1px); background: var(--border); } + .tl-node { position: absolute; left: 0; top: 3px; width: 22px; height: 22px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; + background: var(--vscode-editor-background, var(--field-bg)); border: 1.5px solid var(--border); color: var(--muted); z-index: 1; } + .tl-node .ci { width: 12px; height: 12px; } + .tl-body { flex: 1 1 auto; min-width: 0; padding: 3px 0 8px; } + /* light tool action (read / edit / search …) — a one-liner node */ + .tl-tool .tl-body { padding: 4px 0 6px; } + .tl-tool .toolt { display: block; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11.5px; color: var(--muted); line-height: 16px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + /* heavier command action — collapsible boxed card: header (chevron · verb · chips · state) + → highlighted command → streamed output → status foot → italic why */ + .tl-cmd .tl-node { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 55%, transparent); background: color-mix(in srgb, var(--accent) 12%, var(--vscode-editor-background, var(--field-bg))); } + .tl-cmd.running .tl-node { animation: nodepulse 2s ease-out infinite; } + @keyframes nodepulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 40%, transparent); } 70%, 100% { box-shadow: 0 0 0 6px transparent; } } + .tl-cmd .cmdhead { display: flex; align-items: center; gap: 7px; min-width: 0; cursor: pointer; user-select: none; padding: 2px 0; } + .tl-cmd .cmdchev { flex: 0 0 auto; display: inline-flex; color: var(--muted); } + .tl-cmd .cmdchev .ci { width: 11px; height: 11px; transition: transform .15s ease; } + .tl-cmd.collapsed .cmdchev .ci { transform: rotate(-90deg); } + .tl-cmd .cmdverb { flex: 0 0 auto; font-size: 12.5px; color: var(--vscode-foreground); } + .tl-cmd .cmdhead:hover .cmdverb { text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--muted); } + .tl-cmd .cmdchips { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 5px; overflow: hidden; } + .tl-cmd .cmdchips code { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11px; line-height: 18px; + padding: 0 7px; border: 1px solid var(--border); border-radius: 5px; background: var(--field-bg); color: var(--muted); } + .tl-cmd .cmdchips code.more { flex: 0 0 auto; opacity: .75; } + .tl-cmd .cmdstate { flex: 0 0 auto; display: inline-flex; color: var(--muted); } + .tl-cmd .cmdstate .ci { width: 12px; height: 12px; } + .tl-cmd .cmdstate.ok { color: var(--vscode-terminal-ansiBrightGreen, #4ec97a); } + .tl-cmd .cmdstate.bad { color: var(--vscode-errorForeground, #f14c4c); } + /* Copilot-style falling-dots progress — a 2×3 dot grid cascading downward while a command runs */ + .lcdots { display: inline-grid; grid-template-columns: repeat(2, 3px); gap: 2px; color: var(--accent); align-self: center; } + .lcdots i { width: 3px; height: 3px; border-radius: 50%; background: currentColor; animation: lcdotfall 1.1s ease-in-out infinite; } + .lcdots i:nth-child(2) { animation-delay: .09s; } + .lcdots i:nth-child(3) { animation-delay: .18s; } + .lcdots i:nth-child(4) { animation-delay: .27s; } + .lcdots i:nth-child(5) { animation-delay: .36s; } + .lcdots i:nth-child(6) { animation-delay: .45s; } + @keyframes lcdotfall { 0% { opacity: .15; transform: translateY(-1.5px); } 25%, 55% { opacity: 1; transform: translateY(0); } 85%, 100% { opacity: .15; transform: translateY(1.5px); } } + @media (prefers-reduced-motion: reduce) { .lcdots i { animation: none; opacity: .7; } } + .tl-cmd .cmdbox { margin-top: 7px; border: 1px solid var(--border); border-radius: 8px; background: var(--field-bg); overflow: hidden; } + .tl-cmd.collapsed .cmdbox, .tl-cmd.collapsed .cmdwhy { display: none; } + .tl-cmd .cmdsrc { padding: 9px 12px; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 12px; line-height: 1.55; + white-space: pre-wrap; word-break: break-word; color: var(--vscode-terminal-foreground, var(--vscode-foreground)); } + /* shell syntax tints — VS Code terminal ANSI theme colors, so they follow light/dark */ + .tl-cmd .sh-c { color: var(--vscode-terminal-ansiBrightBlue, var(--vscode-terminal-ansiBlue, #4fc1ff)); font-weight: 600; } + .tl-cmd .sh-f { color: var(--vscode-terminal-ansiBrightCyan, var(--vscode-terminal-ansiCyan, #56b6c2)); } + .tl-cmd .sh-s { color: var(--vscode-terminal-ansiYellow, #ce9178); } + .tl-cmd .sh-o { color: var(--vscode-terminal-ansiBrightRed, var(--vscode-terminal-ansiRed, #d16969)); } + .tl-cmd .sh-v { color: var(--vscode-terminal-ansiBrightGreen, var(--vscode-terminal-ansiGreen, #4ec9b0)); } + .tl-cmd .sh-k { color: var(--vscode-terminal-ansiBrightBlack, var(--muted)); font-style: italic; } + .tl-cmd .termout { margin: 0; padding: 8px 12px; border: none; border-top: 1px solid var(--border); border-radius: 0; + background: color-mix(in srgb, var(--vscode-editor-background, #1e1e1e) 55%, var(--field-bg)); + max-height: 300px; overflow: auto; white-space: pre-wrap; word-break: break-word; + font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 12px; line-height: 1.5; color: var(--vscode-terminal-foreground, var(--vscode-foreground)); } + .tl-cmd .termout:empty { display: none; } + .tl-cmd .termerr { color: var(--vscode-terminal-ansiRed, var(--vscode-errorForeground, #f14c4c)); } + .tl-cmd .cmdfoot { display: flex; align-items: center; gap: 8px; padding: 6px 12px; border-top: 1px solid var(--border); } + .tl-cmd .termstatus { flex: 1 1 auto; font-size: 11.5px; color: var(--muted); display: inline-flex; align-items: center; gap: 6px; } + .tl-cmd .termstatus.ok { color: var(--vscode-terminal-ansiBrightGreen, #4ec97a); } + .tl-cmd .termstatus.bad { color: var(--vscode-errorForeground, #f14c4c); } + .tl-cmd .termstatus .ci { width: 1em; height: 1em; } + .tl-cmd .cmdmeta { flex: 0 0 auto; font-size: 10.5px; color: var(--muted); letter-spacing: .03em; opacity: .75; } + .tl-cmd .termstop { flex: 0 0 auto; cursor: pointer; border: 1px solid var(--border); border-radius: 999px; padding: 2px 10px; font-size: 11px; display: inline-flex; align-items: center; gap: 4px; background: var(--vscode-button-secondaryBackground, rgba(127,127,127,.22)); color: var(--vscode-foreground); } + .tl-cmd .termstop:hover { background: var(--vscode-errorForeground, #f14c4c); color: #fff; border-color: transparent; } + .tl-cmd .termstop[disabled] { opacity: .6; cursor: default; } + .tl-cmd .termstop .ci { width: 1em; height: 1em; } + .tl-cmd .cmdwhy { margin-top: 8px; font-size: 11.5px; font-style: italic; color: var(--muted); white-space: pre-wrap; word-break: break-word; padding-left: 9px; border-left: 2px solid var(--border); } + /* command-approval ask card (Cursor/Claude style) — same skeleton as .tl-cmd, question header, + Run/Skip in the foot; collapses to a one-line verdict once decided */ + .tl-ask .cmdhead { cursor: default; } + .tl-ask .cmdhead:hover .cmdverb { text-decoration: none; } + .tl-ask .cmdhead .cmdverb { font-weight: 600; } + .tl-ask .cmdhead.ask .cmdverb { flex: 1 1 auto; } + .tl-ask.asking .tl-node { animation: nodepulse 2s ease-out infinite; } + .tl-ask .asknote { flex: 1 1 auto; min-width: 0; font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .tl-ask .cmdfoot button { cursor: pointer; border: none; border-radius: 6px; padding: 4px 12px; font-size: 12px; display: inline-flex; align-items: center; gap: 6px; } + .tl-ask .cmdfoot .approve { background: var(--vscode-button-background); color: var(--vscode-button-foreground); } + .tl-ask .cmdfoot .approve:hover { background: var(--vscode-button-hoverBackground); } + .tl-ask .cmdfoot .skip { background: var(--vscode-button-secondaryBackground, rgba(127,127,127,.25)); color: var(--vscode-button-secondaryForeground, var(--vscode-foreground)); } + .tl-ask .cmdfoot kbd { font-family: inherit; font-size: 9.5px; opacity: .75; border: 1px solid color-mix(in srgb, currentColor 35%, transparent); border-radius: 3px; padding: 0 4px; line-height: 14px; } + .tl-ask.skipped .tl-node { color: var(--muted); border-color: var(--border); background: var(--vscode-editor-background, var(--field-bg)); } + .tl-ask.skipped .cmdstate.bad { color: var(--muted); } /* skipped is a choice, not a failure — keep it neutral */ + @media (prefers-reduced-motion: reduce) { .tl-cmd.running .tl-node, .tl-ask.asking .tl-node { animation: none; } } + /* ---- ask_user: clickable clarifying questions ---- */ .questions { border: 1px solid var(--border); border-radius: 10px; margin: 6px 2px; padding: 10px 11px 11px; background: var(--field-bg); } .questions .qhead { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; margin-bottom: 8px; cursor: pointer; user-select: none; } @@ -892,6 +979,8 @@ 'circle': { vb: '0 0 16 16', p: '' }, }; function codicon(name, cls){ const i = IC[name]; if (!i) { return esc(name || ''); } return ''; } + // Falling-dots progress (Copilot-style) shown while a command runs; styled by .lcdots CSS. + const LC_DOTS = ''; // "name.ext · 12 lines" -> badge from extension (JS, TS, PY…), capped at 3 chars. function badgeFor(label){ @@ -1177,10 +1266,12 @@ log.appendChild(d); scrollIfStuck(); } function clearStatus(){ if (agentStatusEl){ agentStatusEl.remove(); agentStatusEl = null; } } + // A tool action (read / edit / search …) — rendered as a light node on the activity timeline rail. function addAgentLine(icon, text){ clearEmpty(); clearStatus(); - const d = document.createElement('div'); d.className = 'agentline'; - d.innerHTML = '' + (IC[icon] ? codicon(icon) : esc(icon || '•')) + '' + esc(text || '') + ''; + const d = document.createElement('div'); d.className = 'tl tl-tool'; + d.innerHTML = '
' + (IC[icon] ? codicon(icon) : esc(icon || '•')) + '
' + + '
' + esc(text || '') + '
'; log.appendChild(d); scrollIfStuck(); } function setAgentStatus(text){ @@ -1205,49 +1296,115 @@ return h + '
'; } // Blocking approval — run_command ONLY (file edits no longer gate; they apply-then-review). + // Quiet Cursor/Claude-style ask card on the activity timeline: question header → + // highlighted command → foot (note · Skip esc · Run ⏎). On decision it collapses to a + // one-line verdict; the live run card follows with the output. + let pendingApproval = null; // { done } while a decision is awaited — Enter approves, Esc skips function addApproval(m){ clearEmpty(); clearStatus(); agentBubble = null; - const cwd = m.cwd || 'workspace'; - const card = document.createElement('div'); card.className = 'approval term'; + const card = document.createElement('div'); card.className = 'tl tl-cmd tl-ask asking'; + const segs = String(m.command || '').split(/\n+|\s*&&\s*|\s*;\s*/).map(s => s.trim()).filter(Boolean); + const chips = segs.slice(0, 2).map(s => '' + esc(s) + '').join('') + + (segs.length > 2 ? '+' + (segs.length - 2) + '' : ''); card.innerHTML = - '
' - + 'zsh — ' + esc(cwd) + '
' - + '
' - + '
' + esc(cwd) + ' ' - + '' + esc(m.command) + '
' - + (m.explanation ? '
# ' + esc(m.explanation) + '
' : '') - + '
' - + '
⌘ LevelCode wants to run this in your terminal' - + '' - + '' - + '
'; + '
' + codicon('terminal') + '
' + + '
' + + '
Run this command?' + + 'zsh' + (m.cwd ? ' · ' + esc(m.cwd) : '') + '
' + + '
' + + '
' + shHighlight(m.command || '') + '
' + + '
' + + 'LevelCode wants to run this in your terminal' + + '' + + '' + + '
' + + '
' + + (m.explanation ? '
' + esc(m.explanation) + '
' : '') + + '
'; log.appendChild(card); scrollIfStuck(); const done = (approved) => { + pendingApproval = null; vscode.postMessage({ type: 'approvalResponse', id: m.id, approved }); - card.classList.add(approved ? 'ran' : 'skipped'); - const caret = card.querySelector('.tcaret'); if (caret) { caret.remove(); } - card.querySelector('.termfoot').innerHTML = '' + (approved ? codicon('check') + ' Approved' : codicon('circle-slash') + ' Skipped') + ''; + card.classList.remove('asking'); + if (!approved) { card.classList.add('skipped'); } + card.querySelector('.tl-body').innerHTML = + '
' + (approved ? 'Approved' : 'Skipped') + '' + + '' + chips + '' + + '' + codicon(approved ? 'check' : 'circle-slash') + '
'; }; card.querySelector('.approve').onclick = () => done(true); card.querySelector('.skip').onclick = () => done(false); + pendingApproval = { done }; + } + // Enter/Esc answer the pending command approval — but never while the user is typing + // in a field, or when the account modal / context popover own the keys. + document.addEventListener('keydown', (e) => { + if (!pendingApproval) { return; } + const t = e.target; + if (t && (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT' || t.isContentEditable)) { return; } + if (acctOverlay.style.display !== 'none' || ctxOpen) { return; } + if (e.key === 'Enter') { e.preventDefault(); pendingApproval.done(true); } + else if (e.key === 'Escape') { e.preventDefault(); pendingApproval.done(false); } + }); + + // Lightweight shell syntax highlighter for the command source shown in the card. + // Tokenizes the RAW string and escapes each piece — never feed pre-escaped HTML in. + function shHighlight(cmd){ + const s = String(cmd || ''); + const re = /(#[^\n]*)|('(?:[^'\\]|\\.)*'?)|("(?:[^"\\]|\\.)*"?)|(\$\{[^}]*\}|\$\w+)|(\n|\|\||&&|;|\||\d?>>?&?\d?|<)|([^\S\n]+)|([^\s'"#$|&;<>]+)/g; + let html = '', atCmd = true, mt; + while ((mt = re.exec(s))) { + const tok = mt[0]; + if (mt[1] != null) { html += '' + esc(tok) + ''; } + else if (mt[2] != null || mt[3] != null) { html += '' + esc(tok) + ''; atCmd = false; } + else if (mt[4] != null) { html += '' + esc(tok) + ''; } + else if (mt[5] != null) { + html += tok === '\n' ? '\n' : '' + esc(tok) + ''; + if (tok === '\n' || tok === '|' || tok === '||' || tok === '&&' || tok === ';') { atCmd = true; } // redirects don't start a new command + } + else if (mt[6] != null) { html += tok; } + else { + if (atCmd && /^\w+=/.test(tok)) { html += '' + esc(tok) + ''; } // FOO=bar prefix — command still to come + else if (atCmd) { html += '' + esc(tok) + ''; atCmd = false; } + else if (/^-{1,2}[\w]/.test(tok)) { html += '' + esc(tok) + ''; } + else { html += esc(tok); } + } + } + return html; } - // Live terminal: an approved command streams its real stdout/stderr here as it runs. + // An executed command — a collapsible card on the activity timeline: + // header (chevron · Running/Ran · command chips · state icon) → highlighted command + // → streamed output (visible by default) → status foot → italic why. function addTermRun(m){ clearEmpty(); clearStatus(); agentBubble = null; - const card = document.createElement('div'); card.className = 'approval term termrun'; + const card = document.createElement('div'); card.className = 'tl tl-cmd running'; + const segs = String(m.command || '').split(/\n+|\s*&&\s*|\s*;\s*/).map(s => s.trim()).filter(Boolean); + const chips = segs.slice(0, 2).map(s => '' + esc(s) + '').join('') + + (segs.length > 2 ? '+' + (segs.length - 2) + '' : ''); card.innerHTML = - '
' - + 'zsh — ' + esc(m.cwd || 'workspace') + '
' - + '
' - + '
' + esc(m.cwd || '') + ' ' - + '' + esc(m.command || '') + '
' - + '
'
-        + '
' + (m.background ? 'running in background… ' : 'running… ') - + '
' + '
' + codicon('terminal') + '
' + + '
' + + '
' + + '' + codicon('chevron-down') + '' + + '' + (m.background ? 'Running in background' : 'Running') + '' + + '' + chips + '' + + '' + LC_DOTS + '' + + '
' + + '
' + + '
' + shHighlight(m.command || '') + '
' + + '
'
+          + '
' + + '
' + LC_DOTS + ' ' + (m.background ? 'running in background…' : 'running…') + '
' + + 'zsh' + (m.cwd ? ' · ' + esc(m.cwd) : '') + '' + + '' + + '
' + + '
' + + (m.explanation ? '
' + esc(m.explanation) + '
' : '') + '
'; log.appendChild(card); scrollIfStuck(); termCards[m.id] = card; + card.querySelector('.cmdhead').onclick = () => card.classList.toggle('collapsed'); const sb = card.querySelector('.termstop'); if (sb) { sb.onclick = () => { sb.disabled = true; sb.textContent = 'stopping…'; vscode.postMessage({ type: 'stopCommand', id: m.id }); }; } } function termOutputAppend(m){ @@ -1261,9 +1418,16 @@ const card = termCards[m.id]; if (!card) { return; } const st = card.querySelector('.termstatus'); if (!st) { return; } const dur = (typeof m.ms === 'number') ? (m.ms >= 1000 ? (m.ms / 1000).toFixed(1) + 's' : m.ms + 'ms') : ''; - if (m.how === 'stopped'){ st.innerHTML = codicon('circle-slash') + ' stopped' + (dur ? ' · ' + dur : ''); st.className = 'termstatus bad'; } - else if (m.how === 'timeout'){ st.innerHTML = codicon('warning') + ' timed out' + (dur ? ' · ' + dur : ''); st.className = 'termstatus bad'; } - else { const ok = m.code === 0; st.innerHTML = (ok ? codicon('check') : codicon('circle-slash')) + ' exit ' + m.code + (dur ? ' · ' + dur : ''); st.className = 'termstatus ' + (ok ? 'ok' : 'bad'); } + let icon, text, cls; + if (m.how === 'stopped'){ icon = 'circle-slash'; text = 'stopped'; cls = 'bad'; } + else if (m.how === 'timeout'){ icon = 'warning'; text = 'timed out'; cls = 'bad'; } + else { const ok = m.code === 0; icon = ok ? 'check' : 'circle-slash'; text = 'exit ' + m.code; cls = ok ? 'ok' : 'bad'; } + st.innerHTML = codicon(icon) + ' ' + text + (dur ? ' · ' + dur : ''); + st.className = 'termstatus ' + cls; + card.classList.remove('running'); // stops the node pulse + const verb = card.querySelector('.cmdverb'); if (verb) { verb.textContent = 'Ran'; } + const state = card.querySelector('.cmdstate'); if (state) { state.innerHTML = codicon(icon); state.className = 'cmdstate ' + cls; } + const sb = card.querySelector('.termstop'); if (sb) { sb.remove(); } delete termCards[m.id]; scrollIfStuck(); } From bf52660d4d4c949e59448e498882a745d2d61177 Mon Sep 17 00:00:00 2001 From: Sergii Demianchuk Date: Thu, 9 Jul 2026 22:25:33 -0400 Subject: [PATCH 6/7] Editor: neutral service card for our-side outages (not a red error or upgrade card) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pairs with thin.ly's upstream-error sanitizer (PR #341): the gateway now returns a safe, classified error, so the editor picks a calmer presentation. Providers forward the structured error 'code'; agent.js and extension.js pass it along on agentError/assistantError; chat.html adds isServiceIssue + addServiceCard — a neutral notice (amber warning icon, no accent, no CTA) — and routes service_unavailable / service_busy / gateway_error to it, keeping the accent upgrade card reserved for a genuine user-wallet cap. Even if the code fails to propagate, the gateway message is already sanitized, so no raw provider text can reach any branch. Co-Authored-By: Claude Opus 4.8 --- extensions/levelcode-ai/agent.js | 3 +- extensions/levelcode-ai/extension.js | 2 +- extensions/levelcode-ai/media/chat.html | 28 ++++++++++++++++++- .../levelcode-ai/providers/openaiCompat.js | 4 +-- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/extensions/levelcode-ai/agent.js b/extensions/levelcode-ai/agent.js index 99843a2..f6cd013 100644 --- a/extensions/levelcode-ai/agent.js +++ b/extensions/levelcode-ai/agent.js @@ -631,6 +631,7 @@ async function runAgent(ctx) { if (step > ctx.maxSteps) { reason = 'limit'; } } catch (e) { const msg = String((e && e.message) || e); + const code = e && e.code; // structured gateway code (e.g. service_unavailable) → webview picks the card dbg('agent.error', { msg, aborted: ctx.signal.aborted }); if (ctx.signal.aborted) { reason = 'stopped'; } else if (/prompt is too long|context.{0,12}(limit|window|length)|exceed.{0,20}(context|maximum)|too many tokens/i.test(msg)) { @@ -638,7 +639,7 @@ async function runAgent(ctx) { ctx.post({ type: 'agentError', message: 'You’ve hit the model’s context window (the conversation got too long). Start a New chat to reset it, switch to a larger-context model, or pin fewer files — then continue.', kind: 'context' }); reason = 'error'; } - else { ctx.post({ type: 'agentError', message: msg }); reason = 'error'; } + else { ctx.post({ type: 'agentError', message: msg, code }); reason = 'error'; } } finally { dbg('agent.done', { reason, steps: step - 1, edits: ctx.editCount || 0, credits: ctx.credits != null ? ctx.credits : null }); // credits: null until the M10 gateway returns real usage; the response bar shows it when present. diff --git a/extensions/levelcode-ai/extension.js b/extensions/levelcode-ai/extension.js index d9d13ea..6069e9c 100644 --- a/extensions/levelcode-ai/extension.js +++ b/extensions/levelcode-ai/extension.js @@ -959,7 +959,7 @@ async function handleSend(text) { post({ type: 'assistantDone' }); } else { conversation.pop(); - post({ type: 'assistantError', message: String((e && e.message) || e) }); + post({ type: 'assistantError', message: String((e && e.message) || e), code: e && e.code }); } } finally { abort = null; diff --git a/extensions/levelcode-ai/media/chat.html b/extensions/levelcode-ai/media/chat.html index 4627598..bc93629 100644 --- a/extensions/levelcode-ai/media/chat.html +++ b/extensions/levelcode-ai/media/chat.html @@ -122,6 +122,11 @@ .upgradecard .ucbtn.primary:hover { filter: brightness(1.1); } .upgradecard .ucbtn.ghost { background: transparent; border: 1px solid var(--border); color: var(--vscode-foreground); } .upgradecard .ucbtn.ghost:hover { background: var(--vscode-toolbar-hoverBackground, rgba(127,127,127,.14)); } + /* our-side outage notice — neutral, NO accent, NO CTA (it is not the user's account/usage) */ + .noticecard { border: 1px solid var(--border); border-radius: 12px; margin: 8px 2px; padding: 13px 15px 14px; background: var(--field-bg); } + .noticecard .uchead { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; margin-bottom: 6px; } + .noticecard .uchead .ci { width: 15px; height: 15px; color: var(--vscode-editorWarning-foreground, #e0a030); } + .noticecard .ucbody { font-size: 12.5px; line-height: 1.55; opacity: .92; } pre { background: var(--vscode-editor-background, #282c34); border: 1px solid var(--border); padding: 9px 11px; border-radius: 8px; overflow-x: auto; margin: 8px 0; min-height: 1.3em; color: var(--vscode-editor-foreground, var(--vscode-foreground)); } pre code { background: none; padding: 0; } code { font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: .92em; } @@ -1507,6 +1512,26 @@ log.appendChild(card); scrollIfStuck(); card.querySelectorAll('[data-act]').forEach((b) => { b.onclick = () => vscode.postMessage({ type: 'accountUpgrade' }); }); } + // Our-side outage / transient issue → a neutral "service" notice, NOT the red error and NOT the + // upgrade card (the gateway already sanitized the message; this just picks a calmer presentation). + function isServiceIssue(m){ + const code = m && m.code; + if (code === 'service_unavailable' || code === 'service_busy' || code === 'gateway_error') { return true; } + return /service_unavailable|service_busy|gateway_error/.test(String((m && m.message) || m || '')); + } + function addServiceCard(m){ + clearStatus(); finishAgentBubble(); + const busy = (m && m.code === 'service_busy') || /service_busy/.test(String((m && m.message) || '')); + const title = busy ? 'Service is busy' : 'Service temporarily unavailable'; + const fallback = busy ? 'The AI service is busy right now. Please wait a moment and try again.' + : 'The AI service is temporarily unavailable. Please try again in a moment.'; + const msg = (m && m.message) ? m.message : fallback; + const card = document.createElement('div'); card.className = 'noticecard'; + card.innerHTML = + '
' + codicon('warning') + '' + esc(title) + '
' + + '
' + esc(msg) + '
'; + log.appendChild(card); scrollIfStuck(); + } function addQuestions(m){ clearEmpty(); clearStatus(); agentBubble = null; const qs = m.questions || []; @@ -2142,6 +2167,7 @@ pending = ''; flushAll = false; doneSignaled = false; const cap = capReachedInfo(m.message); if (cap){ current = null; addUpgradeCard(cap); } + else if (isServiceIssue(m)){ current = null; addServiceCard(m); } else { const html = '' + esc(m.message) + ''; if (current){ streamSetHtml(current, html); } else { add('assistant', html); } @@ -2180,7 +2206,7 @@ else if (m.type === 'debug'){ addDebug(m); } else if (m.type === 'account'){ renderAccount(m); if (m.open) openAccount(); } else if (m.type === 'fileIndex'){ setFileIndex(m.files || []); } - else if (m.type === 'agentError'){ clearStatus(); finishAgentBubble(); const cap = capReachedInfo(m.message); if (cap){ addUpgradeCard(cap); } else { add('assistant', '' + esc(m.message) + ''); } } + else if (m.type === 'agentError'){ clearStatus(); finishAgentBubble(); const cap = capReachedInfo(m.message); if (cap){ addUpgradeCard(cap); } else if (isServiceIssue(m)){ addServiceCard(m); } else { add('assistant', '' + esc(m.message) + ''); } } else if (m.type === 'agentDone'){ clearStatus(); finishAgentBubble(); addAgentDone(m.reason, m.edits, m.credits, m.maxSteps); setStreaming(false); } else if (m.type === 'context'){ selLabel = m.label; renderChips(); } else if (m.type === 'clearContext'){ selLabel = null; renderChips(); } diff --git a/extensions/levelcode-ai/providers/openaiCompat.js b/extensions/levelcode-ai/providers/openaiCompat.js index e7cfe4f..cc213db 100644 --- a/extensions/levelcode-ai/providers/openaiCompat.js +++ b/extensions/levelcode-ai/providers/openaiCompat.js @@ -93,7 +93,7 @@ async function streamOpenAI(opts) { if (!data || data === '[DONE]') { return; } let ev; try { ev = JSON.parse(data); } catch { return; } - if (ev.error) { throw new Error((ev.error && ev.error.message) ? ev.error.message : (label + ' stream error')); } + if (ev.error) { const _e = new Error((ev.error && ev.error.message) ? ev.error.message : (label + ' stream error')); if (ev.error && ev.error.code) { _e.code = ev.error.code; } throw _e; } const t = deltaFromEvent(ev); if (t) { opts.onDelta(t); } }); @@ -184,7 +184,7 @@ async function streamOpenAIAgentTurn(opts) { if (!data || data === '[DONE]') { return; } let ev; try { ev = JSON.parse(data); } catch { return; } - if (ev.error) { throw new Error((ev.error && ev.error.message) ? ev.error.message : (label + ' stream error')); } + if (ev.error) { const _e = new Error((ev.error && ev.error.message) ? ev.error.message : (label + ' stream error')); if (ev.error && ev.error.code) { _e.code = ev.error.code; } throw _e; } if (ev.usage) { // opportunistic — only some providers include usage in the stream if (ev.usage.prompt_tokens) { usage.input_tokens = ev.usage.prompt_tokens; } if (ev.usage.completion_tokens) { usage.output_tokens = ev.usage.completion_tokens; } From 19ca5d8d257d45705f56585ca8f2f15d30f1b53a Mon Sep 17 00:00:00 2001 From: Sergii Demianchuk Date: Thu, 9 Jul 2026 23:14:49 -0400 Subject: [PATCH 7/7] =?UTF-8?q?Branding:=20correct=20GitHub=20org=20in=20p?= =?UTF-8?q?roduct=20links=20(levelcode=20=E2=86=92=20levelcodeai)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit licenseUrl / reportIssueUrl / releaseNotesUrl in product.overlay.json pointed at github.com/levelcode/levelcode; the canonical repo is github.com/levelcodeai/levelcode. Co-Authored-By: Claude Opus 4.8 --- branding/product.overlay.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/branding/product.overlay.json b/branding/product.overlay.json index aaa11bf..be91f73 100644 --- a/branding/product.overlay.json +++ b/branding/product.overlay.json @@ -26,12 +26,12 @@ "_syncComment": "LevelCode Sync (built-in Settings Sync + the `levelcode` auth provider from extensions/levelcode-sync) was REMOVED for now — the managed sync host isn't built and it duplicated the LevelCode Cloud sign-in. Removed at source: the levelcode-sync extension, this configurationSync.store block, and the Welcome 'Sync' step. Re-add when the managed sync host ships.", "licenseName": "MIT", - "licenseUrl": "https://github.com/levelcode/levelcode/blob/main/LICENSE", - "reportIssueUrl": "https://github.com/levelcode/levelcode/issues/new", + "licenseUrl": "https://github.com/levelcodeai/levelcode/blob/main/LICENSE", + "reportIssueUrl": "https://github.com/levelcodeai/levelcode/issues/new", "_downloadComment": "Download + update funnel through levelcode.ai (see site/app/download/[arch]/route.ts). downloadUrl is where the notify-only updater's Download button sends the user; releaseNotesUrl backs its 'Release Notes' button. updateUrl is the feed base — the updater GETs {updateUrl}/api/update/{target}/{quality}/{commit}; until that endpoint is live it returns 404 and the updater silently treats it as 'up to date' (notify-only, non-fatal). RELEASE NOTE: levelcode.ai must resolve to the deployed site before these links work.", "downloadUrl": "https://levelcode.ai/download", - "releaseNotesUrl": "https://github.com/levelcode/levelcode/releases/latest", + "releaseNotesUrl": "https://github.com/levelcodeai/levelcode/releases/latest", "updateUrl": "https://levelcode.ai", "enableTelemetry": false,