browser-assistant 2.8.21 → 2.8.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -286,10 +286,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
286
286
  return { ok: false, error: 'The portal submit command and IDE workspace do not match.' };
287
287
  if (request.action === 'runOnlineIde' && (!ide.active || typeof request.code !== 'string'
288
288
  || !request.code.trim() || new TextEncoder().encode(request.code).length > 12000)) {
289
- return { ok: false, error: 'The matching IDE must be active and the source must fit the terminal input limit.' };
289
+ return { ok: false, error: 'The matching IDE must be active and the app.js source must be at most 12 KB.' };
290
290
  }
291
291
  const results = await chrome.scripting.executeScript({ target: { tabId: ide.id, allFrames: true }, world: 'MAIN',
292
- func: browserAssistantOnlineIdeTerminal,
292
+ func: browserAssistantOnlineIde,
293
293
  args: [request.action, workspace, questionId, request.token, request.action === 'runOnlineIde' ? request.code : null] });
294
294
  const frameResults = results.map(entry => entry.result).filter(Boolean);
295
295
  return frameResults.length === 1 ? frameResults[0] : { ok: false, retry: true, error: 'The IDE workspace is still opening.' };
@@ -1211,6 +1211,8 @@ function mountJournalCountdown(signal) {
1211
1211
  if (result?.done) {
1212
1212
  if (result.passed)
1213
1213
  automation.completed = 1;
1214
+ else if (result.saveUnconfirmed)
1215
+ automation.error = 'Code was pasted into app.js, but the saved file did not match. Check app.js and save before running it.';
1214
1216
  else
1215
1217
  automation.error = result.submitted ? 'Grading finished. Review the test results in the IDE.' : 'The IDE command finished without a confirmed submission. Check the terminal.';
1216
1218
  stopAutomation(result.passed ? 'COMPLETED' : 'CHECK_IDE_RESULT');
@@ -88,9 +88,9 @@ function browserAssistantEditor(operation, selector, expected, value) {
88
88
  return { ok: false, error: error.message };
89
89
  }
90
90
  }
91
- // Runs in the portal IDE frame only. The terminal command is built here from
92
- // fixed operations and encoded source, never from model-generated shell text.
93
- async function browserAssistantOnlineIdeTerminal(action, workspace, questionId, token, source) {
91
+ // Run the IDE's normal paste and autosave flow. The terminal only verifies and
92
+ // grades the saved file; generated source is never passed to the shell.
93
+ async function browserAssistantOnlineIde(action, workspace, questionId, token, source) {
94
94
  if (location.origin !== 'https://ide.ccbp.in' || location.pathname.replace(/\/$/, '') !== `/playground/${workspace}`)
95
95
  return null;
96
96
  const visible = node => node.getClientRects().length > 0;
@@ -101,6 +101,7 @@ async function browserAssistantOnlineIdeTerminal(action, workspace, questionId,
101
101
  const rowContainer = input.closest('.xterm')?.querySelector('.xterm-rows');
102
102
  const rows = () => [...(rowContainer?.children || [])].map(row => row.textContent.trim()).filter(Boolean);
103
103
  const finishMarker = `BA_RESULT_${token}`;
104
+ const saveMarker = `BA_APP_JS_NOT_SAVED_${token}`;
104
105
  const reportPrefix = `https://learning.ccbp.in/ide-coding-submission/${questionId}/`;
105
106
  const reports = lines => {
106
107
  const text = lines.join('');
@@ -119,7 +120,7 @@ async function browserAssistantOnlineIdeTerminal(action, workspace, questionId,
119
120
  const report = index >= 0 && reports(lines.slice(index)).find(id => !previous.reports.includes(id));
120
121
  const passed = result === `${finishMarker}:0` && Boolean(report) && index > 0
121
122
  && lines.slice(Math.max(0, index - 3), index).includes('All test cases passed');
122
- return { ok: true, done: true, passed, submitted: Boolean(report), url: report ? reportPrefix + report : null };
123
+ return { ok: true, done: true, passed, submitted: Boolean(report), saveUnconfirmed: lines.includes(saveMarker), url: report ? reportPrefix + report : null };
123
124
  }
124
125
  if (previous?.token === token)
125
126
  return { ok: false, error: 'This IDE run was already started. Check the terminal.' };
@@ -146,35 +147,60 @@ async function browserAssistantOnlineIdeTerminal(action, workspace, questionId,
146
147
  return { ok: false, retry: true, error: 'Waiting for the workspace app.js file in Explorer.' };
147
148
  files[0].click();
148
149
  await new Promise(resolve => setTimeout(resolve, 200));
149
- const editors = [...document.querySelectorAll('.monaco-editor[data-uri]')].filter(visible);
150
- const editor = editors.length === 1 && editors[0];
151
- if (!editor || !editor.getAttribute('data-uri').endsWith(`/${prompt[2]}/app.js`)) {
150
+ const findEditor = () => {
151
+ const editors = [...document.querySelectorAll('.monaco-editor[data-uri]')].filter(visible);
152
+ const node = editors.length === 1 && editors[0];
153
+ const uri = node && node.getAttribute('data-uri');
154
+ return uri && uri.startsWith('file:') && uri.endsWith(`/${prompt[2]}/app.js`) ? node : null;
155
+ };
156
+ const editor = findEditor();
157
+ if (!editor) {
152
158
  return { ok: false, retry: true, error: 'Waiting for the app.js editor.' };
153
159
  }
160
+ const editorInput = editor.querySelector('textarea.inputarea');
154
161
  const renderedCode = editor.querySelector('.view-lines');
155
- if (!renderedCode || renderedCode.textContent.trim())
156
- return { ok: false, error: 'app.js already contains code. Review the prepared result before replacing it.' };
157
- const bytes = new TextEncoder().encode(source);
158
- const encoded = btoa(Array.from(bytes, byte => String.fromCharCode(byte)).join(''));
162
+ if (!editorInput || !renderedCode?.querySelector('.view-line'))
163
+ return { ok: false, retry: true, error: 'Waiting for the app.js editor input.' };
164
+ const model = window.monaco?.editor?.getEditors?.().find(instance => instance.getDomNode() === editor)?.getModel();
165
+ const isEmpty = () => {
166
+ if (model)
167
+ return model.getValue() === '';
168
+ // The portal bundles Monaco without a global editor API. Require the
169
+ // visible first and only row to be blank; never select or replace text.
170
+ const lineNumbers = editor.querySelectorAll('.margin-view-overlays .line-numbers');
171
+ return lineNumbers.length === 1 && lineNumbers[0].textContent.trim() === '1'
172
+ && renderedCode.querySelectorAll('.view-line').length === 1 && !renderedCode.textContent.trim();
173
+ };
174
+ if (!isEmpty())
175
+ return { ok: false, error: 'app.js is not empty or its contents could not be checked. Review the code in the editor.' };
176
+ const code = source.replace(/\r\n?/g, '\n');
177
+ const bytes = new TextEncoder().encode(code);
159
178
  const digest = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), byte => byte.toString(16).padStart(2, '0')).join('');
160
- const writer = [
161
- 'const fs = require("node:fs")',
162
- 'const [encoded, digest, folder] = process.argv.slice(1)',
163
- 'const code = Buffer.from(encoded, "base64")',
164
- 'if (require("node:crypto").createHash("sha256").update(code).digest("hex") !== digest) throw Error("Incomplete source received")',
165
- 'if (!process.cwd().endsWith("/nodejs/coding-practices/" + folder)) throw Error("Unexpected project directory")',
166
- 'const stat = fs.lstatSync("app.js")',
167
- 'if (!stat.isFile() || fs.readFileSync("app.js", "utf8").trim()) throw Error("app.js is not an empty regular file")',
168
- 'const check = require("node:child_process").spawnSync(process.execPath, ["--check"], {input: code})',
169
- 'if (check.status !== 0) throw Error(String(check.stderr || "JavaScript syntax check failed"))',
170
- 'fs.writeFileSync("app.js", code)',
171
- 'if (!fs.readFileSync("app.js").equals(code)) throw Error("Could not verify the saved file")',
172
- ].join('; ');
173
- // All arguments are restricted to letters, digits, or base64 by construction.
179
+ if (document.visibilityState !== 'visible' || findEditor() !== editor || !isEmpty()) {
180
+ return { ok: false, error: 'The app.js editor changed before pasting.' };
181
+ }
182
+ editorInput.focus();
183
+ if (document.activeElement !== editorInput || editorInput.readOnly || editorInput.disabled) {
184
+ return { ok: false, error: 'The app.js editor is not available for editing.' };
185
+ }
186
+ const pasteData = new DataTransfer();
187
+ pasteData.setData('text/plain', code);
188
+ editorInput.dispatchEvent(new ClipboardEvent('paste', { clipboardData: pasteData, bubbles: true, cancelable: true, composed: true }));
189
+ const pasteDeadline = Date.now() + 1500;
190
+ const didPaste = () => model ? model.getValue() === code : Boolean(renderedCode.textContent.trim());
191
+ while (!didPaste() && Date.now() < pasteDeadline && document.activeElement === editorInput && findEditor() === editor) {
192
+ await new Promise(resolve => setTimeout(resolve, 50));
193
+ }
194
+ if (!didPaste() || findEditor() !== editor || document.activeElement !== editorInput || document.visibilityState !== 'visible') {
195
+ return { ok: false, error: 'The IDE did not confirm the paste into app.js. Check the editor before trying again.' };
196
+ }
197
+ // Check exact saved bytes before grading; allow time for the IDE's autosave.
198
+ // All interpolated values have restricted alphabets.
199
+ const saved = `printf '%s app.js\\n' '${digest}' | sha256sum --check --status`;
174
200
  const echoMarker = `BA_COMMAND_${token}`;
175
- const command = `node -e '${writer}' '${encoded}' '${digest}' '${prompt[2]}' && ccbp submit ${workspace}; printf '\\n${finishMarker}:%s\\n' "$?" #${echoMarker}`;
201
+ const command = `( for ba_try in 1 2 3 4 5 6 7 8 9 10; do ${saved} && break; sleep 0.5; done; if ${saved}; then node --check app.js && ccbp submit ${workspace}; else printf '%s\\n' ${saveMarker}; false; fi ); printf '\\n${finishMarker}:%s\\n' "$?" #${echoMarker}`;
176
202
  if (rows().at(-1) !== before.at(-1))
177
- return { ok: false, error: 'The IDE terminal changed before submission.' };
203
+ return { ok: false, error: 'Code is in app.js, but the IDE terminal changed before grading.' };
178
204
  input.focus();
179
205
  if (document.activeElement !== input)
180
206
  return { ok: false, error: 'The IDE terminal could not be focused.' };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Browser Assistant",
4
- "version": "2.8.21",
4
+ "version": "2.8.23",
5
5
  "description": "Scans webpages for MCQs and coding problems. Powered by Groq (cloud) and Ollama (local fallback).",
6
6
  "permissions": [
7
7
  "activeTab",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-assistant",
3
- "version": "2.8.21",
3
+ "version": "2.8.23",
4
4
  "description": "Chrome extension and local Python backend for exploring webpage questions and coding exercises",
5
5
  "license": "MIT",
6
6
  "repository": {
package/server/main.py CHANGED
@@ -191,7 +191,7 @@ async def lifespan(_app: FastAPI):
191
191
  client.close()
192
192
 
193
193
 
194
- app = FastAPI(title="Browser Assistant API", version="2.8.21", lifespan=lifespan)
194
+ app = FastAPI(title="Browser Assistant API", version="2.8.23", lifespan=lifespan)
195
195
  install_local_guards(app)
196
196
 
197
197
  @app.exception_handler(HTTPException)