Skip to content

Commit 785564f

Browse files
ndemiancclaude
andcommitted
LevelCode editor: AI extension + Atom++→LevelCode rename
atom-ai → levelcode-ai: multi-provider BYOK + metered LevelCode Cloud gateway (free/paid/Auto model picker, credits roster, 👍/👎 reactions), agent mode with tools, inline completions, edit-review, Agent Sketch, Customize panel, onboarding. Full rename: settings levelcode.ai.*, commands levelcode.*, secret-storage keys, display strings, deep-link id levelcode.levelcode-ai, gateway /api/levelcode/v1. Calm theme-following sign-in modal + chevron "level-up" loading spinner (replaces the atom-orbit mark). Editor tests 6/6. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 790efa9 commit 785564f

34 files changed

Lines changed: 1188 additions & 404 deletions

‎extensions/atom-ai/README.md‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1-
# Atom++ — AI
1+
# LevelCode — AI
22

3-
Native, Claude-first AI for Atom++. The chat lives in its own activity-bar panel and talks
4-
**directly** to the provider — there is no Atom++ server in between.
3+
Native, Claude-first AI for LevelCode. The chat lives in its own activity-bar panel and talks
4+
**directly** to the provider — there is no LevelCode server in between.
55

66
**Providers**
77

88
- **Claude** (Anthropic) — bring your own API key (`console.anthropic.com`). The key is stored in
99
VS Code SecretStorage (your OS keychain), never written to settings.
10-
- **Ollama** — local models, no key. Point `atompp.ai.ollama.url` at your server and set
11-
`atompp.ai.ollama.model` to a model you've pulled.
10+
- **Ollama** — local models, no key. Point `levelcode.ai.ollama.url` at your server and set
11+
`levelcode.ai.ollama.model` to a model you've pulled.
1212

1313
**Use**
1414

15-
1. Click the **Atom++ AI** icon in the activity bar.
16-
2. First send prompts for your Anthropic key (or use the 🔑 button / `Atom++: AI: Set Anthropic API Key`).
15+
1. Click the **LevelCode AI** icon in the activity bar.
16+
2. First send prompts for your Anthropic key (or use the 🔑 button / `LevelCode: AI: Set Anthropic API Key`).
1717
3. Type and press Enter. Responses stream in; **Stop** cancels.
1818
4. Select code in the editor → **Add selection** (or `⌥⌘A`) to send it as context.
1919

20-
**Settings:** `atompp.ai.provider`, `atompp.ai.claude.model`, `atompp.ai.claude.maxTokens`,
21-
`atompp.ai.ollama.url`, `atompp.ai.ollama.model`.
20+
**Settings:** `levelcode.ai.provider`, `levelcode.ai.claude.model`, `levelcode.ai.claude.maxTokens`,
21+
`levelcode.ai.ollama.url`, `levelcode.ai.ollama.model`.
2222

2323
**Edit with inline review:** select code → `⌥⌘E` (or right-click → *AI: Edit Selection…*) → describe the change. The model rewrites it and the result appears **inline in the editor** with changed lines highlighted green and a CodeLens above each change offering **✓ Keep / ✗ Undo** (plus **Keep All / Undo All** at the top). Keep accepts a change; Undo restores the original for that hunk. Nothing is final until you Keep.
2424

‎extensions/atom-ai/agent.js‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*---------------------------------------------------------------------------------------------
2-
* Atom++ — AI (M4, feature 1: agentic multi-file tasks)
2+
* LevelCode — AI (M4, feature 1: agentic multi-file tasks)
33
*
44
* An autonomous coding agent. Given a goal, Claude reads/searches/edits files and runs
55
* commands via tools to accomplish it. Read-only tools run automatically; every file write
@@ -17,7 +17,7 @@ const providers = require('./providers/index');
1717
const { formatVerifyFeedback, verifyOutcome, looksUnrunnable, sniffPort, looksReady } = require('./verify');
1818

1919
const SYSTEM_BASE = [
20-
"You are Atom++'s built-in autonomous coding agent. You accomplish the user's goal in their",
20+
"You are LevelCode's built-in autonomous coding agent. You accomplish the user's goal in their",
2121
'workspace using the provided tools. Rules:',
2222
'- Be DECISIVE and FAST. Read only the file you are changing (plus at most 1 other if truly needed), then ACT. Do NOT write long multi-point plans — at most ONE short sentence before each tool call.',
2323
'- Start acting within your first 1-2 turns. Use read_file / list_files / search to understand code before editing — never guess file contents.',
@@ -460,7 +460,7 @@ async function runAgent(ctx) {
460460
if (r.unrunnable) {
461461
// The verify command itself is misconfigured — tell the user, never make the agent "fix" it.
462462
dbg('verify.unrunnable', { tail: String(r.cmdTail || '').slice(0, 160) });
463-
ctx.post({ type: 'agentTool', icon: 'warning', text: 'verify command couldn’t run — check the atompp.ai.verify.command setting' });
463+
ctx.post({ type: 'agentTool', icon: 'warning', text: 'verify command couldn’t run — check the levelcode.ai.verify.command setting' });
464464
}
465465
if (outcome === 'pass') { dbg('verify.pass', { cmd: !!cmd, touched: touchedCount, unrunnable: r.unrunnable }); return false; }
466466
if (outcome === 'exhausted') {
@@ -503,6 +503,18 @@ async function runAgent(ctx) {
503503
ctx.post({ type: 'contextUsage', input: (turn.usage.input_tokens || 0) + (turn.usage.cache_read_input_tokens || 0) + (turn.usage.cache_creation_input_tokens || 0), output: turn.usage.output_tokens || 0, limit: ctx.contextLimit || 200000, model: ctx.model, system: systemTokensEst, tools: TOOLS_TOKENS_EST });
504504
}
505505

506+
// Reasoning models (e.g. Kimi K2.7 Code) emit <think>…</think> inline in the text
507+
// block. Strip it from the STORED turn so the reasoning isn't re-sent — and
508+
// re-metered on the gateway — on every later turn. (The chat view hides it too.)
509+
for (const c of turn.content) {
510+
if (c.type === 'text' && typeof c.text === 'string') {
511+
let t = c.text.replace(/<think\b[^>]*>[\s\S]*?<\/think>/gi, ''); // completed pairs
512+
const close = t.match(/<\/think\s*>/i); // dangling closer (implicit open)
513+
if (close) { t = t.slice(close.index + close[0].length); }
514+
c.text = t.replace(/^\s+/, '');
515+
}
516+
}
517+
506518
// Guard: never push an empty assistant message — Anthropic 400s on content:[] (e.g. a
507519
// turn cut off before any output). Substitute a placeholder and recover or stop.
508520
const hasReal = turn.content.some((c) => (c.type === 'text' && c.text.trim()) || c.type === 'tool_use');

‎extensions/atom-ai/aiEdit.js‎

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*---------------------------------------------------------------------------------------------
2-
* Atom++ — AI (M2: edit selection → reviewable diff)
2+
* LevelCode — AI (M2: edit selection → reviewable diff)
33
*
44
* Select code → describe a change → the model rewrites it → review a side-by-side diff →
55
* ✓ Keep / ✗ Discard from the diff editor's toolbar. The proposed text is served through a
@@ -12,8 +12,8 @@
1212
const vscode = require('vscode');
1313
const path = require('path');
1414

15-
const SCHEME = 'atompp-ai';
16-
const CTX_DIFF_ACTIVE = 'atompp.ai.diffActive';
15+
const SCHEME = 'levelcode-ai';
16+
const CTX_DIFF_ACTIVE = 'levelcode.ai.diffActive';
1717

1818
/** @type {Map<string,string>} virtual-doc uri -> content */
1919
const contents = new Map();
@@ -85,7 +85,7 @@ async function applyEdit() {
8585
cleanup(propUri);
8686
await closeDiffTab(propUri);
8787
updateDiffContext();
88-
vscode.window.setStatusBarMessage('$(check) Atom++ AI: edit applied', 2000);
88+
vscode.window.setStatusBarMessage('$(check) LevelCode AI: edit applied', 2000);
8989
}
9090

9191
async function discardEdit() {
@@ -94,7 +94,7 @@ async function discardEdit() {
9494
cleanup(propUri);
9595
await closeDiffTab(propUri);
9696
updateDiffContext();
97-
vscode.window.setStatusBarMessage('Atom++ AI: edit discarded', 1500);
97+
vscode.window.setStatusBarMessage('LevelCode AI: edit discarded', 1500);
9898
}
9999

100100
/** Whole-line range covering the selection. */
@@ -109,11 +109,11 @@ function fullLineRange(editor) {
109109
async function editSelection(deps) {
110110
const ed = vscode.window.activeTextEditor;
111111
if (!ed || ed.selection.isEmpty) {
112-
vscode.window.showInformationMessage('Atom++ AI: select the code you want to edit first.');
112+
vscode.window.showInformationMessage('LevelCode AI: select the code you want to edit first.');
113113
return;
114114
}
115115
const instruction = await vscode.window.showInputBox({
116-
title: 'Atom++ AI — Edit selection',
116+
title: 'LevelCode AI — Edit selection',
117117
prompt: 'Describe the change',
118118
placeHolder: 'e.g. add error handling and a JSDoc comment',
119119
ignoreFocusOut: true
@@ -129,15 +129,15 @@ async function editSelection(deps) {
129129
let result = '';
130130
try {
131131
await vscode.window.withProgress(
132-
{ location: vscode.ProgressLocation.Notification, title: 'Atom++ AI is editing…', cancellable: true },
132+
{ location: vscode.ProgressLocation.Notification, title: 'LevelCode AI is editing…', cancellable: true },
133133
async (_progress, token) => {
134134
const ac = new AbortController();
135135
token.onCancellationRequested(() => ac.abort());
136136
const onDelta = (d) => { result += d; };
137137
const req = await deps.prepProviderRequest({ prompt: true });
138138
if (!req.ok) {
139139
throw new Error(
140-
req.reason === 'baseURL' ? 'Set a base URL for the custom OpenAI-compatible provider first (atompp.ai.baseURL).'
140+
req.reason === 'baseURL' ? 'Set a base URL for the custom OpenAI-compatible provider first (levelcode.ai.baseURL).'
141141
: req.reason === 'insecureBaseURL' ? 'Refusing to send your API key over plain http to a non-local host. Use an https (or localhost) base URL.'
142142
: 'No API key set for ' + req.label + '.');
143143
}
@@ -149,12 +149,12 @@ async function editSelection(deps) {
149149
}
150150
);
151151
} catch (e) {
152-
vscode.window.showErrorMessage('Atom++ AI edit failed: ' + String((e && e.message) || e));
152+
vscode.window.showErrorMessage('LevelCode AI edit failed: ' + String((e && e.message) || e));
153153
return;
154154
}
155155

156156
const proposed = stripFences(result);
157-
if (!proposed.trim()) { vscode.window.showWarningMessage('Atom++ AI: no edit was produced.'); return; }
157+
if (!proposed.trim()) { vscode.window.showWarningMessage('LevelCode AI: no edit was produced.'); return; }
158158

159159
const ext = path.extname(doc.uri.fsPath) || '.txt';
160160
const n = ++counter;
@@ -165,17 +165,17 @@ async function editSelection(deps) {
165165
pending.set(propUri.toString(), { docUri: doc.uri, range, newText: proposed, origUri });
166166

167167
await vscode.commands.executeCommand('vscode.diff', origUri, propUri,
168-
'Atom++ AI: proposed edit — use ✓ Keep / ✗ Discard above', { preview: true });
168+
'LevelCode AI: proposed edit — use ✓ Keep / ✗ Discard above', { preview: true });
169169
updateDiffContext();
170170
}
171171

172172
/** @param {vscode.ExtensionContext} context @param {object} deps */
173173
function registerAiEdit(context, deps) {
174174
registerProvider(context);
175175
context.subscriptions.push(
176-
vscode.commands.registerCommand('atompp.ai.editSelection', () => editSelection(deps)),
177-
vscode.commands.registerCommand('atompp.ai.applyEdit', applyEdit),
178-
vscode.commands.registerCommand('atompp.ai.discardEdit', discardEdit),
176+
vscode.commands.registerCommand('levelcode.ai.editSelection', () => editSelection(deps)),
177+
vscode.commands.registerCommand('levelcode.ai.applyEdit', applyEdit),
178+
vscode.commands.registerCommand('levelcode.ai.discardEdit', discardEdit),
179179
vscode.window.tabGroups.onDidChangeTabGroups(updateDiffContext),
180180
vscode.window.tabGroups.onDidChangeTabs((e) => {
181181
for (const tab of e.closed) {

‎extensions/atom-ai/customize.js‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*---------------------------------------------------------------------------------------------
2-
* Atom++ — Customize
2+
* LevelCode — Customize
33
*
44
* One product surface for everything: AI key/model, appearance + keymap, sync, updates, and
55
* power-editing/hackability. A self-contained webview whose buttons run the same commands the
@@ -29,7 +29,7 @@ function activeModelId(ai, providerId) {
2929
}
3030

3131
async function gatherStatus(context) {
32-
const ai = vscode.workspace.getConfiguration('atompp.ai');
32+
const ai = vscode.workspace.getConfiguration('levelcode.ai');
3333
const providerId = normId(ai.get('provider', 'claude'));
3434
const p = getProvider(providerId) || getProvider('claude');
3535
const skey = secretStorageKey(providerId);
@@ -48,9 +48,9 @@ async function gatherStatus(context) {
4848
theme: vscode.workspace.getConfiguration('workbench').get('colorTheme', ''),
4949
syncEnabled: !!vscode.workspace.getConfiguration().get('configurationSync.keybindingsPerPlatform') || undefined,
5050
// Sync is stripped from public builds until the managed host ships (strip-unreleased.mjs), so
51-
// only show its section when the atom-sync extension is actually present (its command exists).
52-
syncAvailable: (await vscode.commands.getCommands(true)).includes('atompp.sync.setUp'),
53-
updates: vscode.workspace.getConfiguration('atompp.update').get('enabled', true)
51+
// only show its section when the sync feature is present (its command exists).
52+
syncAvailable: (await vscode.commands.getCommands(true)).includes('levelcode.sync.setUp'),
53+
updates: vscode.workspace.getConfiguration('levelcode.update').get('enabled', true)
5454
};
5555
}
5656

@@ -77,29 +77,29 @@ function htmlFor(nonce, s) {
7777
row('Model', (s.model || '(default)') + (s.modelCaps ? ' · ' + s.modelCaps : '')) +
7878
row('Agent (tool use)', s.agentReady ? 'available' : 'chat only for this model', s.agentReady) +
7979
row('Inline completions', s.completions ? 'on' : 'off', s.completions) +
80-
'<div class="btns">' + btn('Change model / provider', 'atompp.ai.pickModel', true) +
81-
btn(keyBtn, 'atompp.ai.setApiKey', !s.keySet && !s.noKey) +
82-
btn('Sign in to Atom++', 'atompp.ai.account') +
83-
btn('Open chat', 'atompp.ai.focus') +
84-
btn('Agent Sketch', 'atompp.ai.sketch') + '</div>');
80+
'<div class="btns">' + btn('Change model / provider', 'levelcode.ai.pickModel', true) +
81+
btn(keyBtn, 'levelcode.ai.setApiKey', !s.keySet && !s.noKey) +
82+
btn('Sign in to LevelCode', 'levelcode.ai.account') +
83+
btn('Open chat', 'levelcode.ai.focus') +
84+
btn('Agent Sketch', 'levelcode.ai.sketch') + '</div>');
8585

8686
const look = section('🎨', 'Appearance & keys',
8787
row('Theme', s.theme || '') +
8888
'<div class="btns">' + btn('Choose theme', 'workbench.action.selectTheme') +
89-
btn('Apply keymap preset', 'atompp.keymap.apply') +
90-
btn('Import from VS Code', 'atompp.import.vscode') + '</div>');
89+
btn('Apply keymap preset', 'levelcode.keymap.apply') +
90+
btn('Import from VS Code', 'levelcode.import.vscode') + '</div>');
9191

9292
const sync = s.syncAvailable ? section('🔄', 'Sync',
9393
'<p class="muted">Carry your settings, keybindings, theme, MCP & skills across machines.</p>' +
94-
'<div class="btns">' + btn('Set up Sync', 'atompp.sync.setUp') + btn('Sync status', 'atompp.sync.status') + '</div>') : '';
94+
'<div class="btns">' + btn('Set up Sync', 'levelcode.sync.setUp') + btn('Sync status', 'levelcode.sync.status') + '</div>') : '';
9595

9696
const upd = section('⬆️', 'Updates',
9797
row('Auto-check', s.updates ? 'on' : 'off', s.updates) +
98-
'<div class="btns">' + btn('Check for updates', 'atompp.update.check') + '</div>');
98+
'<div class="btns">' + btn('Check for updates', 'levelcode.update.check') + '</div>');
9999

100100
const hack = section('🛠️', 'Hackability',
101101
'<p class="muted">Make it yours — a user init script and your own packages, hot-reloaded.</p>' +
102-
'<div class="btns">' + btn('Open init script', 'atompp.init.edit') + btn('Generate a package', 'atompp.package.generate') + '</div>');
102+
'<div class="btns">' + btn('Open init script', 'levelcode.init.edit') + btn('Generate a package', 'levelcode.package.generate') + '</div>');
103103

104104
return '<!doctype html><html><head><meta charset="utf-8">' +
105105
'<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; style-src \'unsafe-inline\'; script-src \'nonce-' + nonce + '\';">' +
@@ -121,7 +121,7 @@ function htmlFor(nonce, s) {
121121
'.b.primary{background:var(--vscode-button-background);color:var(--vscode-button-foreground);}' +
122122
'.b.primary:hover{background:var(--vscode-button-hoverBackground);}' +
123123
'</style></head><body>' +
124-
'<h1>Customize Atom++</h1><p class="sub">Everything in one place. Buttons run the matching command.</p>' +
124+
'<h1>Customize LevelCode</h1><p class="sub">Everything in one place. Buttons run the matching command.</p>' +
125125
ai + look + sync + upd + hack +
126126
'<script nonce="' + nonce + '">const v=acquireVsCodeApi();' +
127127
'document.querySelectorAll(".b").forEach(b=>b.onclick=()=>v.postMessage({type:"run",command:b.dataset.cmd}));' +
@@ -130,13 +130,13 @@ function htmlFor(nonce, s) {
130130

131131
async function openCustomize(context) {
132132
if (panel) { panel.reveal(); return; }
133-
panel = vscode.window.createWebviewPanel('atompp.customize', 'Customize Atom++', vscode.ViewColumn.Active, { enableScripts: true, retainContextWhenHidden: true });
133+
panel = vscode.window.createWebviewPanel('levelcode.customize', 'Customize LevelCode', vscode.ViewColumn.Active, { enableScripts: true, retainContextWhenHidden: true });
134134
panel.onDidDispose(() => { panel = undefined; });
135135
const nonce = String(Date.now()) + Math.random().toString(36).slice(2);
136136
const render = async () => { if (panel) { panel.webview.html = htmlFor(nonce, await gatherStatus(context)); } };
137137
panel.webview.onDidReceiveMessage(async (m) => {
138138
if (m && m.type === 'run' && m.command) {
139-
try { await vscode.commands.executeCommand(m.command); } catch (e) { vscode.window.showErrorMessage('Atom++: ' + ((e && e.message) || e)); }
139+
try { await vscode.commands.executeCommand(m.command); } catch (e) { vscode.window.showErrorMessage('LevelCode: ' + ((e && e.message) || e)); }
140140
setTimeout(render, 500); // refresh status after the action
141141
}
142142
});

0 commit comments

Comments
 (0)