explorbot 0.1.25 → 0.1.26
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/dist/package.json +1 -1
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/experience.js +1 -1
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/historian/screencast.js +3 -3
- package/dist/src/ai/historian.js +1 -1
- package/dist/src/ai/navigator.js +4 -4
- package/dist/src/ai/pilot.js +8 -5
- package/dist/src/ai/planner.js +2 -4
- package/dist/src/ai/provider.js +5 -3
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher.js +4 -4
- package/dist/src/ai/session-analyst.js +4 -1
- package/dist/src/ai/task-agent.js +2 -2
- package/dist/src/commands/context-aria-command.js +1 -1
- package/dist/src/commands/explore-command.js +67 -25
- package/dist/src/commands/test-command.js +2 -1
- package/dist/src/components/App.js +1 -33
- package/dist/src/components/LogPane.js +9 -3
- package/dist/src/experience-tracker.js +2 -2
- package/dist/src/explorbot.js +1 -1
- package/dist/src/reporter.js +24 -5
- package/dist/src/utils/log-filters.js +27 -0
- package/dist/src/utils/logger.js +28 -1
- package/dist/src/utils/next-steps.js +1 -7
- package/package.json +1 -1
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/experience.ts +1 -1
- package/src/ai/historian/playwright.ts +1 -1
- package/src/ai/historian/screencast.ts +3 -3
- package/src/ai/historian.ts +1 -1
- package/src/ai/navigator.ts +4 -4
- package/src/ai/pilot.ts +8 -5
- package/src/ai/planner.ts +1 -3
- package/src/ai/provider.ts +5 -3
- package/src/ai/researcher/locators.ts +1 -1
- package/src/ai/researcher.ts +4 -4
- package/src/ai/session-analyst.ts +4 -1
- package/src/ai/task-agent.ts +2 -2
- package/src/commands/context-aria-command.ts +1 -1
- package/src/commands/explore-command.ts +68 -23
- package/src/commands/test-command.ts +2 -1
- package/src/components/App.tsx +0 -33
- package/src/components/LogPane.tsx +18 -3
- package/src/experience-tracker.ts +2 -2
- package/src/explorbot.ts +1 -1
- package/src/reporter.ts +24 -6
- package/src/utils/log-filters.ts +26 -0
- package/src/utils/logger.ts +24 -2
- package/src/utils/next-steps.ts +1 -6
package/src/components/App.tsx
CHANGED
|
@@ -14,7 +14,6 @@ import InputPane from './InputPane.js';
|
|
|
14
14
|
import InputReadline from './InputReadline.js';
|
|
15
15
|
import LogPane from './LogPane.js';
|
|
16
16
|
import PlanEditor from './PlanEditor.js';
|
|
17
|
-
import PlanPane, { type PlanSummary } from './PlanPane.js';
|
|
18
17
|
import SessionTimer from './SessionTimer.js';
|
|
19
18
|
import StateTransitionPane from './StateTransitionPane.js';
|
|
20
19
|
import TaskPane, { WINDOW_SIZE } from './TaskPane.js';
|
|
@@ -158,42 +157,20 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
158
157
|
|
|
159
158
|
const planRef = useRef<ReturnType<typeof explorBot.getCurrentPlan>>(undefined);
|
|
160
159
|
const unsubscribeRef = useRef<(() => void) | undefined>(undefined);
|
|
161
|
-
const [completedPlans, setCompletedPlans] = useState<PlanSummary[]>([]);
|
|
162
|
-
const [activePlanInfo, setActivePlanInfo] = useState<PlanSummary | null>(null);
|
|
163
160
|
|
|
164
161
|
useEffect(() => {
|
|
165
|
-
const makeSummary = (plan: NonNullable<ReturnType<typeof explorBot.getCurrentPlan>>): PlanSummary => {
|
|
166
|
-
const enabled = plan.tests.filter((t) => t.enabled);
|
|
167
|
-
return {
|
|
168
|
-
title: plan.title,
|
|
169
|
-
testCount: enabled.length,
|
|
170
|
-
passed: enabled.filter((t) => t.isSuccessful).length,
|
|
171
|
-
failed: enabled.filter((t) => t.hasFailed).length,
|
|
172
|
-
};
|
|
173
|
-
};
|
|
174
|
-
|
|
175
162
|
const subscribeToPlan = (plan: NonNullable<ReturnType<typeof explorBot.getCurrentPlan>>) => {
|
|
176
163
|
if (unsubscribeRef.current) unsubscribeRef.current();
|
|
177
164
|
|
|
178
|
-
if (planRef.current && planRef.current !== plan && planRef.current.tests.length > 0) {
|
|
179
|
-
const summary = makeSummary(planRef.current);
|
|
180
|
-
setCompletedPlans((prev) => {
|
|
181
|
-
if (prev.some((p) => p.title === summary.title)) return prev;
|
|
182
|
-
return [...prev, summary];
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
165
|
planRef.current = plan;
|
|
187
166
|
tasksRef.current = [...plan.tests];
|
|
188
167
|
setTasks(tasksRef.current);
|
|
189
168
|
setTaskScrollOffset(0);
|
|
190
|
-
setActivePlanInfo(makeSummary(plan));
|
|
191
169
|
|
|
192
170
|
let lastInProgressIdx = -1;
|
|
193
171
|
unsubscribeRef.current = plan.onTestsChange((updatedTests) => {
|
|
194
172
|
tasksRef.current = [...updatedTests];
|
|
195
173
|
setTasks(tasksRef.current);
|
|
196
|
-
setActivePlanInfo(makeSummary(plan));
|
|
197
174
|
const inProgressIdx = updatedTests.findIndex((t) => t.status === 'in_progress' && t.enabled);
|
|
198
175
|
if (inProgressIdx >= 0 && inProgressIdx !== lastInProgressIdx) {
|
|
199
176
|
lastInProgressIdx = inProgressIdx;
|
|
@@ -211,17 +188,9 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
211
188
|
subscribeToPlan(currentPlan);
|
|
212
189
|
} else if (!currentPlan && planRef.current) {
|
|
213
190
|
if (unsubscribeRef.current) unsubscribeRef.current();
|
|
214
|
-
if (planRef.current.tests.length > 0) {
|
|
215
|
-
const summary = makeSummary(planRef.current);
|
|
216
|
-
setCompletedPlans((prev) => {
|
|
217
|
-
if (prev.some((p) => p.title === summary.title)) return prev;
|
|
218
|
-
return [...prev, summary];
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
191
|
planRef.current = undefined;
|
|
222
192
|
tasksRef.current = [];
|
|
223
193
|
setTasks([]);
|
|
224
|
-
setActivePlanInfo(null);
|
|
225
194
|
}
|
|
226
195
|
}, 2000);
|
|
227
196
|
|
|
@@ -389,8 +358,6 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
389
358
|
)}
|
|
390
359
|
<Autocomplete />
|
|
391
360
|
</Box>
|
|
392
|
-
|
|
393
|
-
<PlanPane completedPlans={completedPlans} activePlan={activePlanInfo} />
|
|
394
361
|
</Box>
|
|
395
362
|
);
|
|
396
363
|
}
|
|
@@ -22,7 +22,6 @@ const LogPane: React.FC<LogPaneProps> = React.memo(({ verboseMode }) => {
|
|
|
22
22
|
const pendingLogsRef = React.useRef<LogEntry[]>([]);
|
|
23
23
|
const flushTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
24
24
|
|
|
25
|
-
const MAX_MULTILINE_LINES = 16;
|
|
26
25
|
const MAX_STEP_LINES = 8;
|
|
27
26
|
const MAX_SUBSTEP_LINES = 6;
|
|
28
27
|
|
|
@@ -115,6 +114,8 @@ const LogPane: React.FC<LogPaneProps> = React.memo(({ verboseMode }) => {
|
|
|
115
114
|
return { color: 'yellow' as const };
|
|
116
115
|
case 'debug':
|
|
117
116
|
return { color: 'gray' as const, dimColor: true };
|
|
117
|
+
case 'operation':
|
|
118
|
+
return { color: 'gray' as const, dimColor: true };
|
|
118
119
|
case 'substep':
|
|
119
120
|
return { color: 'gray' as const, dimColor: true };
|
|
120
121
|
case 'step':
|
|
@@ -146,7 +147,8 @@ const LogPane: React.FC<LogPaneProps> = React.memo(({ verboseMode }) => {
|
|
|
146
147
|
const cleaned = stripAnsi(dedent(log.content));
|
|
147
148
|
const parsed = parseMarkdownToTerminal(cleaned);
|
|
148
149
|
const lines = parsed.split('\n');
|
|
149
|
-
const
|
|
150
|
+
const maxLines = log.maxLines || 16;
|
|
151
|
+
const truncated = lines.length > maxLines ? `${lines.slice(0, maxLines).join('\n')}\n... (${lines.length - maxLines} more lines)` : parsed;
|
|
150
152
|
return (
|
|
151
153
|
<Box key={index} borderStyle="classic" borderLeft={false} borderRight={false} marginY={1} padding={1} borderColor="dim" overflow="hidden">
|
|
152
154
|
<Text color="gray" dimColor>
|
|
@@ -163,6 +165,7 @@ const LogPane: React.FC<LogPaneProps> = React.memo(({ verboseMode }) => {
|
|
|
163
165
|
type: 'multiline',
|
|
164
166
|
content: `HTML Content:\n\n${markdown}`,
|
|
165
167
|
timestamp: log.timestamp,
|
|
168
|
+
maxLines: 10,
|
|
166
169
|
};
|
|
167
170
|
|
|
168
171
|
return renderLogEntry(multilineLog, index);
|
|
@@ -182,6 +185,18 @@ const LogPane: React.FC<LogPaneProps> = React.memo(({ verboseMode }) => {
|
|
|
182
185
|
);
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
if (log.type === 'operation') {
|
|
189
|
+
return (
|
|
190
|
+
<Box key={index} marginLeft={2} flexDirection="column">
|
|
191
|
+
{lines.map((line, lineIndex) => (
|
|
192
|
+
<Text key={`${index}-${lineIndex}`} {...styles}>
|
|
193
|
+
{lineIndex === 0 ? `· ${line}` : ` ${line}`}
|
|
194
|
+
</Text>
|
|
195
|
+
))}
|
|
196
|
+
</Box>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
185
200
|
if (log.type === 'step') {
|
|
186
201
|
return (
|
|
187
202
|
<Box key={index} flexDirection="column" paddingLeft={2}>
|
|
@@ -212,7 +227,7 @@ const LogPane: React.FC<LogPaneProps> = React.memo(({ verboseMode }) => {
|
|
|
212
227
|
);
|
|
213
228
|
};
|
|
214
229
|
|
|
215
|
-
const maxLogs =
|
|
230
|
+
const maxLogs = 80;
|
|
216
231
|
const visibleLogs = logs.length > maxLogs ? logs.slice(-maxLogs) : logs;
|
|
217
232
|
return <Box flexDirection="column">{visibleLogs.map((log, index) => renderLogEntry(log, index)).filter(Boolean)}</Box>;
|
|
218
233
|
});
|
|
@@ -188,7 +188,7 @@ export class ExperienceTracker {
|
|
|
188
188
|
const updatedContent = `${newEntry}\n\n${content}`;
|
|
189
189
|
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
190
190
|
|
|
191
|
-
tag('
|
|
191
|
+
tag('operation').log(`Added ACTION to: ${stateHash}.md`);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
writeFlow(state: ActionResult, body: string, relatedUrls?: string[]): void {
|
|
@@ -218,7 +218,7 @@ export class ExperienceTracker {
|
|
|
218
218
|
const updatedContent = `${body}\n${content}`;
|
|
219
219
|
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
220
220
|
|
|
221
|
-
tag('
|
|
221
|
+
tag('operation').log(`Added FLOW to: ${stateHash}.md`);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
getAllExperience(): ExperienceFile[] {
|
package/src/explorbot.ts
CHANGED
|
@@ -487,7 +487,7 @@ export class ExplorBot {
|
|
|
487
487
|
return;
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
-
tag('multiline').log(markdown);
|
|
490
|
+
tag('multiline').log(markdown, { maxLines: 22 });
|
|
491
491
|
|
|
492
492
|
const filePath = this.agentSessionAnalyst().writeReport(markdown);
|
|
493
493
|
tag('info').log(`Session report saved: ${relativeToCwd(filePath)}`);
|
package/src/reporter.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { Client } from '@testomatio/reporter';
|
|
3
3
|
import type { Step } from '@testomatio/reporter/types/types.js';
|
|
4
|
-
import {
|
|
4
|
+
import { outputPath } from './config.js';
|
|
5
5
|
import type { ReporterConfig } from './config.js';
|
|
6
6
|
import type { StateManager } from './state-manager.js';
|
|
7
7
|
import { Stats } from './stats.js';
|
|
@@ -103,11 +103,13 @@ export class Reporter {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
const result = await withQuietReporterLogs(async () => {
|
|
107
|
+
this.client = new Client({ apiKey: process.env.TESTOMATIO || '', title: this.buildTitle() });
|
|
108
|
+
const timeoutMs = Number(process.env.TESTOMATIO_TIMEOUT_MS || '15000');
|
|
109
|
+
const timeoutPromise = new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
return await Promise.race([this.client.createRun({ configuration: { exploratory: true } }).then(() => 'success' as const), timeoutPromise]);
|
|
112
|
+
});
|
|
111
113
|
|
|
112
114
|
if (result === 'timeout') {
|
|
113
115
|
debugLog('Reporter run creation timed out');
|
|
@@ -294,7 +296,9 @@ export class Reporter {
|
|
|
294
296
|
}
|
|
295
297
|
|
|
296
298
|
try {
|
|
297
|
-
await
|
|
299
|
+
await withQuietReporterLogs(async () => {
|
|
300
|
+
await this.client.updateRunStatus('finished');
|
|
301
|
+
});
|
|
298
302
|
this.isRunStarted = false;
|
|
299
303
|
debugLog('Testomat.io run finished');
|
|
300
304
|
} catch (error) {
|
|
@@ -340,3 +344,17 @@ export class Reporter {
|
|
|
340
344
|
return;
|
|
341
345
|
}
|
|
342
346
|
}
|
|
347
|
+
|
|
348
|
+
async function withQuietReporterLogs<T>(fn: () => Promise<T>): Promise<T> {
|
|
349
|
+
const previousLevel = process.env.TESTOMATIO_LOG_LEVEL;
|
|
350
|
+
process.env.TESTOMATIO_LOG_LEVEL = 'ERROR';
|
|
351
|
+
try {
|
|
352
|
+
return await fn();
|
|
353
|
+
} finally {
|
|
354
|
+
if (previousLevel === undefined) {
|
|
355
|
+
process.env.TESTOMATIO_LOG_LEVEL = undefined;
|
|
356
|
+
} else {
|
|
357
|
+
process.env.TESTOMATIO_LOG_LEVEL = previousLevel;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class RecentStepFilter {
|
|
2
|
+
private recentStepKeys = new Map<string, number>();
|
|
3
|
+
|
|
4
|
+
constructor(private ttlMs = 15000) {}
|
|
5
|
+
|
|
6
|
+
shouldSuppress(content: string, now = Date.now()): boolean {
|
|
7
|
+
const key = normalizeStepCommand(content);
|
|
8
|
+
if (!key) return false;
|
|
9
|
+
|
|
10
|
+
for (const [existingKey, timestamp] of this.recentStepKeys) {
|
|
11
|
+
if (now - timestamp > this.ttlMs) {
|
|
12
|
+
this.recentStepKeys.delete(existingKey);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (this.recentStepKeys.has(key)) return true;
|
|
17
|
+
this.recentStepKeys.set(key, now);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeStepCommand(content: string): string | null {
|
|
23
|
+
const normalized = content.replace(/\s+/g, ' ').trim();
|
|
24
|
+
if (!normalized.startsWith('I.')) return null;
|
|
25
|
+
return normalized.toLowerCase();
|
|
26
|
+
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -8,9 +8,10 @@ import { marked } from 'marked';
|
|
|
8
8
|
import stripAnsi from 'strip-ansi';
|
|
9
9
|
import { ConfigParser } from '../config.js';
|
|
10
10
|
import { Observability } from '../observability.ts';
|
|
11
|
+
import { RecentStepFilter } from './log-filters.ts';
|
|
11
12
|
import { parseMarkdownToTerminal } from './markdown-terminal.ts';
|
|
12
13
|
|
|
13
|
-
export type LogType = 'info' | 'success' | 'error' | 'warning' | 'debug' | 'substep' | 'step' | 'multiline' | 'html' | 'input';
|
|
14
|
+
export type LogType = 'info' | 'success' | 'error' | 'warning' | 'debug' | 'substep' | 'operation' | 'step' | 'multiline' | 'html' | 'input';
|
|
14
15
|
|
|
15
16
|
export interface TaggedLogEntry {
|
|
16
17
|
type: LogType;
|
|
@@ -18,6 +19,7 @@ export interface TaggedLogEntry {
|
|
|
18
19
|
timestamp?: Date;
|
|
19
20
|
originalArgs?: any[];
|
|
20
21
|
namespace?: string;
|
|
22
|
+
maxLines?: number;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
type LogEntry = TaggedLogEntry;
|
|
@@ -77,6 +79,7 @@ const debugFilter = new DebugFilter();
|
|
|
77
79
|
class ConsoleDestination implements LogDestination {
|
|
78
80
|
private verboseMode = false;
|
|
79
81
|
private forceEnabled = false;
|
|
82
|
+
private recentSteps = new RecentStepFilter();
|
|
80
83
|
|
|
81
84
|
isEnabled(): boolean {
|
|
82
85
|
return this.forceEnabled || !process.env.INK_RUNNING;
|
|
@@ -93,6 +96,8 @@ class ConsoleDestination implements LogDestination {
|
|
|
93
96
|
write(entry: TaggedLogEntry): void {
|
|
94
97
|
if (entry.type === 'debug') return;
|
|
95
98
|
if (entry.type === 'html') return;
|
|
99
|
+
if (entry.type === 'operation' && !this.verboseMode) return;
|
|
100
|
+
if (entry.type === 'step' && !this.verboseMode && this.recentSteps.shouldSuppress(entry.content)) return;
|
|
96
101
|
let content = entry.content;
|
|
97
102
|
if (entry.type === 'multiline') {
|
|
98
103
|
const cleaned = stripAnsi(dedent(entry.content));
|
|
@@ -107,6 +112,8 @@ class ConsoleDestination implements LogDestination {
|
|
|
107
112
|
content = chalk.yellow(content);
|
|
108
113
|
} else if (entry.type === 'step') {
|
|
109
114
|
content = chalk.gray(` ${content}`);
|
|
115
|
+
} else if (entry.type === 'operation') {
|
|
116
|
+
content = chalk.gray(` · ${content}`);
|
|
110
117
|
} else if (entry.type === 'substep') {
|
|
111
118
|
content = chalk.gray(` > ${content}`);
|
|
112
119
|
}
|
|
@@ -247,6 +254,7 @@ class ReactDestination implements LogDestination {
|
|
|
247
254
|
}
|
|
248
255
|
|
|
249
256
|
private shouldWrite(entry: TaggedLogEntry): boolean {
|
|
257
|
+
if (entry.type === 'operation' && !this.debugMode) return false;
|
|
250
258
|
if (entry.type !== 'debug') return true;
|
|
251
259
|
if (this.debugMode) return true;
|
|
252
260
|
if (!entry.namespace) return true;
|
|
@@ -306,7 +314,7 @@ class CaptainDestination implements LogDestination {
|
|
|
306
314
|
|
|
307
315
|
stopCapture(): string[] {
|
|
308
316
|
this.capturing = false;
|
|
309
|
-
const logs = this.entries.filter((e) => e.type !== 'debug' && e.type !== 'html' && e.type !== 'multiline').map((e) => `[${e.type}] ${e.content}`);
|
|
317
|
+
const logs = this.entries.filter((e) => e.type !== 'debug' && e.type !== 'html' && e.type !== 'multiline' && e.type !== 'operation').map((e) => `[${e.type}] ${e.content}`);
|
|
310
318
|
this.entries = [];
|
|
311
319
|
return logs;
|
|
312
320
|
}
|
|
@@ -432,6 +440,7 @@ class Logger {
|
|
|
432
440
|
return;
|
|
433
441
|
}
|
|
434
442
|
|
|
443
|
+
const options = this.extractLogOptions(type, args);
|
|
435
444
|
let content = this.processArgs(args);
|
|
436
445
|
if (type === 'step' && args[0]?.toCode) {
|
|
437
446
|
content = args[0].toCode();
|
|
@@ -441,6 +450,7 @@ class Logger {
|
|
|
441
450
|
content,
|
|
442
451
|
timestamp: new Date(),
|
|
443
452
|
originalArgs: args,
|
|
453
|
+
maxLines: options?.maxLines,
|
|
444
454
|
};
|
|
445
455
|
|
|
446
456
|
if (this.file.isEnabled()) this.file.write(entry);
|
|
@@ -480,6 +490,18 @@ class Logger {
|
|
|
480
490
|
multiline(...args: any[]): void {
|
|
481
491
|
this.log('multiline', ...args);
|
|
482
492
|
}
|
|
493
|
+
|
|
494
|
+
private extractLogOptions(type: LogType, args: any[]): { maxLines?: number } | null {
|
|
495
|
+
if (type !== 'multiline') return null;
|
|
496
|
+
const last = args[args.length - 1];
|
|
497
|
+
if (!last || typeof last !== 'object' || Array.isArray(last)) return null;
|
|
498
|
+
if (!('maxLines' in last)) return null;
|
|
499
|
+
|
|
500
|
+
args.pop();
|
|
501
|
+
const maxLines = Number(last.maxLines);
|
|
502
|
+
if (!Number.isFinite(maxLines) || maxLines <= 0) return null;
|
|
503
|
+
return { maxLines };
|
|
504
|
+
}
|
|
483
505
|
}
|
|
484
506
|
|
|
485
507
|
const logger = Logger.getInstance();
|
package/src/utils/next-steps.ts
CHANGED
|
@@ -42,10 +42,5 @@ export function printNextSteps(sections: NextStepSection[]): void {
|
|
|
42
42
|
blocks.push(lines.join('\n'));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
if (i > 0) tag('info').log('');
|
|
47
|
-
for (const line of blocks[i].split('\n')) {
|
|
48
|
-
tag('info').log(line);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
45
|
+
tag('multiline').log(blocks.join('\n\n'), { maxLines: 18 });
|
|
51
46
|
}
|