explorbot 0.1.11 → 0.1.13
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.
- package/README.md +12 -2
- package/bin/explorbot-cli.ts +21 -21
- package/dist/bin/explorbot-cli.js +3 -3
- package/dist/package.json +4 -3
- package/dist/rules/researcher/container-rules.md +2 -0
- package/dist/src/action-result.js +2 -1
- package/dist/src/action.js +5 -10
- package/dist/src/ai/captain.js +0 -2
- package/dist/src/ai/driller.js +1108 -0
- package/dist/src/ai/historian/codeceptjs.js +2 -2
- package/dist/src/ai/historian/experience.js +1 -0
- package/dist/src/ai/historian/playwright.js +4 -4
- package/dist/src/ai/historian/screencast.js +121 -0
- package/dist/src/ai/historian.js +5 -3
- package/dist/src/ai/pilot.js +31 -22
- package/dist/src/ai/rules.js +3 -5
- package/dist/src/ai/session-analyst.js +117 -0
- package/dist/src/ai/tester.js +13 -2
- package/dist/src/commands/base-command.js +6 -6
- package/dist/src/commands/drill-command.js +3 -2
- package/dist/src/commands/exit-command.js +1 -0
- package/dist/src/commands/explore-command.js +20 -3
- package/dist/src/components/AddRule.js +1 -1
- package/dist/src/explorbot.js +52 -9
- package/dist/src/explorer.js +11 -9
- package/dist/src/reporter.js +68 -4
- package/dist/src/state-manager.js +4 -3
- package/dist/src/stats.js +5 -0
- package/dist/src/utils/aria.js +354 -529
- package/dist/src/utils/hooks-runner.js +2 -8
- package/dist/src/utils/html.js +371 -0
- package/dist/src/utils/strings.js +15 -0
- package/dist/src/utils/unique-names.js +12 -1
- package/dist/src/utils/url-matcher.js +6 -1
- package/dist/src/utils/web-element.js +27 -24
- package/dist/src/utils/xpath.js +1 -1
- package/package.json +4 -3
- package/rules/researcher/container-rules.md +2 -0
- package/src/action-result.ts +2 -1
- package/src/action.ts +5 -12
- package/src/ai/captain.ts +0 -2
- package/src/ai/driller.ts +1194 -0
- package/src/ai/historian/codeceptjs.ts +2 -2
- package/src/ai/historian/experience.ts +3 -2
- package/src/ai/historian/playwright.ts +5 -5
- package/src/ai/historian/screencast.ts +133 -0
- package/src/ai/historian.ts +7 -5
- package/src/ai/pilot.ts +31 -21
- package/src/ai/rules.ts +3 -5
- package/src/ai/session-analyst.ts +133 -0
- package/src/ai/tester.ts +15 -2
- package/src/commands/base-command.ts +6 -6
- package/src/commands/drill-command.ts +3 -2
- package/src/commands/exit-command.ts +1 -0
- package/src/commands/explore-command.ts +22 -3
- package/src/components/AddRule.tsx +1 -1
- package/src/config.ts +10 -0
- package/src/explorbot.ts +59 -11
- package/src/explorer.ts +11 -9
- package/src/reporter.ts +68 -4
- package/src/state-manager.ts +4 -3
- package/src/stats.ts +7 -0
- package/src/utils/aria.ts +367 -537
- package/src/utils/hooks-runner.ts +2 -6
- package/src/utils/html.ts +381 -0
- package/src/utils/strings.ts +17 -0
- package/src/utils/unique-names.ts +13 -0
- package/src/utils/url-matcher.ts +5 -1
- package/src/utils/web-element.ts +31 -28
- package/src/utils/xpath.ts +1 -1
- package/dist/src/ai/bosun.js +0 -456
- package/src/ai/bosun.ts +0 -571
package/dist/src/explorbot.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { ActionResult } from "./action-result.js";
|
|
4
|
-
import { Bosun } from "./ai/bosun.js";
|
|
5
4
|
import { Captain } from "./ai/captain.js";
|
|
5
|
+
import { Driller } from "./ai/driller.js";
|
|
6
6
|
import { ExperienceCompactor } from "./ai/experience-compactor.js";
|
|
7
7
|
import { Fisherman } from "./ai/fisherman.js";
|
|
8
8
|
import { Historian } from "./ai/historian.js";
|
|
@@ -13,6 +13,7 @@ import { AIProvider } from "./ai/provider.js";
|
|
|
13
13
|
import { Quartermaster } from "./ai/quartermaster.js";
|
|
14
14
|
import { Rerunner } from "./ai/rerunner.js";
|
|
15
15
|
import { Researcher } from "./ai/researcher.js";
|
|
16
|
+
import { SessionAnalyst } from "./ai/session-analyst.js";
|
|
16
17
|
import { Tester } from "./ai/tester.js";
|
|
17
18
|
import { createAgentTools } from "./ai/tools.js";
|
|
18
19
|
import { ApiClient } from "./api/api-client.js";
|
|
@@ -24,6 +25,7 @@ import Explorer from "./explorer.js";
|
|
|
24
25
|
import { KnowledgeTracker } from "./knowledge-tracker.js";
|
|
25
26
|
import { Plan } from "./test-plan.js";
|
|
26
27
|
import { setVerboseMode, tag } from "./utils/logger.js";
|
|
28
|
+
import { relativeToCwd } from "./utils/next-steps.js";
|
|
27
29
|
import { sanitizeFilename } from "./utils/strings.js";
|
|
28
30
|
export class ExplorBot {
|
|
29
31
|
configParser;
|
|
@@ -38,6 +40,8 @@ export class ExplorBot {
|
|
|
38
40
|
lastPlanError = null;
|
|
39
41
|
lastSavedPlanPath = null;
|
|
40
42
|
agents = {};
|
|
43
|
+
sessionPlans = [];
|
|
44
|
+
lastReportedTestCount = 0;
|
|
41
45
|
constructor(options = {}) {
|
|
42
46
|
this.options = options;
|
|
43
47
|
this.configParser = ConfigParser.getInstance();
|
|
@@ -218,7 +222,10 @@ export class ExplorBot {
|
|
|
218
222
|
return (this.agents.historian ||= this.createAgent(({ ai, explorer, config }) => {
|
|
219
223
|
const experienceTracker = explorer.getStateManager().getExperienceTracker();
|
|
220
224
|
const reporter = explorer.getReporter();
|
|
221
|
-
return new Historian(ai, experienceTracker, reporter, explorer.getStateManager(), config,
|
|
225
|
+
return new Historian(ai, experienceTracker, reporter, explorer.getStateManager(), config, {
|
|
226
|
+
recorder: explorer.getPlaywrightRecorder(),
|
|
227
|
+
helper: explorer.playwrightHelper,
|
|
228
|
+
});
|
|
222
229
|
}));
|
|
223
230
|
}
|
|
224
231
|
agentRerunner() {
|
|
@@ -236,14 +243,15 @@ export class ExplorBot {
|
|
|
236
243
|
}
|
|
237
244
|
return this.agents.rerunner;
|
|
238
245
|
}
|
|
239
|
-
|
|
240
|
-
return (this.agents.
|
|
241
|
-
const researcher = this.agentResearcher();
|
|
246
|
+
agentDriller() {
|
|
247
|
+
return (this.agents.driller ||= this.createAgent(({ ai, explorer }) => {
|
|
242
248
|
const navigator = this.agentNavigator();
|
|
243
|
-
|
|
244
|
-
return new Bosun(explorer, ai, researcher, navigator, tools);
|
|
249
|
+
return new Driller(explorer, ai, navigator);
|
|
245
250
|
}));
|
|
246
251
|
}
|
|
252
|
+
agentSessionAnalyst() {
|
|
253
|
+
return (this.agents.sessionAnalyst ||= this.createAgent(({ ai }) => new SessionAnalyst(ai)));
|
|
254
|
+
}
|
|
247
255
|
agentFisherman() {
|
|
248
256
|
const fishermanConfig = this.config.ai?.agents?.fisherman;
|
|
249
257
|
const hasApiConfig = !!this.config.api;
|
|
@@ -306,7 +314,7 @@ export class ExplorBot {
|
|
|
306
314
|
}
|
|
307
315
|
this.lastPlanError = null;
|
|
308
316
|
try {
|
|
309
|
-
this.
|
|
317
|
+
this.setCurrentPlan(await planner.plan(feature, opts.style, opts.extend, opts.completedPlans));
|
|
310
318
|
}
|
|
311
319
|
catch (err) {
|
|
312
320
|
this.lastPlanError = err instanceof Error ? err : new Error(String(err));
|
|
@@ -373,10 +381,45 @@ export class ExplorBot {
|
|
|
373
381
|
if (!existsSync(planPath)) {
|
|
374
382
|
throw new Error(`Plan file not found: ${planPath}`);
|
|
375
383
|
}
|
|
376
|
-
this.
|
|
384
|
+
this.setCurrentPlan(Plan.fromMarkdown(planPath));
|
|
377
385
|
return this.currentPlan;
|
|
378
386
|
}
|
|
379
387
|
setCurrentPlan(plan) {
|
|
380
388
|
this.currentPlan = plan;
|
|
389
|
+
if (plan && !this.sessionPlans.includes(plan)) {
|
|
390
|
+
this.sessionPlans.push(plan);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
getSessionTests() {
|
|
394
|
+
return this.sessionPlans.flatMap((p) => p.tests.filter((t) => t.startTime != null));
|
|
395
|
+
}
|
|
396
|
+
async printSessionAnalysis() {
|
|
397
|
+
const analystConfig = this.config.ai?.agents?.analyst;
|
|
398
|
+
if (analystConfig?.enabled === false)
|
|
399
|
+
return;
|
|
400
|
+
const tests = this.getSessionTests();
|
|
401
|
+
if (tests.length === 0)
|
|
402
|
+
return;
|
|
403
|
+
if (tests.length === this.lastReportedTestCount)
|
|
404
|
+
return;
|
|
405
|
+
try {
|
|
406
|
+
const markdown = await this.agentSessionAnalyst().analyze(tests);
|
|
407
|
+
if (!markdown) {
|
|
408
|
+
this.lastReportedTestCount = tests.length;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
tag('multiline').log(markdown);
|
|
412
|
+
const filePath = this.agentSessionAnalyst().writeReport(markdown);
|
|
413
|
+
tag('info').log(`Session report saved: ${relativeToCwd(filePath)}`);
|
|
414
|
+
const reporter = this.explorer?.getReporter();
|
|
415
|
+
if (reporter?.isEnabled()) {
|
|
416
|
+
await reporter.setRunDescription(markdown);
|
|
417
|
+
}
|
|
418
|
+
this.lastReportedTestCount = tests.length;
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
422
|
+
tag('warning').log(`Session analysis failed: ${message}`);
|
|
423
|
+
}
|
|
381
424
|
}
|
|
382
425
|
}
|
package/dist/src/explorer.js
CHANGED
|
@@ -15,8 +15,9 @@ import { KnowledgeTracker } from './knowledge-tracker.js';
|
|
|
15
15
|
import { PlaywrightRecorder } from "./playwright-recorder.js";
|
|
16
16
|
import { Reporter } from "./reporter.js";
|
|
17
17
|
import { StateManager } from './state-manager.js';
|
|
18
|
+
import { ELEMENT_EXTRACTION_CONFIG, getElementDataExtractorSource } from "./utils/html.js";
|
|
18
19
|
import { createDebug, log, tag } from './utils/logger.js';
|
|
19
|
-
import { WebElement
|
|
20
|
+
import { WebElement } from "./utils/web-element.js";
|
|
20
21
|
const debugLog = createDebug('explorbot:explorer');
|
|
21
22
|
const FATAL_BROWSER_ERRORS = /Frame was detached|Target closed|Execution context was destroyed|Protocol error|Session closed/i;
|
|
22
23
|
const RECOVERABLE_NAVIGATION_ERRORS = /net::ERR_ABORTED|page\.screenshot.*Timeout|waiting for fonts to load/i;
|
|
@@ -270,11 +271,11 @@ class Explorer {
|
|
|
270
271
|
async getEidxInContainer(containerCss) {
|
|
271
272
|
const page = this.playwrightHelper.page;
|
|
272
273
|
try {
|
|
273
|
-
const selector = containerCss ? `${containerCss} [
|
|
274
|
+
const selector = containerCss ? `${containerCss} [${ELEMENT_EXTRACTION_CONFIG.attrs.eidx}]` : `[${ELEMENT_EXTRACTION_CONFIG.attrs.eidx}]`;
|
|
274
275
|
const elements = await page.locator(selector).all();
|
|
275
276
|
const result = [];
|
|
276
277
|
for (const el of elements) {
|
|
277
|
-
const attr = await el.getAttribute(
|
|
278
|
+
const attr = await el.getAttribute(ELEMENT_EXTRACTION_CONFIG.attrs.eidx);
|
|
278
279
|
if (attr)
|
|
279
280
|
result.push(attr);
|
|
280
281
|
}
|
|
@@ -293,7 +294,7 @@ class Explorer {
|
|
|
293
294
|
const page = this.playwrightHelper.page;
|
|
294
295
|
const base = container ? page.locator(container) : page;
|
|
295
296
|
const el = locator.startsWith('//') ? base.locator(`xpath=${locator}`) : base.locator(locator);
|
|
296
|
-
return await el.first().getAttribute(
|
|
297
|
+
return await el.first().getAttribute(ELEMENT_EXTRACTION_CONFIG.attrs.eidx);
|
|
297
298
|
}
|
|
298
299
|
catch (error) {
|
|
299
300
|
if (this.isFatalBrowserError(error)) {
|
|
@@ -612,6 +613,7 @@ function toCodeceptjsTest(test) {
|
|
|
612
613
|
codeceptjsTest.fullTitle = () => `${parent.title} ${test.scenario}`;
|
|
613
614
|
codeceptjsTest.state = 'pending';
|
|
614
615
|
codeceptjsTest.notes = test.getPrintableNotes();
|
|
616
|
+
codeceptjsTest._explorbotTest = test;
|
|
615
617
|
return codeceptjsTest;
|
|
616
618
|
}
|
|
617
619
|
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
@@ -629,7 +631,7 @@ function parseAriaRefs(ariaSnapshot) {
|
|
|
629
631
|
return entries;
|
|
630
632
|
}
|
|
631
633
|
export async function annotatePageElements(page) {
|
|
632
|
-
const ariaSnapshot = await page.locator('body').ariaSnapshot({
|
|
634
|
+
const ariaSnapshot = await page.locator('body').ariaSnapshot({ mode: 'ai' });
|
|
633
635
|
const refEntries = parseAriaRefs(ariaSnapshot);
|
|
634
636
|
const byRole = new Map();
|
|
635
637
|
for (const { role, name, ref } of refEntries) {
|
|
@@ -643,21 +645,21 @@ export async function annotatePageElements(page) {
|
|
|
643
645
|
const elements = [];
|
|
644
646
|
for (const [role, entries] of byRole) {
|
|
645
647
|
try {
|
|
646
|
-
const rawList = await page.getByRole(role).evaluateAll((domElements, [data, extractFnStr]) => {
|
|
648
|
+
const rawList = await page.getByRole(role).evaluateAll((domElements, [data, extractFnStr, config]) => {
|
|
647
649
|
const extract = new Function(`return ${extractFnStr}`)();
|
|
648
650
|
const results = [];
|
|
649
651
|
let ariaIdx = 0;
|
|
650
652
|
for (const el of domElements) {
|
|
651
653
|
if (ariaIdx >= data.length)
|
|
652
654
|
break;
|
|
653
|
-
el.setAttribute(
|
|
654
|
-
const elData = extract(el);
|
|
655
|
+
el.setAttribute(config.attrs.eidx, data[ariaIdx].ref);
|
|
656
|
+
const elData = extract(el, config);
|
|
655
657
|
if (elData)
|
|
656
658
|
results.push(elData);
|
|
657
659
|
ariaIdx++;
|
|
658
660
|
}
|
|
659
661
|
return results;
|
|
660
|
-
}, [entries,
|
|
662
|
+
}, [entries, getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG]);
|
|
661
663
|
for (const raw of rawList) {
|
|
662
664
|
elements.push(WebElement.fromRawData(raw, role));
|
|
663
665
|
}
|
package/dist/src/reporter.js
CHANGED
|
@@ -14,8 +14,19 @@ export class Reporter {
|
|
|
14
14
|
if (this.reporterEnabled && (!process.env.TESTOMATIO || config?.html)) {
|
|
15
15
|
this.configureHtmlPipe();
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
if (this.reporterEnabled && config?.markdown) {
|
|
18
|
+
this.configureMarkdownPipe();
|
|
19
|
+
}
|
|
20
|
+
if (this.reporterEnabled) {
|
|
21
|
+
this.configureRunGroup(config?.runGroup);
|
|
22
|
+
}
|
|
23
|
+
debugLog('Reporter initialized', {
|
|
24
|
+
enabled: this.reporterEnabled,
|
|
25
|
+
testomatio: Boolean(process.env.TESTOMATIO),
|
|
26
|
+
html: Boolean(process.env.TESTOMATIO_HTML_REPORT_SAVE),
|
|
27
|
+
markdown: Boolean(process.env.TESTOMATIO_MARKDOWN_REPORT_SAVE),
|
|
28
|
+
runGroup: process.env.TESTOMATIO_RUNGROUP_TITLE || null,
|
|
29
|
+
});
|
|
19
30
|
}
|
|
20
31
|
buildTitle() {
|
|
21
32
|
if (process.env.TESTOMATIO_TITLE)
|
|
@@ -39,7 +50,31 @@ export class Reporter {
|
|
|
39
50
|
configureHtmlPipe() {
|
|
40
51
|
process.env.TESTOMATIO_HTML_REPORT_SAVE = '1';
|
|
41
52
|
process.env.TESTOMATIO_HTML_REPORT_FOLDER = outputPath('reports');
|
|
42
|
-
|
|
53
|
+
process.env.TESTOMATIO_HTML_FILENAME = `${Stats.sessionLabel()}.html`;
|
|
54
|
+
debugLog('HTML report pipe configured', {
|
|
55
|
+
folder: process.env.TESTOMATIO_HTML_REPORT_FOLDER,
|
|
56
|
+
filename: process.env.TESTOMATIO_HTML_FILENAME,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
configureMarkdownPipe() {
|
|
60
|
+
process.env.TESTOMATIO_MARKDOWN_REPORT_SAVE = '1';
|
|
61
|
+
process.env.TESTOMATIO_MARKDOWN_REPORT_FOLDER = outputPath('reports');
|
|
62
|
+
process.env.TESTOMATIO_MARKDOWN_FILENAME = `${Stats.sessionLabel()}-tests.md`;
|
|
63
|
+
debugLog('Markdown report pipe configured', {
|
|
64
|
+
folder: process.env.TESTOMATIO_MARKDOWN_REPORT_FOLDER,
|
|
65
|
+
filename: process.env.TESTOMATIO_MARKDOWN_FILENAME,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
configureRunGroup(runGroup) {
|
|
69
|
+
if (process.env.TESTOMATIO_RUNGROUP_TITLE)
|
|
70
|
+
return;
|
|
71
|
+
if (runGroup === null)
|
|
72
|
+
return;
|
|
73
|
+
if (runGroup) {
|
|
74
|
+
process.env.TESTOMATIO_RUNGROUP_TITLE = runGroup;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
process.env.TESTOMATIO_RUNGROUP_TITLE = `Explorbot ${new Date().toISOString().slice(0, 10)}`;
|
|
43
78
|
}
|
|
44
79
|
async startRun() {
|
|
45
80
|
if (this.isRunStarted) {
|
|
@@ -78,6 +113,7 @@ export class Reporter {
|
|
|
78
113
|
const noteEntries = Object.entries(test.notes)
|
|
79
114
|
.map(([timestampKey, note]) => ({
|
|
80
115
|
startTime: note.startTime,
|
|
116
|
+
endTime: note.endTime,
|
|
81
117
|
message: note.message,
|
|
82
118
|
status: note.status,
|
|
83
119
|
screenshot: note.screenshot,
|
|
@@ -105,7 +141,7 @@ export class Reporter {
|
|
|
105
141
|
const step = {
|
|
106
142
|
category: 'user',
|
|
107
143
|
title: noteEntry.message,
|
|
108
|
-
duration: 0,
|
|
144
|
+
duration: Math.max(0, Math.round(noteEntry.endTime - noteEntry.startTime)),
|
|
109
145
|
status: noteEntry.status || 'none',
|
|
110
146
|
steps: noteSteps.length > 0 ? noteSteps : undefined,
|
|
111
147
|
};
|
|
@@ -148,6 +184,7 @@ export class Reporter {
|
|
|
148
184
|
meta = Object.fromEntries(Object.entries(meta).filter(([, v]) => v));
|
|
149
185
|
}
|
|
150
186
|
const steps = this.combineStepsAndNotes(test, screenshotFile);
|
|
187
|
+
const durationMs = test.getDurationMs();
|
|
151
188
|
const testData = {
|
|
152
189
|
rid: test.id,
|
|
153
190
|
title: test.scenario,
|
|
@@ -162,6 +199,7 @@ export class Reporter {
|
|
|
162
199
|
files: Object.values(test.artifacts) || [],
|
|
163
200
|
message: test.summary || this.extractLastNoteMessage(test) || '',
|
|
164
201
|
meta,
|
|
202
|
+
time: durationMs != null ? Math.round(durationMs) : 0,
|
|
165
203
|
};
|
|
166
204
|
debugLog(testData);
|
|
167
205
|
await this.client.addTestRun(status, testData);
|
|
@@ -187,6 +225,32 @@ export class Reporter {
|
|
|
187
225
|
isEnabled() {
|
|
188
226
|
return this.isRunStarted;
|
|
189
227
|
}
|
|
228
|
+
async setRunDescription(text) {
|
|
229
|
+
if (!this.isRunStarted)
|
|
230
|
+
return;
|
|
231
|
+
if (!process.env.TESTOMATIO)
|
|
232
|
+
return;
|
|
233
|
+
const runId = this.client.runId;
|
|
234
|
+
if (!runId)
|
|
235
|
+
return;
|
|
236
|
+
const baseUrl = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
237
|
+
const url = `${baseUrl}/api/reporter/${runId}`;
|
|
238
|
+
try {
|
|
239
|
+
const response = await fetch(url, {
|
|
240
|
+
method: 'PUT',
|
|
241
|
+
headers: { 'Content-Type': 'application/json' },
|
|
242
|
+
body: JSON.stringify({ api_key: process.env.TESTOMATIO, description: text }),
|
|
243
|
+
});
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
debugLog('Run description update failed:', response.status, response.statusText);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
debugLog('Run description updated');
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
debugLog('Failed to update run description:', error);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
190
254
|
extractLastNoteMessage(test) {
|
|
191
255
|
const notes = Object.values(test.notes);
|
|
192
256
|
if (notes.length === 0)
|
|
@@ -70,8 +70,8 @@ export class StateManager {
|
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
72
72
|
* Extract state path from full URL
|
|
73
|
-
* Removes domain, port, protocol
|
|
74
|
-
* Keeps path and hash: /path/to/page#section
|
|
73
|
+
* Removes domain, port, protocol
|
|
74
|
+
* Keeps path, query, and hash: /path/to/page?tab=users#section
|
|
75
75
|
*/
|
|
76
76
|
/**
|
|
77
77
|
* Update current state from ActionResult and record transition if state changed
|
|
@@ -418,7 +418,8 @@ export class StateManager {
|
|
|
418
418
|
export function normalizeUrl(url) {
|
|
419
419
|
try {
|
|
420
420
|
const parsed = new URL(url, 'http://localhost');
|
|
421
|
-
|
|
421
|
+
const path = parsed.pathname.replace(/^\/+|\/+$/g, '');
|
|
422
|
+
return `${path}${parsed.search}${parsed.hash}`;
|
|
422
423
|
}
|
|
423
424
|
catch {
|
|
424
425
|
return url.replace(/^\/+|\/+$/g, '');
|
package/dist/src/stats.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { uniqExplorationName } from "./utils/unique-names.js";
|
|
1
2
|
export class Stats {
|
|
2
3
|
static startTime = Date.now();
|
|
4
|
+
static sessionName = uniqExplorationName();
|
|
3
5
|
static researches = 0;
|
|
4
6
|
static tests = 0;
|
|
5
7
|
static plans = 0;
|
|
@@ -42,4 +44,7 @@ export class Stats {
|
|
|
42
44
|
const totalTokens = Object.values(Stats.models).reduce((sum, m) => sum + m.total, 0);
|
|
43
45
|
return totalTokens > 0;
|
|
44
46
|
}
|
|
47
|
+
static sessionLabel() {
|
|
48
|
+
return `${Stats.mode || 'session'}-${Stats.sessionName}`;
|
|
49
|
+
}
|
|
45
50
|
}
|