Public: anyone can read this; the crew can change it.
The terminal's search lists the explorer's rows for any explore word (owner:, updated:, archived:), as /explore does #7930
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/7926-the-terminal-s-search-lists-the-explorer → main 3 commits fork
6 files
changed +26
−6
docs/MCP.md +1 −1
| @@ -78,7 +78,7 @@ for a tunnel and a forge that sleeps. | ||
| 78 | 78 | | `egress_grants` | `{ repo, op?, destination? }`: the private `host:port` destinations an app's forge.json asks for and which are granted; `op` grant or revoke by a site admin (never an agent), and every op needs an admin-scope token; the same as the settings page and `/api/v1/repos/:owner/:name/egress-grants` | yes | |
| 79 | 79 | | `email_verify` | no input: send the caller a new verification email, with the API's rate limit and errors; the answer never carries the link | yes | |
| 80 | 80 | | `events` | `{ repo, kind?, limit?, cursor? }`: the repository's events (of one `kind`, e.g. `repo.settings_updated`, when given), newest first, as you may see them (a webhook's URL and an app's variable names are the admins'): `{ items, next_cursor }`, `limit` 1 to 200 (default 50), `cursor` from `next_cursor` | | |
| 81 | | `explore` | `{ query?, cursor? }`: the apps and people you can see, ranked, with their numbers; `kind:` `sort:` and words in one line; page on with `cursor` | | | |
| 81 | | `explore` | `{ query?, cursor? }`: the apps and people you can see, ranked, with their numbers; `kind:` `sort:` `updated:` `owner:` `archived:` and words in one line; page on with `cursor` | | | |
| 82 | 82 | | `family` | the forge that sent this one and, for a site admin, those it sent, with state and version | | |
| 83 | 83 | | `feed` | `{ limit?, cursor? }`: your home timeline — tells, replies, runs, changes and looks — ranked, with who, what, why and a link; the same items as `GET /api/v1/feed`; `digest` (first page only) is home's "Since you were here" card, `{ heading, lines: [{ repo, says, href }] }` or null; `quietRuns` counts agent runs of the last 24 h that pushed, opened or said nothing, which the timeline leaves out; an item with `lines: [{ text, link }]` and `more` is a folded burst (3+ issues one author opened in one repository within 2 minutes: `planned N issues in owner/name`, linking to its map) — `lines` are the first three, `more` the rest | | |
| 84 | 84 | | `feed_chips` | `{ kind?, value?, hidden? }`: the feed chips you switched off, which `feed` and `asks` leave out; with `kind` (`kind` or `person`), `value` and `hidden` it switches one off or on | yes | |
src/actions/explore.ts +7
| @@ -92,6 +92,13 @@ export function parseExploreQuery(line: string): ExploreQuery { | ||
| 92 | 92 | return readExploreLine(line).query; |
| 93 | 93 | } |
| 94 | 94 | |
| 95 | const EXPLORE_KEYS = /(^|\s)(kind|sort|updated|owner|archived):/i; | |
| 96 | ||
| 97 | /** Whether a line has any word the explorer reads as a key, so every surface takes it as the explorer's. */ | |
| 98 | export function isExploreLine(line: string): boolean { | |
| 99 | return EXPLORE_KEYS.test(line); | |
| 100 | } | |
| 101 | ||
| 95 | 102 | /** The query and the `kind:`/`sort:` words it did not know, as typed. */ |
| 96 | 103 | export interface ExploreRead { |
| 97 | 104 | readonly query: ExploreQuery; |
src/http/mcp/tools/explore.ts +2 −2
| @@ -3,7 +3,7 @@ import { bad, good, type Tool, str } from "../shared.ts"; | ||
| 3 | 3 | |
| 4 | 4 | export const tool: Tool = { |
| 5 | 5 | name: "explore", |
| 6 | doc: "`{ query?, cursor? }`: the apps and people you can see, ranked, with their numbers; `kind:` `sort:` and words in one line; page on with `cursor`", | |
| 6 | doc: "`{ query?, cursor? }`: the apps and people you can see, ranked, with their numbers; `kind:` `sort:` `updated:` `owner:` `archived:` and words in one line; page on with `cursor`", | |
| 7 | 7 | description: |
| 8 | 8 | "The apps, people (and, for an admin, forges) you can see, ranked.", |
| 9 | 9 | inputSchema: { |
| @@ -12,7 +12,7 @@ export const tool: Tool = { | ||
| 12 | 12 | query: { |
| 13 | 13 | type: "string", |
| 14 | 14 | maxLength: 200, |
| 15 | description: "Words; kind: sort: updated:7d", | |
| 15 | description: "Words; kind: sort: updated:7d owner: archived:", | |
| 16 | 16 | }, |
| 17 | 17 | cursor: { |
| 18 | 18 | type: "string", |
src/tui/screens/dashboard.ts +2 −2
| @@ -14,7 +14,7 @@ | ||
| 14 | 14 | * whole of this screen's design. |
| 15 | 15 | */ |
| 16 | 16 | |
| 17 | import { exploreLine, exploreNote, explorePage, parseExploreQuery, readExploreLine, type ExploreRow } from "../../actions/explore.ts"; | |
| 17 | import { exploreLine, isExploreLine, exploreNote, explorePage, parseExploreQuery, readExploreLine, type ExploreRow } from "../../actions/explore.ts"; | |
| 18 | 18 | import { protectionNotice } from "../../kernel/protection.ts"; |
| 19 | 19 | import { holdWords, plannerHold } from "../../crew/autonomy.ts"; |
| 20 | 20 | import { governorSeen } from "../../crew/governed.ts"; |
| @@ -221,7 +221,7 @@ export function dashboardScreen(f: Session, ctx: Ctx): Screen { | ||
| 221 | 221 | */ |
| 222 | 222 | let explored: { line: string; rows: readonly ExploreRow[] } | undefined; |
| 223 | 223 | let askedLine = ""; |
| 224 | const exploring = (): boolean => /(^|\s)(kind|sort):/i.test(filter.query); | |
| 224 | const exploring = (): boolean => isExploreLine(filter.query); | |
| 225 | 225 | const exploredRows = (): readonly ExploreRow[] => (exploring() ? (explored?.rows ?? []) : []); |
| 226 | 226 | const syncExplore = (): void => { |
| 227 | 227 | if (!exploring()) { |
test/explore.test.ts +7 −1
| @@ -9,7 +9,7 @@ import { mkdtemp, rm } from "node:fs/promises"; | ||
| 9 | 9 | import { tmpdir } from "node:os"; |
| 10 | 10 | import { join } from "node:path"; |
| 11 | 11 | import { createApp, type App } from "../src/app.ts"; |
| 12 | import { explorePage, exploreLine, parseExploreQuery } from "../src/actions/explore.ts"; | |
| 12 | import { explorePage, exploreLine, isExploreLine, parseExploreQuery } from "../src/actions/explore.ts"; | |
| 13 | 13 | import { suspendAccount } from "../src/actions/accounts.ts"; |
| 14 | 14 | import { createRepo } from "../src/actions/repos.ts"; |
| 15 | 15 | import { createApp as insertApp } from "../src/core/apps.ts"; |
| @@ -624,3 +624,9 @@ describe("explore past the first page", () => { | ||
| 624 | 624 | expect(seen.length).toBe(COUNT - 14 + 1); |
| 625 | 625 | }); |
| 626 | 626 | }); |
| 627 | ||
| 628 | test("isExploreLine is true for each explore key and false for plain words", () => { | |
| 629 | for (const line of ["kind:app", "sort:forks", "updated:7d", "owner:trevato", "archived:yes", "garden OWNER:x"]) | |
| 630 | expect(isExploreLine(line)).toBe(true); | |
| 631 | for (const line of ["", "garden", "zed-one", "downer:x"]) expect(isExploreLine(line)).toBe(false); | |
| 632 | }); |
test/tui-dashboard-explore.test.ts +7
| @@ -162,3 +162,10 @@ test("an unknown sort is named under Read as, with the words it takes", async () | ||
| 162 | 162 | const text = await typed("kind:app sort:nonsense"); |
| 163 | 163 | expect(text).toContain("sort:nonsense is not a sort — use score, forks"); |
| 164 | 164 | }); |
| 165 | ||
| 166 | test("owner:<name> alone lists that owner's apps as the explorer", async () => { | |
| 167 | const text = await typed("owner:alice"); | |
| 168 | expect(text).toContain("Read as: owner:alice"); | |
| 169 | expect(text).toContain("alice/zed"); | |
| 170 | expect(text).not.toContain("bob/zed-one"); | |
| 171 | }); |
new above
offline