Public: anyone can read this; the crew can change it.

Terminal waves: a row opens its wave file, and a live browse says it cannot read waves #2102

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/2076-terminal-waves-a-row-opens-its-wave-file → main 2 commits fork

  • Merged. scout/2076-terminal-waves-a-row-opens-its-wave-file was merged into main as 47f611f.

Review

Sign in to review this change.
3 files changed +81 −9
README.md +3 −1
@@ -972,7 +972,9 @@ work here.
972972 earlier wave does not parse (naming the file) or nothing new has merged.
973973 `/:owner/:name/waves` lists the waves on the default branch, newest version
974974 first, each linking to its file (linked from that settings card); in the
975 terminal, `V` in a repository opens the same list.
975 terminal, `V` in a repository opens the same list, and Enter on a row opens
976 its `waves/<file>` in the file viewer (a live browse of another forge says
977 waves are only read on this forge).
976978
977979 - **Accounts & access** — register/sign in, personal access tokens with coarse
978980 scopes (ro/rw/admin), public and private repositories.
src/tui/screens/waves.ts +29 −5
@@ -1,7 +1,8 @@
11 /**
22 * A repository's waves: the same rows, in the same order, as the web page,
33 * read through `story/waves.ts`. Local repositories only — a live browse of
4 * another forge has no `waves/` we can read here, and says so.
4 * another forge has no `waves/` we can read here, and says so. Enter on a row
5 * opens its file in the workspace viewer on the default branch.
56 */
67
78 import { canRead } from "../../core/repos.ts";
@@ -14,7 +15,8 @@ import { truncate } from "../term/width.ts";
1415 import { fill, len, split } from "../ui/layout.ts";
1516 import { emptyState } from "../ui/widgets.ts";
1617 import type { Session } from "../session.ts";
17 import { ListState, drawList, plural, runAsync } from "./common.ts";
18 import { ListState, drawList, hitList, plural, runAsync } from "./common.ts";
19 import { blobScreen } from "./workspace.ts";
1820 import { localRepo, sourceName, whereRows, whereStrip } from "./where.ts";
1921
2022 /** One line per wave, as the screen draws it. */
@@ -36,6 +38,14 @@ export function wavesScreen(f: Session, ctx: Ctx, src: RepoSource): Screen {
3638 let loaded = false;
3739 const state = new ListState();
3840
41 const openAt = (i: number): void => {
42 const entry = entries[i];
43 if (entry === undefined || repo === undefined) return;
44 ctx.push(
45 blobScreen(f, ctx, src, repo.defaultBranch, `waves/${entry.file}`),
46 );
47 };
48
3949 const reload = async (): Promise<void> => {
4050 // The web's gate, not a weaker one: whoever reaches this screen, a
4151 // repository they cannot read has no waves to show.
@@ -49,7 +59,11 @@ export function wavesScreen(f: Session, ctx: Ctx, src: RepoSource): Screen {
4959
5060 return {
5161 title: "Waves",
52 keys: [binding(["j", "k"], "move"), binding(["r"], "refresh")],
62 keys: [
63 binding(["j", "k"], "move"),
64 binding(["enter"], "open"),
65 binding(["r"], "refresh"),
66 ],
5367 load: reload,
5468 render(area, s) {
5569 const c = ctx.theme.c;
@@ -72,14 +86,24 @@ export function wavesScreen(f: Session, ctx: Ctx, src: RepoSource): Screen {
7286 if (entries.length === 0) {
7387 emptyState(s, body!, c, {
7488 title: loaded ? "No waves yet" : "Reading…",
75 body: loaded ? "This repository has no waves/ directory." : "",
89 body: !loaded
90 ? ""
91 : repo === undefined
92 ? "Waves are only read on this forge."
93 : "This repository has no waves/ directory.",
7694 });
7795 return;
7896 }
79 drawList(s, body!, c, state, waveRows(entries));
97 const rows = waveRows(entries);
98 drawList(s, body!, c, state, rows);
99 hitList(ctx, body!, state, rows.length, { open: openAt });
80100 },
81101 key(e) {
82102 if (state.key(e, entries.length)) return true;
103 if (e.key === "enter") {
104 openAt(state.index);
105 return true;
106 }
83107 if (e.key === "r") {
84108 runAsync(ctx, reload);
85109 return true;
test/tui-waves.test.ts +49 −3
@@ -10,6 +10,7 @@ import type { User } from "../src/core/users.ts";
1010 import { writeWave, type Wave } from "../src/story/wave.ts";
1111 import { createForgeApp } from "../src/tui/forge.ts";
1212 import { wavesScreen } from "../src/tui/screens/waves.ts";
13 import type { RepoSource } from "../src/view/source.ts";
1314 import { local } from "../src/tui/screens/where.ts";
1415 import { headlessTerminal } from "../src/tui/term/terminal.ts";
1516 import { quietLogs } from "./helpers/globals.ts";
@@ -24,6 +25,29 @@ const wave = (version: string, date: string, headline: string): Wave => ({
2425 highlights: [1, 2, 3].map((n) => ({ title: `Thing ${n}`, why: "because" })),
2526 pulls: [{ title: "#1 A pull", url: "https://forge.example/o/r/pulls/1" }],
2627 });
28 /** The same source, read live from another forge. */
29 const remoteOf = (src: RepoSource): RepoSource => ({
30 ...src,
31 where: {
32 kind: "origin",
33 origin: {
34 id: "o1",
35 userId: "u1",
36 kind: "github",
37 apiUrl: "https://api.github.test",
38 webUrl: "https://github.test",
39 login: "someone",
40 createdAt: "2026-01-01T00:00:00Z",
41 lastSyncAt: null,
42 lastError: "",
43 authKind: "pat",
44 expiresAt: null,
45 needsReauth: false,
46 },
47 fullName: "someone/else",
48 htmlUrl: "https://github.test/someone/else",
49 },
50 });
2751 const text = (w: Wave): string => {
2852 const r = writeWave(w);
2953 if (!r.ok) throw r.error;
@@ -68,7 +92,11 @@ describe("waves in the terminal", () => {
6892 await rm(dataDir, { recursive: true, force: true });
6993 });
7094
71 const open = async (user: User, repo: Repo): Promise<{ text: string; stop(): void }> => {
95 const open = async (
96 user: User,
97 repo: Repo,
98 remote = false,
99 ): Promise<{ text: string; stop(): void; app: ReturnType<typeof createForgeApp>["app"] }> => {
72100 const terminal = headlessTerminal(120, 40);
73101 const { app, session } = createForgeApp(
74102 forge,
@@ -77,9 +105,11 @@ describe("waves in the terminal", () => {
77105 );
78106 app.start();
79107 await settleApp(app);
80 app.push(wavesScreen(session, app.ctx, local(session, repo)));
108 const here = local(session, repo);
109 const src = remote ? remoteOf(here) : here;
110 app.push(wavesScreen(session, app.ctx, src));
81111 await settleApp(app);
82 return { text: app.screen.toText(), stop: () => app.stop() };
112 return { text: app.screen.toText(), stop: () => app.stop(), app };
83113 };
84114
85115 test("lists the web page's rows in the same order, bad files last", async () => {
@@ -98,4 +128,20 @@ describe("waves in the terminal", () => {
98128 expect(h.text).not.toContain("Secret launch");
99129 expect(h.text).toContain("No waves yet");
100130 });
131
132 test("Enter on a wave row opens its file in the viewer", async () => {
133 const h = await open(alice, pub);
134 h.app.dispatch({ type: "key", key: "enter", ctrl: false, alt: false, shift: false });
135 await settleApp(h.app);
136 const after = h.app.screen.toText();
137 h.stop();
138 expect(after).toContain("0.10.0.md");
139 });
140
141 test("a live browse says waves are only read on this forge", async () => {
142 const h = await open(alice, pub, true);
143 h.stop();
144 expect(h.text).toContain("only read on this forge");
145 expect(h.text).not.toContain("no waves/ directory");
146 });
101147 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs