@link-assistant/hive-mind 1.52.0 ā 1.53.0
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/CHANGELOG.md +12 -0
- package/package.json +2 -1
- package/src/agent-token-usage.lib.mjs +96 -0
- package/src/agent.lib.mjs +50 -145
- package/src/agent.prompts.lib.mjs +18 -0
- package/src/claude.budget-stats.lib.mjs +41 -61
- package/src/claude.lib.mjs +23 -15
- package/src/claude.prompts.lib.mjs +1 -0
- package/src/codex.lib.mjs +57 -5
- package/src/codex.prompts.lib.mjs +1 -0
- package/src/github-cost-info.lib.mjs +70 -0
- package/src/github.lib.mjs +2 -54
- package/src/opencode.lib.mjs +62 -0
- package/src/opencode.prompts.lib.mjs +18 -0
- package/src/playwright-mcp.lib.mjs +298 -0
- package/src/solve.config.lib.mjs +6 -1
- package/src/solve.mjs +24 -27
- package/src/solve.restart-shared.lib.mjs +29 -2
package/src/claude.lib.mjs
CHANGED
|
@@ -13,10 +13,12 @@ import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs
|
|
|
13
13
|
import { createInteractiveHandler } from './interactive-mode.lib.mjs';
|
|
14
14
|
import { initProgressMonitoring } from './solve.progress-monitoring.lib.mjs';
|
|
15
15
|
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
16
|
+
import Decimal from 'decimal.js-light';
|
|
16
17
|
import { displayBudgetStats, createEmptySubSessionUsage, accumulateModelUsage, displayModelUsage, displayCostComparison, mergeResultModelUsage, createSubAgentCallEntry, accumulateSubAgentUsage } from './claude.budget-stats.lib.mjs';
|
|
17
18
|
import { buildClaudeResumeCommand } from './claude.command-builder.lib.mjs';
|
|
18
19
|
import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
|
|
19
20
|
import { CLAUDE_MODELS as availableModels } from './models/index.mjs'; // Issue #1221
|
|
21
|
+
import { buildMcpConfigWithoutPlaywright } from './playwright-mcp.lib.mjs';
|
|
20
22
|
export { availableModels }; // Re-export for backward compatibility
|
|
21
23
|
const showResumeCommand = async (sessionId, tempDir, claudePath, model, log) => {
|
|
22
24
|
if (!sessionId || !tempDir) return;
|
|
@@ -429,51 +431,48 @@ export const checkModelVisionCapability = async modelId => {
|
|
|
429
431
|
return false;
|
|
430
432
|
}
|
|
431
433
|
};
|
|
432
|
-
/** Calculate USD cost for a model's usage with detailed breakdown */
|
|
434
|
+
/** Calculate USD cost for a model's usage with detailed breakdown (Issue #1600: uses Decimal for precision) */
|
|
433
435
|
export const calculateModelCost = (usage, modelInfo, includeBreakdown = false) => {
|
|
434
436
|
if (!modelInfo || !modelInfo.cost) {
|
|
435
437
|
return includeBreakdown ? { total: 0, breakdown: null } : 0;
|
|
436
438
|
}
|
|
437
439
|
const cost = modelInfo.cost;
|
|
440
|
+
const million = new Decimal(1000000);
|
|
438
441
|
const breakdown = {
|
|
439
442
|
input: { tokens: 0, costPerMillion: 0, cost: 0 },
|
|
440
443
|
cacheWrite: { tokens: 0, costPerMillion: 0, cost: 0 },
|
|
441
444
|
cacheRead: { tokens: 0, costPerMillion: 0, cost: 0 },
|
|
442
445
|
output: { tokens: 0, costPerMillion: 0, cost: 0 },
|
|
443
446
|
};
|
|
444
|
-
// Input tokens cost (per million tokens)
|
|
445
447
|
if (usage.inputTokens && cost.input) {
|
|
446
448
|
breakdown.input = {
|
|
447
449
|
tokens: usage.inputTokens,
|
|
448
450
|
costPerMillion: cost.input,
|
|
449
|
-
cost: (usage.inputTokens
|
|
451
|
+
cost: new Decimal(usage.inputTokens).div(million).mul(new Decimal(cost.input)).toNumber(),
|
|
450
452
|
};
|
|
451
453
|
}
|
|
452
|
-
// Cache creation tokens cost
|
|
453
454
|
if (usage.cacheCreationTokens && cost.cache_write) {
|
|
454
455
|
breakdown.cacheWrite = {
|
|
455
456
|
tokens: usage.cacheCreationTokens,
|
|
456
457
|
costPerMillion: cost.cache_write,
|
|
457
|
-
cost: (usage.cacheCreationTokens
|
|
458
|
+
cost: new Decimal(usage.cacheCreationTokens).div(million).mul(new Decimal(cost.cache_write)).toNumber(),
|
|
458
459
|
};
|
|
459
460
|
}
|
|
460
|
-
// Cache read tokens cost
|
|
461
461
|
if (usage.cacheReadTokens && cost.cache_read) {
|
|
462
462
|
breakdown.cacheRead = {
|
|
463
463
|
tokens: usage.cacheReadTokens,
|
|
464
464
|
costPerMillion: cost.cache_read,
|
|
465
|
-
cost: (usage.cacheReadTokens
|
|
465
|
+
cost: new Decimal(usage.cacheReadTokens).div(million).mul(new Decimal(cost.cache_read)).toNumber(),
|
|
466
466
|
};
|
|
467
467
|
}
|
|
468
|
-
// Output tokens cost
|
|
469
468
|
if (usage.outputTokens && cost.output) {
|
|
470
469
|
breakdown.output = {
|
|
471
470
|
tokens: usage.outputTokens,
|
|
472
471
|
costPerMillion: cost.output,
|
|
473
|
-
cost: (usage.outputTokens
|
|
472
|
+
cost: new Decimal(usage.outputTokens).div(million).mul(new Decimal(cost.output)).toNumber(),
|
|
474
473
|
};
|
|
475
474
|
}
|
|
476
|
-
const totalCost = breakdown.input.cost
|
|
475
|
+
const totalCost = new Decimal(breakdown.input.cost).plus(breakdown.cacheWrite.cost).plus(breakdown.cacheRead.cost).plus(breakdown.output.cost).toNumber();
|
|
477
476
|
if (includeBreakdown) {
|
|
478
477
|
return {
|
|
479
478
|
total: totalCost,
|
|
@@ -619,7 +618,7 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
|
|
|
619
618
|
let totalCacheCreationTokens = 0;
|
|
620
619
|
let totalCacheReadTokens = 0;
|
|
621
620
|
let totalOutputTokens = 0;
|
|
622
|
-
let
|
|
621
|
+
let totalCostDecimal = new Decimal(0);
|
|
623
622
|
let hasCostData = false;
|
|
624
623
|
for (const usage of Object.values(modelUsage)) {
|
|
625
624
|
totalInputTokens += usage.inputTokens;
|
|
@@ -627,7 +626,7 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
|
|
|
627
626
|
totalCacheReadTokens += usage.cacheReadTokens;
|
|
628
627
|
totalOutputTokens += usage.outputTokens;
|
|
629
628
|
if (usage.costUSD !== null) {
|
|
630
|
-
|
|
629
|
+
totalCostDecimal = totalCostDecimal.plus(new Decimal(usage.costUSD));
|
|
631
630
|
hasCostData = true;
|
|
632
631
|
}
|
|
633
632
|
}
|
|
@@ -642,7 +641,7 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
|
|
|
642
641
|
cacheReadTokens: totalCacheReadTokens,
|
|
643
642
|
outputTokens: totalOutputTokens,
|
|
644
643
|
totalTokens,
|
|
645
|
-
totalCostUSD: hasCostData ?
|
|
644
|
+
totalCostUSD: hasCostData ? totalCostDecimal.toNumber() : null,
|
|
646
645
|
// Issue #1501: Peak context usage (max single-request fill) and dedup stats
|
|
647
646
|
peakContextUsage: globalPeakContext,
|
|
648
647
|
duplicateEntriesSkipped: duplicateCount,
|
|
@@ -774,6 +773,14 @@ export const executeClaudeCommand = async params => {
|
|
|
774
773
|
await log(`š Resuming from session: ${argv.resume}`);
|
|
775
774
|
claudeArgs = `--resume ${argv.resume} ${claudeArgs}`;
|
|
776
775
|
}
|
|
776
|
+
let mcpConfigPath = null;
|
|
777
|
+
if (argv.playwrightMcp === false) {
|
|
778
|
+
mcpConfigPath = await buildMcpConfigWithoutPlaywright(log);
|
|
779
|
+
if (mcpConfigPath) {
|
|
780
|
+
claudeArgs += ` --strict-mcp-config --mcp-config "${mcpConfigPath}"`;
|
|
781
|
+
await log('š Playwright MCP physically disabled for this session via --strict-mcp-config', { verbose: true });
|
|
782
|
+
}
|
|
783
|
+
}
|
|
777
784
|
claudeArgs += ` -p "${escapedPrompt}" --append-system-prompt "${escapedSystemPrompt}"`;
|
|
778
785
|
const fullCommand = `(cd "${tempDir}" && ${claudePath} ${claudeArgs} | jq -c .)`;
|
|
779
786
|
await log(`\n${formatAligned('š', 'Raw command:', '')}`);
|
|
@@ -797,11 +804,12 @@ export const executeClaudeCommand = async params => {
|
|
|
797
804
|
if (!isNewVersion && thinkLevel) await log(`š Thinking level (via keywords): ${thinkLevel}`, { verbose: true });
|
|
798
805
|
}
|
|
799
806
|
const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
|
|
807
|
+
const mcpDisableArgs = mcpConfigPath ? ['--strict-mcp-config', '--mcp-config', mcpConfigPath] : [];
|
|
800
808
|
if (argv.resume) {
|
|
801
809
|
const simpleEscapedPrompt = prompt.replace(/"/g, '\\"');
|
|
802
|
-
execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
|
|
810
|
+
execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
|
|
803
811
|
} else {
|
|
804
|
-
execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
812
|
+
execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
805
813
|
}
|
|
806
814
|
await log(`${formatAligned('š', 'Command details:', '')}`);
|
|
807
815
|
await log(formatAligned('š', 'Working directory:', tempDir, 2));
|
|
@@ -268,6 +268,7 @@ GitHub CLI command patterns.
|
|
|
268
268
|
Playwright MCP usage (browser automation via mcp__playwright__* tools).
|
|
269
269
|
- When you develop frontend web applications (HTML, CSS, JavaScript, React, Vue, Angular, etc.), use Playwright MCP tools to test the UI in a real browser.
|
|
270
270
|
- When WebFetch tool fails to retrieve expected content (e.g., returns empty content, JavaScript-rendered pages, or login-protected pages), use Playwright MCP tools (browser_navigate, browser_snapshot) as a fallback for web browsing.
|
|
271
|
+
- When WebSearch tool fails or returns insufficient results, use Playwright MCP tools (browser_navigate, browser_snapshot) as a fallback for internet search.
|
|
271
272
|
- When you need to interact with dynamic web pages that require JavaScript execution, use Playwright MCP tools.
|
|
272
273
|
- When you need to visually verify how a web page looks or take screenshots, use browser_take_screenshot from Playwright MCP.
|
|
273
274
|
- When you need to fill forms, click buttons, or perform user interactions on web pages, use Playwright MCP tools (browser_click, browser_type, browser_fill_form).
|
package/src/codex.lib.mjs
CHANGED
|
@@ -21,8 +21,9 @@ import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
|
21
21
|
import { mapModelToId, resolveCodexReasoningEffort } from './codex.options.lib.mjs';
|
|
22
22
|
import { createInteractiveHandler } from './interactive-mode.lib.mjs';
|
|
23
23
|
import { initProgressMonitoring } from './solve.progress-monitoring.lib.mjs';
|
|
24
|
+
import { getCodexPlaywrightMcpDisableConfigArgs } from './playwright-mcp.lib.mjs';
|
|
24
25
|
|
|
25
|
-
const CODEX_USAGE_FIELD_NAMES = ['input_tokens', 'cached_input_tokens', 'output_tokens'];
|
|
26
|
+
const CODEX_USAGE_FIELD_NAMES = ['input_tokens', 'cached_input_tokens', 'output_tokens', 'cache_write_tokens', 'cache_creation_input_tokens', 'reasoning_tokens', 'input_tokens_details.cached_tokens', 'input_tokens_details.cache_read_tokens', 'input_tokens_details.cache_write_tokens', 'input_tokens_details.cache_creation_tokens', 'input_tokens_details.cache_creation_input_tokens', 'output_tokens_details.reasoning_tokens'];
|
|
26
27
|
const getCodexExecEnv = (verbose = false) => (verbose ? { ...process.env, RUST_LOG: 'debug' } : { ...process.env });
|
|
27
28
|
const CODEX_MODEL_DIAGNOSTIC_PATHS = [
|
|
28
29
|
['model', data => data?.model],
|
|
@@ -32,6 +33,40 @@ const CODEX_MODEL_DIAGNOSTIC_PATHS = [
|
|
|
32
33
|
['message.model', data => data?.message?.model],
|
|
33
34
|
];
|
|
34
35
|
|
|
36
|
+
const createCodexTokenFieldAvailability = () => ({
|
|
37
|
+
inputTokens: false,
|
|
38
|
+
outputTokens: false,
|
|
39
|
+
reasoningTokens: false,
|
|
40
|
+
cacheReadTokens: false,
|
|
41
|
+
cacheWriteTokens: false,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const hasOwnPath = (object, pathName) => {
|
|
45
|
+
let cursor = object;
|
|
46
|
+
for (const part of pathName.split('.')) {
|
|
47
|
+
if (!cursor || typeof cursor !== 'object' || !Object.hasOwn(cursor, part)) return false;
|
|
48
|
+
cursor = cursor[part];
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const getPathValue = (object, pathName) => pathName.split('.').reduce((cursor, part) => cursor?.[part], object);
|
|
54
|
+
|
|
55
|
+
const getFirstObservedNumber = (object, pathNames) => {
|
|
56
|
+
for (const pathName of pathNames) {
|
|
57
|
+
if (!hasOwnPath(object, pathName)) continue;
|
|
58
|
+
const value = getPathValue(object, pathName);
|
|
59
|
+
return Number.isFinite(value) ? value : 0;
|
|
60
|
+
}
|
|
61
|
+
return 0;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const hasAnyObservedPath = (object, pathNames) => pathNames.some(pathName => hasOwnPath(object, pathName));
|
|
65
|
+
|
|
66
|
+
const CODEX_CACHE_READ_USAGE_PATHS = ['cached_input_tokens', 'input_tokens_details.cached_tokens', 'input_tokens_details.cache_read_tokens'];
|
|
67
|
+
const CODEX_CACHE_WRITE_USAGE_PATHS = ['cache_write_tokens', 'cache_creation_input_tokens', 'input_tokens_details.cache_write_tokens', 'input_tokens_details.cache_creation_tokens', 'input_tokens_details.cache_creation_input_tokens'];
|
|
68
|
+
const CODEX_REASONING_USAGE_PATHS = ['reasoning_tokens', 'output_tokens_details.reasoning_tokens'];
|
|
69
|
+
|
|
35
70
|
export const createCodexTokenUsage = requestedModelId => ({
|
|
36
71
|
inputTokens: 0,
|
|
37
72
|
outputTokens: 0,
|
|
@@ -42,6 +77,7 @@ export const createCodexTokenUsage = requestedModelId => ({
|
|
|
42
77
|
stepCount: 0,
|
|
43
78
|
requestedModelId: requestedModelId || null,
|
|
44
79
|
respondedModelId: requestedModelId || null,
|
|
80
|
+
tokenFieldAvailability: createCodexTokenFieldAvailability(),
|
|
45
81
|
});
|
|
46
82
|
|
|
47
83
|
const createEmptyCodexItemUsage = () => ({
|
|
@@ -162,6 +198,7 @@ export const parseCodexExecJsonOutput = (output, state = {}, requestedModelId =
|
|
|
162
198
|
observedModelDiagnosticPaths: state.observedModelDiagnosticPaths || [],
|
|
163
199
|
};
|
|
164
200
|
|
|
201
|
+
nextState.tokenUsage.tokenFieldAvailability ||= createCodexTokenFieldAvailability();
|
|
165
202
|
const observedModelPaths = new Set(nextState.observedModelDiagnosticPaths);
|
|
166
203
|
|
|
167
204
|
for (const rawLine of output.split('\n')) {
|
|
@@ -205,17 +242,28 @@ export const parseCodexExecJsonOutput = (output, state = {}, requestedModelId =
|
|
|
205
242
|
}
|
|
206
243
|
|
|
207
244
|
if (eventType === 'turn.completed' && data.usage && typeof data.usage === 'object') {
|
|
208
|
-
const inputTokens =
|
|
209
|
-
const cachedInputTokens =
|
|
210
|
-
const
|
|
245
|
+
const inputTokens = getFirstObservedNumber(data.usage, ['input_tokens']);
|
|
246
|
+
const cachedInputTokens = getFirstObservedNumber(data.usage, CODEX_CACHE_READ_USAGE_PATHS);
|
|
247
|
+
const cacheWriteTokens = getFirstObservedNumber(data.usage, CODEX_CACHE_WRITE_USAGE_PATHS);
|
|
248
|
+
const outputTokens = getFirstObservedNumber(data.usage, ['output_tokens']);
|
|
249
|
+
const reasoningTokens = getFirstObservedNumber(data.usage, CODEX_REASONING_USAGE_PATHS);
|
|
250
|
+
|
|
251
|
+
if (hasOwnPath(data.usage, 'input_tokens')) nextState.tokenUsage.tokenFieldAvailability.inputTokens = true;
|
|
252
|
+
if (hasAnyObservedPath(data.usage, CODEX_CACHE_READ_USAGE_PATHS)) nextState.tokenUsage.tokenFieldAvailability.cacheReadTokens = true;
|
|
253
|
+
if (hasAnyObservedPath(data.usage, CODEX_CACHE_WRITE_USAGE_PATHS)) nextState.tokenUsage.tokenFieldAvailability.cacheWriteTokens = true;
|
|
254
|
+
if (hasOwnPath(data.usage, 'output_tokens')) nextState.tokenUsage.tokenFieldAvailability.outputTokens = true;
|
|
255
|
+
if (hasAnyObservedPath(data.usage, CODEX_REASONING_USAGE_PATHS)) nextState.tokenUsage.tokenFieldAvailability.reasoningTokens = true;
|
|
256
|
+
|
|
211
257
|
const nonCachedInputTokens = Math.max(0, inputTokens - cachedInputTokens);
|
|
212
258
|
nextState.tokenUsage.inputTokens += nonCachedInputTokens;
|
|
213
259
|
nextState.tokenUsage.cacheReadTokens += cachedInputTokens;
|
|
260
|
+
nextState.tokenUsage.cacheWriteTokens += cacheWriteTokens;
|
|
214
261
|
nextState.tokenUsage.outputTokens += outputTokens;
|
|
262
|
+
nextState.tokenUsage.reasoningTokens += reasoningTokens;
|
|
215
263
|
nextState.tokenUsage.totalTokens = nextState.tokenUsage.inputTokens + nextState.tokenUsage.cacheReadTokens + nextState.tokenUsage.outputTokens + nextState.tokenUsage.cacheWriteTokens;
|
|
216
264
|
nextState.tokenUsage.stepCount += 1;
|
|
217
265
|
|
|
218
|
-
const usageFieldSet = CODEX_USAGE_FIELD_NAMES.filter(fieldName =>
|
|
266
|
+
const usageFieldSet = CODEX_USAGE_FIELD_NAMES.filter(fieldName => hasOwnPath(data.usage, fieldName));
|
|
219
267
|
if (usageFieldSet.length > 0) nextState.observedUsageFieldSets.push(usageFieldSet);
|
|
220
268
|
}
|
|
221
269
|
|
|
@@ -537,6 +585,10 @@ export const executeCodexCommand = async params => {
|
|
|
537
585
|
} else {
|
|
538
586
|
codexArgs += ` --model ${shellQuote(mappedModel)}`;
|
|
539
587
|
}
|
|
588
|
+
const codexPlaywrightMcpDisableConfigArgs = argv.playwrightMcp === false ? await getCodexPlaywrightMcpDisableConfigArgs(log) : [];
|
|
589
|
+
for (const arg of codexPlaywrightMcpDisableConfigArgs) {
|
|
590
|
+
codexArgs += ` ${shellQuote(arg)}`;
|
|
591
|
+
}
|
|
540
592
|
codexArgs += ` --json --skip-git-repo-check -o ${shellQuote(lastMessageFile)} -c ${shellQuote(`model_reasoning_effort=${reasoningEffort}`)} -c ${shellQuote('model_reasoning_summary=auto')} --dangerously-bypass-approvals-and-sandbox`;
|
|
541
593
|
|
|
542
594
|
const fullCommand = `(cd ${shellQuote(tempDir)} && cat ${shellQuote(promptFile)} | ${codexPath} ${codexArgs})`;
|
|
@@ -260,6 +260,7 @@ GitHub CLI command patterns.
|
|
|
260
260
|
Playwright MCP usage (browser automation via MCP tools).
|
|
261
261
|
- When you develop frontend web applications or debug UI issues, use Playwright MCP tools to test the UI in a real browser.
|
|
262
262
|
- When simple fetch-based browsing is insufficient for dynamic pages, use Playwright MCP browser automation as a fallback.
|
|
263
|
+
- When WebSearch tool fails or returns insufficient results, use Playwright MCP browser automation as a fallback for internet search.
|
|
263
264
|
- When reproducing or verifying UI bugs, take before/after screenshots and close the browser when finished.`
|
|
264
265
|
: ''
|
|
265
266
|
}${
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import Decimal from 'decimal.js-light';
|
|
4
|
+
|
|
5
|
+
const formatTokenCount = value => (Number.isFinite(value) ? value : 0).toLocaleString();
|
|
6
|
+
|
|
7
|
+
const isObservedTokenField = (usage, fieldName) => {
|
|
8
|
+
const value = usage?.[fieldName];
|
|
9
|
+
if (Number.isFinite(value) && value > 0) return true;
|
|
10
|
+
if (usage?.tokenFieldAvailability?.[fieldName] === true) return true;
|
|
11
|
+
if (Array.isArray(usage?.availableTokenFields) && usage.availableTokenFields.includes(fieldName)) return true;
|
|
12
|
+
return false;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const buildTokenUsageString = tokenUsage => {
|
|
16
|
+
const parts = [`${formatTokenCount(tokenUsage.inputTokens)} input`, `${formatTokenCount(tokenUsage.outputTokens)} output`];
|
|
17
|
+
if (isObservedTokenField(tokenUsage, 'reasoningTokens')) parts.push(`${formatTokenCount(tokenUsage.reasoningTokens)} reasoning`);
|
|
18
|
+
if (isObservedTokenField(tokenUsage, 'cacheReadTokens')) parts.push(`${formatTokenCount(tokenUsage.cacheReadTokens)} cache read`);
|
|
19
|
+
if (isObservedTokenField(tokenUsage, 'cacheWriteTokens')) parts.push(`${formatTokenCount(tokenUsage.cacheWriteTokens)} cache write`);
|
|
20
|
+
return `\n- Token usage: ${parts.join(', ')}`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Build cost estimation string for log comments (Issue #1250, Issue #1557, Issue #1600: Decimal precision) */
|
|
24
|
+
export const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) => {
|
|
25
|
+
const hasPublic = totalCostUSD !== null && totalCostUSD !== undefined;
|
|
26
|
+
const hasAnthropic = anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined;
|
|
27
|
+
const hasPricing = pricingInfo && (pricingInfo.modelName || pricingInfo.tokenUsage || pricingInfo.isFreeModel || pricingInfo.isOpencodeFreeModel);
|
|
28
|
+
const hasOpencodeCost = pricingInfo?.opencodeCost !== null && pricingInfo?.opencodeCost !== undefined;
|
|
29
|
+
if (!hasPublic && !hasAnthropic && !hasPricing && !hasOpencodeCost) return '';
|
|
30
|
+
const publicDec = hasPublic ? new Decimal(totalCostUSD) : null;
|
|
31
|
+
const anthropicDec = hasAnthropic ? new Decimal(anthropicTotalCostUSD) : null;
|
|
32
|
+
if (publicDec && anthropicDec && publicDec.toFixed(6) === anthropicDec.toFixed(6)) return `\n\n### š° Cost: **$${anthropicDec.toFixed(6)}**`;
|
|
33
|
+
let costInfo = '\n\n### š° **Cost estimation:**';
|
|
34
|
+
if (pricingInfo?.modelName) {
|
|
35
|
+
costInfo += `\n- Model: ${pricingInfo.modelName}`;
|
|
36
|
+
if (pricingInfo.provider) costInfo += `\n- Provider: ${pricingInfo.provider}`;
|
|
37
|
+
}
|
|
38
|
+
if (hasPublic) {
|
|
39
|
+
if (pricingInfo?.isFreeModel && publicDec.eq(0) && !pricingInfo?.baseModelName) {
|
|
40
|
+
costInfo += '\n- Public pricing estimate: $0.00 (Free model)';
|
|
41
|
+
} else {
|
|
42
|
+
let pricingRef = '';
|
|
43
|
+
if (pricingInfo?.baseModelName && pricingInfo?.originalProvider) {
|
|
44
|
+
pricingRef = ` (based on ${pricingInfo.originalProvider} ${pricingInfo.baseModelName} prices)`;
|
|
45
|
+
} else if (pricingInfo?.originalProvider) {
|
|
46
|
+
pricingRef = ` (based on ${pricingInfo.originalProvider} prices)`;
|
|
47
|
+
}
|
|
48
|
+
costInfo += `\n- Public pricing estimate: $${publicDec.toFixed(6)}${pricingRef}`;
|
|
49
|
+
}
|
|
50
|
+
} else if (hasPricing) {
|
|
51
|
+
costInfo += '\n- Public pricing estimate: unknown';
|
|
52
|
+
}
|
|
53
|
+
if (hasOpencodeCost) {
|
|
54
|
+
if (pricingInfo.isOpencodeFreeModel) {
|
|
55
|
+
costInfo += '\n- Calculated by OpenCode Zen: $0.00 (Free model)';
|
|
56
|
+
} else {
|
|
57
|
+
costInfo += `\n- Calculated by OpenCode Zen: $${new Decimal(pricingInfo.opencodeCost).toFixed(6)}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (pricingInfo?.tokenUsage) costInfo += buildTokenUsageString(pricingInfo.tokenUsage);
|
|
61
|
+
if (hasAnthropic) {
|
|
62
|
+
costInfo += `\n- Calculated by Anthropic: $${anthropicDec.toFixed(6)}`;
|
|
63
|
+
if (hasPublic) {
|
|
64
|
+
const diff = anthropicDec.minus(publicDec);
|
|
65
|
+
const pct = publicDec.gt(0) ? diff.div(publicDec).mul(100) : new Decimal(0);
|
|
66
|
+
costInfo += `\n- Difference: $${diff.toFixed(6)} (${pct.gt(0) ? '+' : ''}${pct.toFixed(2)}%)`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return costInfo;
|
|
70
|
+
};
|
package/src/github.lib.mjs
CHANGED
|
@@ -14,60 +14,8 @@ import { formatResetTimeWithRelative } from './usage-limit.lib.mjs'; // See: htt
|
|
|
14
14
|
import { getToolDisplayName, getModelInfoForComment } from './models/index.mjs';
|
|
15
15
|
export { getToolDisplayName }; // Re-export for use by other modules
|
|
16
16
|
import { buildBudgetStatsString } from './claude.budget-stats.lib.mjs';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) => {
|
|
20
|
-
const hasPublic = totalCostUSD !== null && totalCostUSD !== undefined;
|
|
21
|
-
const hasAnthropic = anthropicTotalCostUSD !== null && anthropicTotalCostUSD !== undefined;
|
|
22
|
-
const hasPricing = pricingInfo && (pricingInfo.modelName || pricingInfo.tokenUsage || pricingInfo.isFreeModel || pricingInfo.isOpencodeFreeModel);
|
|
23
|
-
const hasOpencodeCost = pricingInfo?.opencodeCost !== null && pricingInfo?.opencodeCost !== undefined;
|
|
24
|
-
if (!hasPublic && !hasAnthropic && !hasPricing && !hasOpencodeCost) return '';
|
|
25
|
-
// Issue #1557: Simplified display when public and Anthropic costs match
|
|
26
|
-
if (hasPublic && hasAnthropic && totalCostUSD.toFixed(6) === anthropicTotalCostUSD.toFixed(6)) return `\n\n### š° Cost: **$${anthropicTotalCostUSD.toFixed(6)}**`;
|
|
27
|
-
let costInfo = '\n\n### š° **Cost estimation:**';
|
|
28
|
-
if (pricingInfo?.modelName) {
|
|
29
|
-
costInfo += `\n- Model: ${pricingInfo.modelName}`;
|
|
30
|
-
if (pricingInfo.provider) costInfo += `\n- Provider: ${pricingInfo.provider}`;
|
|
31
|
-
}
|
|
32
|
-
if (hasPublic) {
|
|
33
|
-
if (pricingInfo?.isFreeModel && totalCostUSD === 0 && !pricingInfo?.baseModelName) {
|
|
34
|
-
costInfo += '\n- Public pricing estimate: $0.00 (Free model)';
|
|
35
|
-
} else {
|
|
36
|
-
let pricingRef = '';
|
|
37
|
-
if (pricingInfo?.baseModelName && pricingInfo?.originalProvider) {
|
|
38
|
-
pricingRef = ` (based on ${pricingInfo.originalProvider} ${pricingInfo.baseModelName} prices)`;
|
|
39
|
-
} else if (pricingInfo?.originalProvider) {
|
|
40
|
-
pricingRef = ` (based on ${pricingInfo.originalProvider} prices)`;
|
|
41
|
-
}
|
|
42
|
-
costInfo += `\n- Public pricing estimate: $${totalCostUSD.toFixed(6)}${pricingRef}`;
|
|
43
|
-
}
|
|
44
|
-
} else if (hasPricing) {
|
|
45
|
-
costInfo += '\n- Public pricing estimate: unknown';
|
|
46
|
-
}
|
|
47
|
-
if (hasOpencodeCost) {
|
|
48
|
-
if (pricingInfo.isOpencodeFreeModel) {
|
|
49
|
-
costInfo += '\n- Calculated by OpenCode Zen: $0.00 (Free model)';
|
|
50
|
-
} else {
|
|
51
|
-
costInfo += `\n- Calculated by OpenCode Zen: $${pricingInfo.opencodeCost.toFixed(6)}`;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
if (pricingInfo?.tokenUsage) {
|
|
55
|
-
const u = pricingInfo.tokenUsage;
|
|
56
|
-
let tokenInfo = `\n- Token usage: ${u.inputTokens?.toLocaleString() || 0} input, ${u.outputTokens?.toLocaleString() || 0} output`;
|
|
57
|
-
if (u.reasoningTokens > 0) tokenInfo += `, ${u.reasoningTokens.toLocaleString()} reasoning`;
|
|
58
|
-
if (u.cacheReadTokens > 0 || u.cacheWriteTokens > 0) tokenInfo += `, ${u.cacheReadTokens?.toLocaleString() || 0} cache read, ${u.cacheWriteTokens?.toLocaleString() || 0} cache write`;
|
|
59
|
-
costInfo += tokenInfo;
|
|
60
|
-
}
|
|
61
|
-
if (hasAnthropic) {
|
|
62
|
-
costInfo += `\n- Calculated by Anthropic: $${anthropicTotalCostUSD.toFixed(6)}`;
|
|
63
|
-
if (hasPublic) {
|
|
64
|
-
const diff = anthropicTotalCostUSD - totalCostUSD;
|
|
65
|
-
const pct = totalCostUSD > 0 ? (diff / totalCostUSD) * 100 : 0;
|
|
66
|
-
costInfo += `\n- Difference: $${diff.toFixed(6)} (${pct > 0 ? '+' : ''}${pct.toFixed(2)}%)`;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return costInfo;
|
|
70
|
-
};
|
|
17
|
+
import { buildCostInfoString } from './github-cost-info.lib.mjs';
|
|
18
|
+
export { buildCostInfoString };
|
|
71
19
|
export const maskGitHubToken = maskToken; // Alias for backward compatibility
|
|
72
20
|
export const escapeCodeBlocksInLog = logContent => logContent.replace(/```/g, '\\`\\`\\`'); // Escape ``` in logs
|
|
73
21
|
export const checkFileInBranch = async (owner, repo, fileName, branchName) => {
|
package/src/opencode.lib.mjs
CHANGED
|
@@ -19,6 +19,11 @@ import { timeouts } from './config.lib.mjs';
|
|
|
19
19
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
20
20
|
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
21
21
|
import { opencodeModels, defaultModels } from './models/index.mjs';
|
|
22
|
+
import { checkPlaywrightMcpPackageAvailability, getOpenCodePlaywrightMcpDisableEnv } from './playwright-mcp.lib.mjs';
|
|
23
|
+
import { createAgentTokenUsage, accumulateAgentStepFinishUsage, parseAgentTokenUsage as parseOpenCodeTokenUsage } from './agent-token-usage.lib.mjs';
|
|
24
|
+
import { calculateAgentPricing } from './agent.lib.mjs';
|
|
25
|
+
|
|
26
|
+
export { parseOpenCodeTokenUsage };
|
|
22
27
|
|
|
23
28
|
// Model mapping to translate aliases to full model IDs for OpenCode
|
|
24
29
|
// Issue #1473: Uses centralized opencodeModels from models/index.mjs (single source of truth)
|
|
@@ -97,6 +102,9 @@ export const handleOpenCodeRuntimeSwitch = async () => {
|
|
|
97
102
|
await log('ā¹ļø OpenCode runtime handling not required for this operation');
|
|
98
103
|
};
|
|
99
104
|
|
|
105
|
+
/** Check if Playwright MCP is available for OpenCode @returns {Promise<boolean>} */
|
|
106
|
+
export const checkPlaywrightMcpAvailability = checkPlaywrightMcpPackageAvailability;
|
|
107
|
+
|
|
100
108
|
// Main function to execute OpenCode with prompts and settings
|
|
101
109
|
export const executeOpenCode = async params => {
|
|
102
110
|
const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, formatAligned, getResourceSnapshot, opencodePath = 'opencode', $ } = params;
|
|
@@ -237,11 +245,20 @@ export const executeOpenCodeCommand = async params => {
|
|
|
237
245
|
await log(` Memory: ${resourcesBefore.memory.split('\n')[1]}`, { verbose: true });
|
|
238
246
|
await log(` Load: ${resourcesBefore.load}`, { verbose: true });
|
|
239
247
|
|
|
248
|
+
const opencodeEnv = { ...process.env };
|
|
249
|
+
|
|
250
|
+
// Apply Playwright MCP session state before launching OpenCode.
|
|
251
|
+
if (argv.playwrightMcp === false) {
|
|
252
|
+
Object.assign(opencodeEnv, await getOpenCodePlaywrightMcpDisableEnv({ env: opencodeEnv, cwd: tempDir, log }));
|
|
253
|
+
await log('š Playwright MCP physically disabled for this OpenCode session via --no-playwright-mcp', { verbose: true });
|
|
254
|
+
}
|
|
255
|
+
|
|
240
256
|
// Build OpenCode command
|
|
241
257
|
let execCommand;
|
|
242
258
|
|
|
243
259
|
// Map model alias to full ID
|
|
244
260
|
const mappedModel = mapModelToId(argv.model);
|
|
261
|
+
const streamingTokenUsage = createAgentTokenUsage();
|
|
245
262
|
|
|
246
263
|
// Build opencode command arguments
|
|
247
264
|
let opencodeArgs = `run --format json --model ${mappedModel}`;
|
|
@@ -268,17 +285,28 @@ export const executeOpenCodeCommand = async params => {
|
|
|
268
285
|
await log(`${fullCommand}`);
|
|
269
286
|
await log('');
|
|
270
287
|
|
|
288
|
+
const buildPricingInfo = async () => {
|
|
289
|
+
const tokenUsage = streamingTokenUsage;
|
|
290
|
+
if (tokenUsage.stepCount === 0) {
|
|
291
|
+
return { tokenUsage, pricingInfo: null, publicPricingEstimate: null };
|
|
292
|
+
}
|
|
293
|
+
const pricingInfo = await calculateAgentPricing(mappedModel, tokenUsage);
|
|
294
|
+
return { tokenUsage, pricingInfo, publicPricingEstimate: pricingInfo?.totalCostUSD ?? null };
|
|
295
|
+
};
|
|
296
|
+
|
|
271
297
|
try {
|
|
272
298
|
// Pipe the prompt file to opencode via stdin
|
|
273
299
|
if (argv.resume) {
|
|
274
300
|
execCommand = $({
|
|
275
301
|
cwd: tempDir,
|
|
276
302
|
mirror: false,
|
|
303
|
+
env: opencodeEnv,
|
|
277
304
|
})`cat ${promptFile} | ${opencodePath} run --format json --resume ${argv.resume} --model ${mappedModel}`;
|
|
278
305
|
} else {
|
|
279
306
|
execCommand = $({
|
|
280
307
|
cwd: tempDir,
|
|
281
308
|
mirror: false,
|
|
309
|
+
env: opencodeEnv,
|
|
282
310
|
})`cat ${promptFile} | ${opencodePath} run --format json --model ${mappedModel}`;
|
|
283
311
|
}
|
|
284
312
|
|
|
@@ -313,6 +341,7 @@ export const executeOpenCodeCommand = async params => {
|
|
|
313
341
|
for (const line of lines) {
|
|
314
342
|
if (!line.trim()) continue;
|
|
315
343
|
const data = sanitizeObjectStrings(JSON.parse(line));
|
|
344
|
+
accumulateAgentStepFinishUsage(streamingTokenUsage, data);
|
|
316
345
|
// Track text content for result summary
|
|
317
346
|
// OpenCode outputs text via 'text', 'assistant', 'message', or 'result' type events
|
|
318
347
|
if (data.type === 'text' && data.text) {
|
|
@@ -355,6 +384,7 @@ export const executeOpenCodeCommand = async params => {
|
|
|
355
384
|
for (const line of lines) {
|
|
356
385
|
if (!line.trim()) continue;
|
|
357
386
|
const data = sanitizeObjectStrings(JSON.parse(line));
|
|
387
|
+
accumulateAgentStepFinishUsage(streamingTokenUsage, data);
|
|
358
388
|
if (data.type === 'text' && data.text) {
|
|
359
389
|
lastTextContent = data.text;
|
|
360
390
|
} else if (data.type === 'assistant' && data.message?.content) {
|
|
@@ -427,12 +457,14 @@ export const executeOpenCodeCommand = async params => {
|
|
|
427
457
|
await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
|
|
428
458
|
await log(` Load: ${resourcesAfter.load}`, { verbose: true });
|
|
429
459
|
|
|
460
|
+
const pricingResult = await buildPricingInfo();
|
|
430
461
|
return {
|
|
431
462
|
success: false,
|
|
432
463
|
sessionId,
|
|
433
464
|
limitReached: false,
|
|
434
465
|
limitResetTime: null,
|
|
435
466
|
permissionPromptDetected: true,
|
|
467
|
+
...pricingResult,
|
|
436
468
|
resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
|
|
437
469
|
};
|
|
438
470
|
}
|
|
@@ -466,17 +498,41 @@ export const executeOpenCodeCommand = async params => {
|
|
|
466
498
|
await log(` Memory: ${resourcesAfter.memory.split('\n')[1]}`, { verbose: true });
|
|
467
499
|
await log(` Load: ${resourcesAfter.load}`, { verbose: true });
|
|
468
500
|
|
|
501
|
+
const pricingResult = await buildPricingInfo();
|
|
469
502
|
return {
|
|
470
503
|
success: false,
|
|
471
504
|
sessionId,
|
|
472
505
|
limitReached,
|
|
473
506
|
limitResetTime,
|
|
507
|
+
...pricingResult,
|
|
474
508
|
resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
|
|
475
509
|
};
|
|
476
510
|
}
|
|
477
511
|
|
|
478
512
|
await log('\n\nā
OpenCode command completed');
|
|
479
513
|
|
|
514
|
+
const pricingResult = await buildPricingInfo();
|
|
515
|
+
if (pricingResult.tokenUsage.stepCount > 0) {
|
|
516
|
+
await log('\nš° Token Usage Summary:');
|
|
517
|
+
await log(` š ${pricingResult.pricingInfo?.modelName || mappedModel} (${pricingResult.tokenUsage.stepCount} steps):`);
|
|
518
|
+
await log(` Input tokens: ${pricingResult.tokenUsage.inputTokens.toLocaleString()}`);
|
|
519
|
+
await log(` Output tokens: ${pricingResult.tokenUsage.outputTokens.toLocaleString()}`);
|
|
520
|
+
if (pricingResult.tokenUsage.reasoningTokens > 0 || pricingResult.tokenUsage.tokenFieldAvailability?.reasoningTokens) {
|
|
521
|
+
await log(` Reasoning tokens: ${pricingResult.tokenUsage.reasoningTokens.toLocaleString()}`);
|
|
522
|
+
}
|
|
523
|
+
if (pricingResult.tokenUsage.cacheReadTokens > 0 || pricingResult.tokenUsage.tokenFieldAvailability?.cacheReadTokens) {
|
|
524
|
+
await log(` Cache read: ${pricingResult.tokenUsage.cacheReadTokens.toLocaleString()}`);
|
|
525
|
+
}
|
|
526
|
+
if (pricingResult.tokenUsage.cacheWriteTokens > 0 || pricingResult.tokenUsage.tokenFieldAvailability?.cacheWriteTokens) {
|
|
527
|
+
await log(` Cache write: ${pricingResult.tokenUsage.cacheWriteTokens.toLocaleString()}`);
|
|
528
|
+
}
|
|
529
|
+
if (pricingResult.pricingInfo?.totalCostUSD !== null && pricingResult.pricingInfo?.totalCostUSD !== undefined) {
|
|
530
|
+
await log(` Public pricing estimate: $${pricingResult.pricingInfo.totalCostUSD.toFixed(6)}`);
|
|
531
|
+
} else {
|
|
532
|
+
await log(' Cost: Not available (could not fetch pricing)');
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
480
536
|
// Issue #1263: Log if result summary was captured
|
|
481
537
|
if (lastTextContent) {
|
|
482
538
|
await log('š Captured result summary from OpenCode output', { verbose: true });
|
|
@@ -487,6 +543,7 @@ export const executeOpenCodeCommand = async params => {
|
|
|
487
543
|
sessionId,
|
|
488
544
|
limitReached,
|
|
489
545
|
limitResetTime,
|
|
546
|
+
...pricingResult,
|
|
490
547
|
resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
|
|
491
548
|
};
|
|
492
549
|
} catch (error) {
|
|
@@ -510,6 +567,9 @@ export const executeOpenCodeCommand = async params => {
|
|
|
510
567
|
sessionId: null,
|
|
511
568
|
limitReached: false,
|
|
512
569
|
limitResetTime: null,
|
|
570
|
+
tokenUsage: streamingTokenUsage.stepCount > 0 ? streamingTokenUsage : null,
|
|
571
|
+
pricingInfo: null,
|
|
572
|
+
publicPricingEstimate: null,
|
|
513
573
|
resultSummary: null, // Issue #1263: No result summary available on error
|
|
514
574
|
};
|
|
515
575
|
}
|
|
@@ -607,7 +667,9 @@ export const checkForUncommittedChanges = async (tempDir, owner, repo, branchNam
|
|
|
607
667
|
export default {
|
|
608
668
|
validateOpenCodeConnection,
|
|
609
669
|
handleOpenCodeRuntimeSwitch,
|
|
670
|
+
checkPlaywrightMcpAvailability,
|
|
610
671
|
executeOpenCode,
|
|
611
672
|
executeOpenCodeCommand,
|
|
612
673
|
checkForUncommittedChanges,
|
|
674
|
+
parseOpenCodeTokenUsage,
|
|
613
675
|
};
|
|
@@ -226,6 +226,24 @@ GitHub CLI command patterns.
|
|
|
226
226
|
- When adding issue comment, use gh issue comment NUMBER --body "text" --repo OWNER/REPO.
|
|
227
227
|
- When viewing PR details, use gh pr view NUMBER --repo OWNER/REPO.
|
|
228
228
|
- When filtering with jq, use gh api repos/${owner}/${repo}/pulls/${prNumber}/comments --paginate --jq 'reverse | .[0:5]'.${
|
|
229
|
+
argv && argv.promptPlaywrightMcp
|
|
230
|
+
? `
|
|
231
|
+
|
|
232
|
+
Playwright MCP usage (browser automation via MCP tools).
|
|
233
|
+
- When you develop frontend web applications (HTML, CSS, JavaScript, React, Vue, Angular, etc.), use Playwright MCP tools to test the UI in a real browser.
|
|
234
|
+
- When WebFetch tool fails to retrieve expected content (e.g., returns empty content, JavaScript-rendered pages, or login-protected pages), use Playwright MCP tools (browser_navigate, browser_snapshot) as a fallback for web browsing.
|
|
235
|
+
- When WebSearch tool fails or returns insufficient results, use Playwright MCP tools (browser_navigate, browser_snapshot) as a fallback for internet search.
|
|
236
|
+
- When you need to interact with dynamic web pages that require JavaScript execution, use Playwright MCP tools.
|
|
237
|
+
- When you need to visually verify how a web page looks or take screenshots, use browser_take_screenshot from Playwright MCP.
|
|
238
|
+
- When you need to fill forms, click buttons, or perform user interactions on web pages, use Playwright MCP tools (browser_click, browser_type, browser_fill_form).
|
|
239
|
+
- When you need to test responsive design or different viewport sizes, use browser_resize from Playwright MCP.
|
|
240
|
+
- When you finish using the browser, close it with browser_close to free resources.
|
|
241
|
+
- When reproducing UI bugs, use browser_take_screenshot to capture the problem state before implementing any fix.
|
|
242
|
+
- When fixing UI bugs, take before/after screenshots to provide visual evidence of the fix for human verification.
|
|
243
|
+
- When creating UI tests, save baseline screenshots to the repository for visual regression testing.
|
|
244
|
+
- When verifying UI fixes, compare screenshots to ensure the fix does not introduce unintended visual changes.`
|
|
245
|
+
: ''
|
|
246
|
+
}${
|
|
229
247
|
modelSupportsVision
|
|
230
248
|
? `
|
|
231
249
|
|