dsh-inner-browser-use 0.41.0 → 0.42.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/README.md +1 -1
- package/README.zh.md +1 -1
- package/docs/ARCHITECTURE.md +16 -0
- package/docs/BENCH.md +15 -0
- package/package.json +1 -1
- package/read.js +63 -5
- package/sidecar.js +45 -0
- package/tools.js +27 -4
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ live picture of that browser, and your own cursor keeps working on the picture.
|
|
|
12
12
|
| --- | --- |
|
|
13
13
|
| `browser_open` | Starts the browser (or reuses the running one) and opens an address. `headless: false` shows a real window on the desktop instead. |
|
|
14
14
|
| `browser_view` | Returns the address, the title, every control as `1 … 120`, and the visible text. |
|
|
15
|
-
| `browser_read` | Reads the page the way a person does: one Markdown block of headings, paragraphs, lists and links, no controls. Far fewer tokens than a snapshot on long pages, and the links stay usable. Give it a `query` and only the sentences that answer it come back: BM25 over Readability-style blocks, then per-sentence scoring with heading, position and link-density priors, idf-weighted coverage, MMR de-duplication and a character budget — 92% fewer tokens on a long page in the bench. A question the page cannot answer returns the nearest passages and says so instead of pasting the page (79% fewer tokens in the bench). |
|
|
15
|
+
| `browser_read` | Reads the page the way a person does: one Markdown block of headings, paragraphs, lists and links, no controls. Far fewer tokens than a snapshot on long pages, and the links stay usable. Give it a `query` and only the sentences that answer it come back: BM25 over Readability-style blocks, then per-sentence scoring with heading, position and link-density priors, idf-weighted coverage, MMR de-duplication and a character budget — 92% fewer tokens on a long page in the bench. A question the page cannot answer returns the nearest passages and says so instead of pasting the page (79% fewer tokens in the bench). When the tab has no answer, the other tabs this chat holds are read in the same call and the answer comes back with the tab it came from — one read instead of switch-and-read-again (90% fewer characters in the bench); `across: false` stays on this tab. |
|
|
16
16
|
| `browser_find` | Finds words (or a regex) anywhere on the page: matching controls come back with a clickable number, matching text with its point, each saying whether it is on screen. |
|
|
17
17
|
| `browser_extract` | Returns the items a CSS selector matches, one compact line each with its link, instead of the whole page. |
|
|
18
18
|
| `browser_click` | Clicks a numbered control, or an `x`/`y` point. `double: true` double-clicks. A cookie banner or modal over the control is cleared first, then the click happens. |
|
package/README.zh.md
CHANGED
|
@@ -11,7 +11,7 @@ profile,不是产品自带的那个浏览页签:右栏放的是它的画面
|
|
|
11
11
|
| --- | --- |
|
|
12
12
|
| `browser_open` | 启动浏览器(已在跑就复用)并打开一个地址。传 `headless: false` 会改为在桌面上开真实窗口。 |
|
|
13
13
|
| `browser_view` | 返回地址、标题、编号成 `1 … 120` 的控件,以及可见文字。 |
|
|
14
|
-
| `browser_read` | 按人读文章的方式给 Markdown(标题、段落、列表、链接)。带 `query`
|
|
14
|
+
| `browser_read` | 按人读文章的方式给 Markdown(标题、段落、列表、链接)。带 `query` 只回能回答的那几句(段落排序后再按句子打分,标题、位置、链接密度做先验,重复句去掉,按字符预算装箱);问不到时只回最近的三段并明说没答上,不再把整页端回来;当前页答不上来还会顺手读这个会话自己开着的其它页签,答案在哪页就从哪页回并标出页签标题(bench 里少 90% 字符),`across: false` 只读当前页。页面和上次读的没变就只回一行,不重发正文(`force: true` 强制重发)。 |
|
|
15
15
|
| `browser_extract` | 按 CSS 选择器把列表项读成一行一条、带链接,比整页读省得多。 |
|
|
16
16
|
| `browser_view` 的 `map` | 大页面先看地标:各类控件各多少个、去重后的控件和编号、正文多少字,但不带回正文。 |
|
|
17
17
|
| `browser_click` | 点编号控件,或点 `x`/`y` 坐标。`double: true` 双击。控件被横幅或弹层挡着时,先关掉它再点。 |
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -192,6 +192,22 @@ characters) and no text, and the tool turns that into a line saying nothing dire
|
|
|
192
192
|
question plus those passages. The whole page is still one `browser_view` away, so the expensive
|
|
193
193
|
fallback — a page pasted into the context for a question it never addressed — is gone.
|
|
194
194
|
|
|
195
|
+
## Answering across tabs
|
|
196
|
+
|
|
197
|
+
A question can be answered by a tab that is not the one on screen. Moving the mirror there and
|
|
198
|
+
reading again costs two calls, a screencast teardown, and the person's view moved for nothing, so
|
|
199
|
+
a miss tries the tabs instead: `elsewhere(session)` lists the tabs the calling chat holds (its own
|
|
200
|
+
mirror excluded), opens one read-only CDP handle per tab — `Cdp` on that tab's own
|
|
201
|
+
`webSocketUrl`, never a full `SidecarPage`, never `cast()`, never a screencast stop — and
|
|
202
|
+
`readTabs` pools blocks from all of them under one `maxChars` ceiling and runs the same
|
|
203
|
+
`evidence()` pass once, so a merged answer costs the same characters as a single-tab one. Every
|
|
204
|
+
row keeps the title of the tab its text came from, and every handle is closed in a `finally`.
|
|
205
|
+
|
|
206
|
+
Only the calling session's claimed tabs are candidates: another chat's tab, and an unclaimed one,
|
|
207
|
+
are not read. At most three extra tabs are opened, and the path only runs when the current tab has
|
|
208
|
+
no answer, so a normal question pays nothing for it. `across: false` turns it off. `pruneStrays`
|
|
209
|
+
counts claimed tabs as owned, because a tab a chat holds is not a stray.
|
|
210
|
+
|
|
195
211
|
## Sign-ins and reclaim
|
|
196
212
|
|
|
197
213
|
Sign-in state lives in the profile, and session cookies — the kind a sign-in usually sets,
|
package/docs/BENCH.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## 答案在另一个页签(0.42.0)
|
|
2
|
+
|
|
3
|
+
两个页签:前台一页在讲别的事,后台另一页才有答案。左边是旧做法——切过去、再读一遍;
|
|
4
|
+
右边是一次跨页读。
|
|
5
|
+
|
|
6
|
+
| 项 | 切页签读两遍 | 一次跨页读 |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| 调用次数 | 2 | 1 |
|
|
9
|
+
| 回来的字符数 | 879 | 90 |
|
|
10
|
+
| 省下 | — | 90% |
|
|
11
|
+
| 出处 | 要自己记 | 每段标页签标题 |
|
|
12
|
+
| 读这一趟 | 7 ms | 6 ms |
|
|
13
|
+
|
|
14
|
+
跨页只在当前页答不上来时发生,正常命中路径的延迟不变(`read-answer` 仍是 1172 → 90 字)。
|
|
15
|
+
|
|
1
16
|
## 问了但页面没答(0.41.0)
|
|
2
17
|
|
|
3
18
|
同一张故事页,问一个页面里根本没有的词(`quantum chromodynamics`)。左边是旧的兜底:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-inner-browser-use",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "An agent-owned Chromium sidecar mirrored into the DeepSeek Harness right column: the agent drives it, the person watches and can take the mouse, and logins survive restarts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/read.js
CHANGED
|
@@ -1206,6 +1206,17 @@ function fold(word) {
|
|
|
1206
1206
|
/** How often a term may occur before it stops adding meaning. */
|
|
1207
1207
|
const SATURATION = 1.5
|
|
1208
1208
|
|
|
1209
|
+
/**
|
|
1210
|
+
* A passage short enough to show when nothing answers: one sentence or so,
|
|
1211
|
+
* whitespace collapsed, with an ellipsis saying it was cut.
|
|
1212
|
+
* @param {unknown} value - block text.
|
|
1213
|
+
* @returns {string} the passage.
|
|
1214
|
+
*/
|
|
1215
|
+
function brief(value) {
|
|
1216
|
+
const body = String(value ?? '').replace(/\s+/g, ' ').trim()
|
|
1217
|
+
return body.length > 260 ? `${body.slice(0, 260).replace(/\s+\S*$/, '')}…` : body
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1209
1220
|
/**
|
|
1210
1221
|
* Rank blocks against a query the way a search engine ranks a passage: BM25,
|
|
1211
1222
|
* with the heading counted as a place a term can match. Then read only the
|
|
@@ -1386,13 +1397,10 @@ export async function readRelevant(page, query, options = {}) {
|
|
|
1386
1397
|
/* A short, untrimmed paragraph is what a miss shows: enough to say what the
|
|
1387
1398
|
page is about, never enough to be the page. */
|
|
1388
1399
|
const near = (blocks) => blocks
|
|
1389
|
-
.map((block) =>
|
|
1400
|
+
.map((block) => brief(block?.t))
|
|
1390
1401
|
.filter((body) => body !== '')
|
|
1391
1402
|
.slice(0, 3)
|
|
1392
|
-
.map((body) => {
|
|
1393
|
-
const cut = body.length > 260 ? `${body.slice(0, 260).replace(/\s+\S*$/, '')}…` : body
|
|
1394
|
-
return { heading: '', text: cut }
|
|
1395
|
-
})
|
|
1403
|
+
.map((body) => ({ heading: '', text: body }))
|
|
1396
1404
|
try {
|
|
1397
1405
|
const raw = String((await evaluateIn(page, blocks(budget * 8))) ?? '{}')
|
|
1398
1406
|
const said = JSON.parse(raw)
|
|
@@ -1441,6 +1449,56 @@ export async function readRelevant(page, query, options = {}) {
|
|
|
1441
1449
|
}
|
|
1442
1450
|
}
|
|
1443
1451
|
|
|
1452
|
+
/**
|
|
1453
|
+
* Answer one question from several tabs at once: each tab is ranked on its own
|
|
1454
|
+
* words, then the sentences are pooled and packed under one ceiling, so an
|
|
1455
|
+
* answer that lives in a background tab costs one read instead of a tab switch
|
|
1456
|
+
* and a second read.
|
|
1457
|
+
* @param {Array<{title?: string, url?: string, cdp: any}>} pages - read-only handles.
|
|
1458
|
+
* @param {string} query - what the caller wants to know.
|
|
1459
|
+
* @param {{maxChars?: number, limit?: number}} [options] - ceiling across all tabs.
|
|
1460
|
+
* @returns {Promise<{rows: Array<{text: string, heading: string, at: number, tab: string}>, tabs: Array<{title: string, url: string, blocks: number, fault?: string}>, chars: number, matched: number, total: number, nearby: Array<{tab: string, text: string}>}>} pooled evidence.
|
|
1461
|
+
*/
|
|
1462
|
+
export async function readTabs(pages, query, options = {}) {
|
|
1463
|
+
const ceiling = Math.max(400, Math.min(24_000, Number(options.maxChars ?? 4000)))
|
|
1464
|
+
const list = Array.isArray(pages) ? pages.filter((page) => page !== null && page !== undefined && page.cdp !== undefined) : []
|
|
1465
|
+
const share = Math.max(400, Math.floor(ceiling / Math.max(1, list.length)))
|
|
1466
|
+
const tabs = []
|
|
1467
|
+
const pool = []
|
|
1468
|
+
const owner = []
|
|
1469
|
+
for (let at = 0; at < list.length; at += 1) {
|
|
1470
|
+
const page = list[at]
|
|
1471
|
+
const title = String(page.title ?? '') === '' ? String(page.url ?? '') : String(page.title)
|
|
1472
|
+
try {
|
|
1473
|
+
const raw = String((await evaluateIn(page, blocks(share * 8))) ?? '{}')
|
|
1474
|
+
const said = JSON.parse(raw)
|
|
1475
|
+
const found = Array.isArray(said?.blocks) ? said.blocks : []
|
|
1476
|
+
for (const block of found) { pool.push(block); owner.push(at) }
|
|
1477
|
+
tabs.push({ title, url: String(page.url ?? ''), blocks: found.length })
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
/* One tab that will not answer is not a failed question: the others are
|
|
1480
|
+
still worth reading, and the caller is told which one stayed quiet. */
|
|
1481
|
+
tabs.push({ title, url: String(page.url ?? ''), blocks: 0, fault: String(error?.message ?? error).split('\n')[0].slice(0, 160) })
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
const limit = Math.max(1, Math.min(60, Number(options.limit ?? 12)))
|
|
1485
|
+
const found = evidence(pool, query, { maxChars: ceiling, limit })
|
|
1486
|
+
const rows = found.rows.map((row) => {
|
|
1487
|
+
const tab = tabs[owner[row.at] ?? 0]?.title ?? ''
|
|
1488
|
+
return Object.assign({}, row, { tab })
|
|
1489
|
+
})
|
|
1490
|
+
/* Nothing answered anywhere: the nearest passage of every tab says what each
|
|
1491
|
+
one is about, which is what makes the next look cheap. */
|
|
1492
|
+
const nearby = rows.length > 0 ? [] : tabs
|
|
1493
|
+
.map((tab, at) => {
|
|
1494
|
+
const first = pool.find((block, slot) => owner[slot] === at && brief(block?.t) !== '')
|
|
1495
|
+
return first === undefined ? null : { tab: tab.title, text: brief(first.t) }
|
|
1496
|
+
})
|
|
1497
|
+
.filter((row) => row !== null)
|
|
1498
|
+
const chars = rows.reduce((sum, row) => sum + row.text.length, 0)
|
|
1499
|
+
return { rows, tabs, chars, matched: rows.length, total: pool.length, nearby }
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1444
1502
|
/**
|
|
1445
1503
|
* Read the page as Markdown, through the same deadline as a snapshot.
|
|
1446
1504
|
* @param {any} page - sidecar page.
|
package/sidecar.js
CHANGED
|
@@ -2337,6 +2337,11 @@ export class Sidecar {
|
|
|
2337
2337
|
const id = String(page?.targetId ?? '')
|
|
2338
2338
|
if (id !== '') owned.add(id)
|
|
2339
2339
|
}
|
|
2340
|
+
/* A tab a chat claimed is a tab that chat holds, even when the mirror is on
|
|
2341
|
+
another one: pruning it would take away the tab a cross-tab read needs. */
|
|
2342
|
+
for (const set of this.claims.values()) {
|
|
2343
|
+
for (const id of set) owned.add(String(id))
|
|
2344
|
+
}
|
|
2340
2345
|
let remembered = []
|
|
2341
2346
|
try {
|
|
2342
2347
|
remembered = Object.values(JSON.parse(readFileSync(tabsFile, 'utf8')) ?? {}).map(String)
|
|
@@ -2873,6 +2878,46 @@ export class Sidecar {
|
|
|
2873
2878
|
return shown
|
|
2874
2879
|
}
|
|
2875
2880
|
|
|
2881
|
+
/**
|
|
2882
|
+
* Read-only links to the other tabs this chat holds, so one question can be
|
|
2883
|
+
* answered by the tab that has the answer. A handle can read (it has a
|
|
2884
|
+
* connection) and must be closed; it never starts a screencast, moves the
|
|
2885
|
+
* mirror or replaces the page this chat is driving.
|
|
2886
|
+
* @param {string} session - chat asking.
|
|
2887
|
+
* @param {number} [limit] - how many other tabs at most, 1 to 3.
|
|
2888
|
+
* @returns {Promise<Array<{title: string, url: string, cdp: any, close: () => void}>>} handles.
|
|
2889
|
+
*/
|
|
2890
|
+
async elsewhere(session, limit = 3) {
|
|
2891
|
+
const most = Math.max(1, Math.min(3, Number(limit) || 3))
|
|
2892
|
+
const shown = await this.windowsFor(session)
|
|
2893
|
+
const rows = await this.tabs(session)
|
|
2894
|
+
/* The chat-level tab list drops the connection address, so the browser's own
|
|
2895
|
+
list is what a read-only handle is opened from. */
|
|
2896
|
+
const source = this.pageFor(String(session ?? '')) ?? this.page
|
|
2897
|
+
const raw = typeof source?.tabs === 'function' ? await source.tabs() : []
|
|
2898
|
+
const byId = new Map(raw.map((tab) => [String(tab.targetId), tab]))
|
|
2899
|
+
const out = []
|
|
2900
|
+
for (const row of shown) {
|
|
2901
|
+
if (out.length >= most) break
|
|
2902
|
+
if (row.current === true) continue
|
|
2903
|
+
const tab = byId.get(String(rows[row.index]?.targetId ?? ''))
|
|
2904
|
+
if (tab === undefined || String(tab.webSocketUrl ?? '') === '') continue
|
|
2905
|
+
try {
|
|
2906
|
+
const cdp = new Cdp(tab.webSocketUrl)
|
|
2907
|
+
await cdp.ready
|
|
2908
|
+
out.push({
|
|
2909
|
+
title: String(row.title === '' ? tab.title : row.title),
|
|
2910
|
+
url: String(row.url === '' ? tab.url : row.url),
|
|
2911
|
+
cdp,
|
|
2912
|
+
close: () => cdp.close(),
|
|
2913
|
+
})
|
|
2914
|
+
} catch {
|
|
2915
|
+
/* A tab that will not open is one tab less; the read still happens. */
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
return out
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2876
2921
|
/**
|
|
2877
2922
|
* Move the mirror to another tab, leaving the old one open where it was.
|
|
2878
2923
|
* @param {number} index - a row from `tabs()`.
|
package/tools.js
CHANGED
|
@@ -17,7 +17,7 @@ import { mkdir, readdir, stat, unlink, writeFile } from 'node:fs/promises'
|
|
|
17
17
|
import { VIEW, DESKTOP_UA } from './sidecar.js?v=1'
|
|
18
18
|
import { planOf, gather, sizeText, coreRows } from './media.js?m=2'
|
|
19
19
|
import { dropLogin, listLogins, readLogin, saveLogin, vaultState } from './vault.js'
|
|
20
|
-
import { readPage, renderPage, readArticle, readRelevant, findOnPage, locate, locateFirst, FRAME_BASE, covering, contextFor, identityOf, sameThing, differs, labelSpot, selectSpot, bounds, pictures, clickInDom, writeInDom, REF_GONE } from './read.js?v=1'
|
|
20
|
+
import { readPage, renderPage, readArticle, readRelevant, readTabs, findOnPage, locate, locateFirst, FRAME_BASE, covering, contextFor, identityOf, sameThing, differs, labelSpot, selectSpot, bounds, pictures, clickInDom, writeInDom, REF_GONE } from './read.js?v=1'
|
|
21
21
|
import { brief as askBrief, recordRound, lastRoundNote, lastRound, markRound, MARK_STATUS } from './review.js?v=1'
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -2233,6 +2233,7 @@ export function sidecarTools(sidecar, refs, host = {}) { // ui-copy-ignore
|
|
|
2233
2233
|
max_chars: { type: 'integer', description: 'Character ceiling. Default 6000; the result says when it was cut.' },
|
|
2234
2234
|
query: { type: 'string', description: 'What you want to know. With a query, only the passages that answer it come back, ranked — the cheapest way to read a long page. Without one, the whole page comes back.' },
|
|
2235
2235
|
force: { type: 'boolean', description: 'Read again even if the page has not changed since this chat last read it.' },
|
|
2236
|
+
across: { type: 'boolean', description: 'When this tab has no answer, also read the other tabs this chat holds and answer from whichever has it. Default true; pass false to stay on this one.' },
|
|
2236
2237
|
},
|
|
2237
2238
|
additionalProperties: false,
|
|
2238
2239
|
},
|
|
@@ -2260,6 +2261,28 @@ export function sidecarTools(sidecar, refs, host = {}) { // ui-copy-ignore
|
|
|
2260
2261
|
const found = await readRelevant(page, asked, { maxChars: ceiling })
|
|
2261
2262
|
if (found.fault !== undefined) return `the page did not answer when read — ${String(found.fault).split('\n')[0].slice(0, 200)}`
|
|
2262
2263
|
if (found.text === '') {
|
|
2264
|
+
/* The answer may be in the tab behind this one. Reading every tab
|
|
2265
|
+
this chat holds is one call, where switching and reading again is
|
|
2266
|
+
two, so the miss tries the others before it gives up. */
|
|
2267
|
+
let note = ''
|
|
2268
|
+
if (args?.across !== false) {
|
|
2269
|
+
const others = await sidecar.elsewhere(currentSession()).catch(() => [])
|
|
2270
|
+
if (others.length > 0) {
|
|
2271
|
+
try {
|
|
2272
|
+
const pooled = await readTabs(others, asked, { maxChars: ceiling })
|
|
2273
|
+
if (pooled.rows.length > 0) {
|
|
2274
|
+
const body = pooled.rows
|
|
2275
|
+
.map((row) => `## ${String(row.tab)}\n${String(row.text)}`)
|
|
2276
|
+
.join('\n\n')
|
|
2277
|
+
const from = pooled.tabs.length === 1 ? 'another tab' : `${String(pooled.tabs.length)} other tabs`
|
|
2278
|
+
return `${found.url}\n# ${found.title === '' ? '(untitled)' : found.title}\n\nthis tab does not answer ${JSON.stringify(asked)}; the answer is in ${from} this chat holds (${String(pooled.matched)} passages):\n\n${body}`
|
|
2279
|
+
}
|
|
2280
|
+
note = ` (${String(pooled.tabs.length)} other tab${pooled.tabs.length === 1 ? '' : 's'} read too, none answers it)`
|
|
2281
|
+
} finally {
|
|
2282
|
+
for (const one of others) one.close()
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2263
2286
|
/* A question the page does not answer costs the nearest passages and
|
|
2264
2287
|
a sentence saying so, not the whole page: whole pages are what
|
|
2265
2288
|
this tool exists to avoid, and the caller can ask for one. */
|
|
@@ -2268,13 +2291,13 @@ export function sidecarTools(sidecar, refs, host = {}) { // ui-copy-ignore
|
|
|
2268
2291
|
const body = closest
|
|
2269
2292
|
.map((row) => `${row.heading === '' ? '' : `## ${String(row.heading)}\n`}${String(row.text)}`)
|
|
2270
2293
|
.join('\n\n')
|
|
2271
|
-
return keep(`${found.url}\n# ${found.title === '' ? '(untitled)' : found.title}\n\nnothing directly answers ${JSON.stringify(asked)} (${String(found.total)} blocks read)
|
|
2294
|
+
return keep(`${found.url}\n# ${found.title === '' ? '(untitled)' : found.title}\n\nnothing directly answers ${JSON.stringify(asked)} (${String(found.total)} blocks read).${note} The closest passages:\n\n${body}\n\nask for the whole page with browser_view, or try other words.`)
|
|
2272
2295
|
}
|
|
2273
2296
|
const whole = await readArticle(page, { maxChars: ceiling })
|
|
2274
2297
|
if (whole.fault === undefined && whole.text !== '') {
|
|
2275
|
-
return keep(`${whole.url}\n# ${whole.title === '' ? '(untitled)' : whole.title}\n\nnothing here answers ${JSON.stringify(asked)} (${String(found.total)} blocks read)
|
|
2298
|
+
return keep(`${whole.url}\n# ${whole.title === '' ? '(untitled)' : whole.title}\n\nnothing here answers ${JSON.stringify(asked)} (${String(found.total)} blocks read).${note} so the whole page follows:\n\n${whole.text}${whole.truncated ? `\n\n(cut at ${String(whole.chars)} characters; raise max_chars for more)` : ''}`)
|
|
2276
2299
|
}
|
|
2277
|
-
return `${found.url}\n# ${found.title === '' ? '(untitled)' : found.title}\n\nnothing here answers ${JSON.stringify(asked)} (${String(found.total)} blocks read)
|
|
2300
|
+
return `${found.url}\n# ${found.title === '' ? '(untitled)' : found.title}\n\nnothing here answers ${JSON.stringify(asked)} (${String(found.total)} blocks read).${note} Call browser_view for the whole page, or try other words.`
|
|
2278
2301
|
}
|
|
2279
2302
|
return keep(`${found.url}\n# ${found.title === '' ? '(untitled)' : found.title}\n\n${found.text}\n\n(${String(found.matched)} of ${String(found.total)} blocks, ranked for ${JSON.stringify(asked)})`)
|
|
2280
2303
|
}
|