Merge pull request #47 from only-cli/feat/find-in-footer

feat: offer find in every actions footer
This commit is contained in:
Mark Cli
2026-09-02 10:47:26 -04:00
committed by GitHub
4 changed files with 32 additions and 3 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ $ oc open news.ycombinator.com
[1] Show HN: I built a tiny CSV toolkit
[2] 312 comments
...
actions: do <n> | read <n> | next | raw
actions: do <n> | find <query> | read <n> | next | raw
$ oc do 1
```
+4 -2
View File
@@ -234,7 +234,7 @@ export function find(query, { session = DEFAULT_SESSION, budget = 500 } = {}) {
return [
`1 match for "${query}"${separately}, region [${only.n}]`,
read(only.n, { session, budget: budget * FINISH }),
`actions: ${follow}read <n> | next | raw`,
`actions: ${follow}find <query> | read <n> | next | raw`,
].join('\n');
}
@@ -264,7 +264,9 @@ export function find(query, { session = DEFAULT_SESSION, budget = 500 } = {}) {
if (shown < hits.length) {
lines.push(`... ${hits.length - shown} more matches, narrow the query or raise --budget`);
}
lines.push(`actions: ${[hasLinks && 'do <n>', 'read <n>', 'next', 'raw'].filter(Boolean).join(' | ')}`);
// Offering find on its own output is what makes "narrow the query" above an
// action rather than advice.
lines.push(`actions: ${[hasLinks && 'do <n>', 'find <query>', 'read <n>', 'next', 'raw'].filter(Boolean).join(' | ')}`);
return lines.join('\n');
}
+7
View File
@@ -141,8 +141,15 @@ export function render(page, { budget = 500, from = 0 } = {}) {
// An input on the page adds no action. 'fill' and 'submit' are still stubs
// that throw, and this footer is the line an agent trusts for what to run
// next, so naming one of them costs a turn and returns nothing.
//
// 'find' comes before 'read' because it is the cheaper way to go deeper: one
// command lands on the block that matters, where 'read' needs the right
// number first and 'next' pages toward it. The entry costs about three
// tokens on every render, and skipping one 'next' on a long page pays for
// a hundred of them.
const actions = [
hasLinks && 'do <n>',
'find <query>',
'read <n>',
rest.length && 'next',
'raw',
+20
View File
@@ -282,3 +282,23 @@ test('no footer names a command that is not available yet', async () => {
}
}
});
test('every footer offers find, the cheapest way to go deeper on a page', () => {
// SKILL.md lists find first under "going further, cheapest first", and the
// footer is what an agent actually reads, so the two have to agree. Same
// three footer sites as the stub probe above: a render, and both of find's
// paths.
open();
const outputs = [
render(page(), { budget: 500 }).text,
find('postgres'),
find('a'),
];
const footers = outputs.map((out) => out.split('\n').find((line) => line.startsWith('actions:')));
assert.equal(footers.filter(Boolean).length, outputs.length, `every output should carry a footer:\n${footers.join('\n')}`);
for (const footer of footers) {
assert.ok(footer.includes('find <query>'), `footer should offer find:\n${footer}`);
// Cheapest first: find is listed ahead of read.
assert.ok(footer.indexOf('find <query>') < footer.indexOf('read <n>'), `find should come before read:\n${footer}`);
}
});