@underactive/pi-topping 0.6.5 → 0.7.1
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 +10 -6
- package/package.json +1 -1
- package/src/activity-meter.ts +3 -3
- package/src/format.ts +32 -12
- package/src/menu.ts +27 -39
- package/src/pi-installer.ts +15 -6
- package/src/preview.ts +18 -17
- package/src/prompt-decorator.ts +31 -17
- package/src/session.ts +50 -45
- package/src/settings.ts +79 -55
- package/src/setup-command.ts +94 -48
- package/src/toppings.ts +67 -18
- package/src/word-packs.ts +1 -1
- package/src/words.ts +2 -10
package/src/setup-command.ts
CHANGED
|
@@ -1,74 +1,99 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { hyperlink } from "@earendil-works/pi-tui";
|
|
3
|
-
import { showMenu } from "./menu.ts";
|
|
4
|
-
import { type PiInstallResult, spawnPiInstall } from "./pi-installer.ts";
|
|
5
|
-
import { stripControlChars } from "./util.ts";
|
|
3
|
+
import { type MenuValue, showMenu } from "./menu.ts";
|
|
4
|
+
import { type PiInstallResult, spawnPiInstall, spawnPiRemove } from "./pi-installer.ts";
|
|
6
5
|
import { setSetupCheckDisabled } from "./setup-check.ts";
|
|
7
|
-
import {
|
|
6
|
+
import { detectToppings, type ToppingStatus } from "./toppings.ts";
|
|
7
|
+
import { stripControlChars } from "./util.ts";
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const PI_COMMAND_TIMEOUT_MS = 120_000;
|
|
10
10
|
const STDERR_SNIPPET_CHARS = 300;
|
|
11
11
|
|
|
12
|
-
type
|
|
12
|
+
type PackageAction = "install" | "remove";
|
|
13
|
+
type PiPackageRunner = (spec: string, timeoutMs: number) => Promise<PiInstallResult>;
|
|
14
|
+
type FailedAction = { action: PackageAction; spec: string; error: string };
|
|
15
|
+
type SetupRow = { status: ToppingStatus; action: PackageAction };
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
function selectedRows(
|
|
18
|
+
rows: ReadonlyMap<string, SetupRow>,
|
|
19
|
+
values: Record<string, boolean>,
|
|
20
|
+
action: PackageAction,
|
|
21
|
+
): ToppingStatus[] {
|
|
22
|
+
return [...rows.entries()]
|
|
23
|
+
.filter(([id, row]) => row.action === action && values[id] === true)
|
|
24
|
+
.map(([, row]) => row.status);
|
|
18
25
|
}
|
|
19
26
|
|
|
20
|
-
function renderDescription(
|
|
21
|
-
|
|
27
|
+
function renderDescription(
|
|
28
|
+
rows: ReadonlyMap<string, SetupRow>,
|
|
29
|
+
activeItemId: string | undefined,
|
|
30
|
+
values: Record<string, MenuValue>,
|
|
31
|
+
): string[] {
|
|
32
|
+
if (activeItemId === undefined) return ["Select a topping to see what it does."];
|
|
33
|
+
const active = rows.get(activeItemId);
|
|
22
34
|
if (!active) return ["Select a topping to see what it does."];
|
|
23
|
-
const url = `https://pi.dev/packages/${active.topping.pkg}`;
|
|
24
|
-
|
|
35
|
+
const url = `https://pi.dev/packages/${active.status.topping.pkg}`;
|
|
36
|
+
const action = values[activeItemId] === true
|
|
37
|
+
? `Checked: will ${active.action === "install" ? "install" : "uninstall"}`
|
|
38
|
+
: "Unchecked: no change";
|
|
39
|
+
return [active.status.topping.provides, "", hyperlink(url, url), "", action];
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
function errorMessage(error: unknown): string {
|
|
28
43
|
return error instanceof Error ? error.message : String(error);
|
|
29
44
|
}
|
|
30
45
|
|
|
31
|
-
function
|
|
46
|
+
function snippet(text: string): string {
|
|
47
|
+
return stripControlChars(text.trim()).slice(0, STDERR_SNIPPET_CHARS);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildReport(installed: string[], removed: string[], failed: FailedAction[]): string {
|
|
32
51
|
const lines: string[] = [];
|
|
33
|
-
if (
|
|
52
|
+
if (installed.length > 0) lines.push(`✓ Installed: ${installed.join(", ")}`);
|
|
53
|
+
if (removed.length > 0) lines.push(`✓ Removed: ${removed.join(", ")}`);
|
|
34
54
|
if (failed.length > 0) {
|
|
35
55
|
lines.push("✗ Failed:");
|
|
36
|
-
for (const failure of failed) lines.push(` ${failure.spec}: ${failure.error}`);
|
|
56
|
+
for (const failure of failed) lines.push(` ${failure.action} ${failure.spec}: ${failure.error}`);
|
|
37
57
|
}
|
|
38
|
-
if (
|
|
58
|
+
if (installed.length > 0 || removed.length > 0) {
|
|
39
59
|
lines.push("");
|
|
40
|
-
lines.push("Restart Pi afterwards to
|
|
60
|
+
lines.push("Restart Pi afterwards to apply the changes.");
|
|
41
61
|
}
|
|
42
62
|
return lines.join("\n");
|
|
43
63
|
}
|
|
44
64
|
|
|
45
|
-
async function
|
|
65
|
+
async function runPackageCommands(
|
|
46
66
|
ctx: ExtensionCommandContext,
|
|
47
67
|
selected: readonly ToppingStatus[],
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
runner: PiPackageRunner,
|
|
69
|
+
action: PackageAction,
|
|
70
|
+
): Promise<{ succeeded: string[]; failed: FailedAction[] }> {
|
|
50
71
|
const succeeded: string[] = [];
|
|
51
|
-
const failed:
|
|
72
|
+
const failed: FailedAction[] = [];
|
|
52
73
|
for (const { topping } of selected) {
|
|
53
74
|
const spec = `npm:${topping.pkg}`;
|
|
54
|
-
ctx.ui.notify(
|
|
75
|
+
ctx.ui.notify(`${action === "install" ? "Installing" : "Removing"} ${spec}…`, "info");
|
|
55
76
|
try {
|
|
56
|
-
const result = await
|
|
77
|
+
const result = await runner(spec, PI_COMMAND_TIMEOUT_MS);
|
|
57
78
|
if (result.code === 0) {
|
|
58
79
|
succeeded.push(spec);
|
|
59
80
|
} else {
|
|
60
|
-
failed.push({ spec, error:
|
|
81
|
+
failed.push({ action, spec, error: snippet(result.stderr || result.stdout || `exit ${result.code}`) });
|
|
61
82
|
}
|
|
62
83
|
} catch (error) {
|
|
63
|
-
failed.push({ spec, error:
|
|
84
|
+
failed.push({ action, spec, error: snippet(errorMessage(error)) });
|
|
64
85
|
}
|
|
65
86
|
}
|
|
66
87
|
return { succeeded, failed };
|
|
67
88
|
}
|
|
68
89
|
|
|
69
|
-
export function registerSetupCommand(
|
|
90
|
+
export function registerSetupCommand(
|
|
91
|
+
pi: ExtensionAPI,
|
|
92
|
+
install: PiPackageRunner = spawnPiInstall,
|
|
93
|
+
remove: PiPackageRunner = spawnPiRemove,
|
|
94
|
+
): void {
|
|
70
95
|
pi.registerCommand("topping-setup", {
|
|
71
|
-
description: "Install
|
|
96
|
+
description: "Install or uninstall Pi Topping sibling extensions or manage the missing-extension notice.",
|
|
72
97
|
handler: async (args, ctx) => {
|
|
73
98
|
const action = args.trim();
|
|
74
99
|
if (action === "disable-side-toppings-check" || action === "enable-side-toppings-check") {
|
|
@@ -96,41 +121,62 @@ export function registerSetupCommand(pi: ExtensionAPI, install: ToppingInstaller
|
|
|
96
121
|
return;
|
|
97
122
|
}
|
|
98
123
|
|
|
99
|
-
const
|
|
100
|
-
|
|
124
|
+
const statuses = detectToppings(pi);
|
|
125
|
+
const removable = statuses.filter((status) => status.packageSource !== undefined);
|
|
126
|
+
const missing = statuses.filter((status) => !status.active && status.packageSource === undefined);
|
|
127
|
+
if (missing.length === 0 && removable.length === 0) {
|
|
101
128
|
ctx.ui.notify("All Pi Topping extensions are already active in this session.", "info");
|
|
102
129
|
return;
|
|
103
130
|
}
|
|
104
131
|
|
|
132
|
+
const rows = new Map<string, SetupRow>();
|
|
133
|
+
for (const status of missing) rows.set(`install:${status.topping.pkg}`, { status, action: "install" });
|
|
134
|
+
for (const status of removable) rows.set(`remove:${status.topping.pkg}`, { status, action: "remove" });
|
|
135
|
+
const sections = [];
|
|
136
|
+
if (missing.length > 0) {
|
|
137
|
+
sections.push({
|
|
138
|
+
title: "Missing Toppings",
|
|
139
|
+
items: missing.map(({ topping, onDisk }) => ({
|
|
140
|
+
id: `install:${topping.pkg}`,
|
|
141
|
+
label: `${topping.pkg}${onDisk ? " (on disk, not active)" : ""}`,
|
|
142
|
+
value: true,
|
|
143
|
+
})),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (removable.length > 0) {
|
|
147
|
+
sections.push({
|
|
148
|
+
title: "Installed Toppings",
|
|
149
|
+
items: removable.map(({ topping, active }) => ({
|
|
150
|
+
id: `remove:${topping.pkg}`,
|
|
151
|
+
label: `${topping.pkg}${active ? " (active)" : " (installed, not active)"}`,
|
|
152
|
+
value: false,
|
|
153
|
+
})),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
105
157
|
const result = await showMenu<Record<string, boolean>>(ctx, {
|
|
106
158
|
title: "Pi Topping: Setup",
|
|
107
|
-
sections
|
|
108
|
-
|
|
109
|
-
title: "Missing Toppings",
|
|
110
|
-
items: missing.map(({ topping, onDisk }) => ({
|
|
111
|
-
id: topping.pkg,
|
|
112
|
-
label: `${topping.pkg}${onDisk ? " (on disk, not active)" : ""}`,
|
|
113
|
-
value: true,
|
|
114
|
-
})),
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
hints: ["↑↓ move", "PgUp/PgDn page", "␣ toggle", "⏎ install", "esc cancel"],
|
|
159
|
+
sections,
|
|
160
|
+
hints: ["↑↓ move", "PgUp/PgDn page", "␣ toggle", "⏎ apply", "esc cancel"],
|
|
118
161
|
previewTitle: "Description",
|
|
119
|
-
preview: (
|
|
162
|
+
preview: (values, _elapsedMs, activeItemId) => ({ lines: renderDescription(rows, activeItemId, values) }),
|
|
120
163
|
});
|
|
121
164
|
if (!result.applied) {
|
|
122
165
|
ctx.ui.notify("Pi Topping setup cancelled.", "info");
|
|
123
166
|
return;
|
|
124
167
|
}
|
|
125
168
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
169
|
+
const removals = selectedRows(rows, result.values, "remove");
|
|
170
|
+
const installs = selectedRows(rows, result.values, "install");
|
|
171
|
+
if (removals.length === 0 && installs.length === 0) {
|
|
172
|
+
ctx.ui.notify("No Pi Topping changes selected.", "info");
|
|
129
173
|
return;
|
|
130
174
|
}
|
|
131
175
|
|
|
132
|
-
const
|
|
133
|
-
|
|
176
|
+
const removalResult = await runPackageCommands(ctx, removals, remove, "remove");
|
|
177
|
+
const installResult = await runPackageCommands(ctx, installs, install, "install");
|
|
178
|
+
const failed = [...removalResult.failed, ...installResult.failed];
|
|
179
|
+
ctx.ui.notify(buildReport(installResult.succeeded, removalResult.succeeded, failed), failed.length > 0 ? "warning" : "info");
|
|
134
180
|
},
|
|
135
181
|
});
|
|
136
182
|
}
|
package/src/toppings.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { getAgentDir, type ExtensionAPI, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { getAgentDir, type ExtensionAPI, type ExtensionUIContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { isSetupCheckDisabled } from "./setup-check.ts";
|
|
@@ -15,6 +16,8 @@ export interface ToppingStatus {
|
|
|
15
16
|
readonly topping: Topping;
|
|
16
17
|
readonly active: boolean;
|
|
17
18
|
readonly onDisk: boolean;
|
|
19
|
+
/** Exact npm source recorded in Pi's user settings for this topping. */
|
|
20
|
+
readonly packageSource: string | undefined;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export const TOPPINGS: readonly Topping[] = [
|
|
@@ -42,6 +45,12 @@ export const TOPPINGS: readonly Topping[] = [
|
|
|
42
45
|
dir: "pi-topping-web-tools",
|
|
43
46
|
provides: "web research and browser automation",
|
|
44
47
|
},
|
|
48
|
+
{
|
|
49
|
+
pkg: "@underactive/pi-topping-moa-fusion",
|
|
50
|
+
matches: /pi-topping-moa-fusion/i,
|
|
51
|
+
dir: "pi-topping-moa-fusion",
|
|
52
|
+
provides: "Mixture-of-Agents Fusion plan mode",
|
|
53
|
+
},
|
|
45
54
|
];
|
|
46
55
|
|
|
47
56
|
interface PiSettings {
|
|
@@ -51,6 +60,7 @@ interface PiSettings {
|
|
|
51
60
|
|
|
52
61
|
interface SettingsToppingState {
|
|
53
62
|
packageInstalled: boolean;
|
|
63
|
+
packageSource?: string;
|
|
54
64
|
extensionPresent: boolean;
|
|
55
65
|
extensionActive: boolean;
|
|
56
66
|
}
|
|
@@ -61,6 +71,22 @@ function stringEntries(value: unknown): string[] {
|
|
|
61
71
|
return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : [];
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
function sourceEntries(value: unknown): string[] {
|
|
75
|
+
if (!Array.isArray(value)) return [];
|
|
76
|
+
return value.flatMap((entry) => {
|
|
77
|
+
if (typeof entry === "string") return [entry];
|
|
78
|
+
return isPlainObject(entry) && typeof entry.source === "string" ? [entry.source] : [];
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function npmPackageName(source: string): string | undefined {
|
|
83
|
+
const value = source.trim();
|
|
84
|
+
if (!value.startsWith("npm:")) return undefined;
|
|
85
|
+
const spec = value.slice("npm:".length);
|
|
86
|
+
const versionIndex = spec.lastIndexOf("@");
|
|
87
|
+
return versionIndex > 0 ? spec.slice(0, versionIndex) : spec;
|
|
88
|
+
}
|
|
89
|
+
|
|
64
90
|
function readPiSettings(): PiSettings | undefined {
|
|
65
91
|
try {
|
|
66
92
|
const parsed: unknown = JSON.parse(readFileSync(join(getAgentDir(), "settings.json"), "utf8"));
|
|
@@ -78,10 +104,12 @@ function extensionEntry(entry: string): { value: string; disabled: boolean } {
|
|
|
78
104
|
|
|
79
105
|
function settingsState(settings: PiSettings | undefined, topping: Topping): SettingsToppingState {
|
|
80
106
|
let packageInstalled = false;
|
|
107
|
+
let packageSource: string | undefined;
|
|
81
108
|
let extensionPresent = false;
|
|
82
109
|
let extensionActive = false;
|
|
83
|
-
for (const entry of
|
|
110
|
+
for (const entry of sourceEntries(settings?.packages)) {
|
|
84
111
|
if (topping.matches.test(entry)) packageInstalled = true;
|
|
112
|
+
if (npmPackageName(entry) === topping.pkg) packageSource = entry;
|
|
85
113
|
}
|
|
86
114
|
for (const entry of stringEntries(settings?.extensions)) {
|
|
87
115
|
const normalized = extensionEntry(entry);
|
|
@@ -89,7 +117,7 @@ function settingsState(settings: PiSettings | undefined, topping: Topping): Sett
|
|
|
89
117
|
extensionPresent = true;
|
|
90
118
|
if (!normalized.disabled) extensionActive = true;
|
|
91
119
|
}
|
|
92
|
-
return { packageInstalled, extensionPresent, extensionActive };
|
|
120
|
+
return { packageInstalled, packageSource, extensionPresent, extensionActive };
|
|
93
121
|
}
|
|
94
122
|
|
|
95
123
|
function runtimeActiveToppings(pi: ExtensionAPI): Array<true | undefined> | undefined {
|
|
@@ -117,7 +145,7 @@ export function detectToppings(pi: ExtensionAPI): ToppingStatus[] {
|
|
|
117
145
|
const checkoutPresent = existsSync(join(extensionsDir, topping.dir));
|
|
118
146
|
const onDisk = configured.packageInstalled || configured.extensionPresent || checkoutPresent;
|
|
119
147
|
const fallbackActive = configured.packageInstalled || configured.extensionActive || checkoutPresent;
|
|
120
|
-
return { topping, active: runtime?.[index] ?? fallbackActive, onDisk };
|
|
148
|
+
return { topping, active: runtime?.[index] ?? fallbackActive, onDisk, packageSource: configured.packageSource };
|
|
121
149
|
});
|
|
122
150
|
}
|
|
123
151
|
|
|
@@ -125,31 +153,52 @@ export function findMissingToppings(pi: ExtensionAPI): ToppingStatus[] {
|
|
|
125
153
|
return detectToppings(pi).filter((status) => !status.active);
|
|
126
154
|
}
|
|
127
155
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
156
|
+
interface BannerLine {
|
|
157
|
+
readonly plain: string;
|
|
158
|
+
readonly styled: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function renderMissingBanner(missing: readonly ToppingStatus[], theme: Pick<Theme, "fg">): string {
|
|
162
|
+
const text = (value: string): string => theme.fg("text", value);
|
|
163
|
+
const accent = (value: string): string => theme.fg("accent", value);
|
|
164
|
+
const warning = (value: string): string => theme.fg("warning", value);
|
|
165
|
+
const title: BannerLine = {
|
|
166
|
+
plain: `pi-topping: ${missing.length} topping extensions missing`,
|
|
167
|
+
styled: text(`pi-topping: ${missing.length} topping extensions missing`),
|
|
168
|
+
};
|
|
169
|
+
const body: BannerLine[] = [
|
|
170
|
+
...missing.map(({ topping }) => ({
|
|
171
|
+
plain: `• ${topping.pkg} — ${topping.provides}`,
|
|
172
|
+
styled: `${text("• ")}${accent(topping.pkg)}${text(` — ${topping.provides}`)}`,
|
|
173
|
+
})),
|
|
174
|
+
{ plain: "", styled: "" },
|
|
175
|
+
{
|
|
176
|
+
plain: "Run /topping-setup to selectively install them",
|
|
177
|
+
styled: `${text("Run ")}${accent("/topping-setup")}${text(" to selectively install them")}`,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
plain: "Run /topping-setup disable-side-toppings-check to suppress this message",
|
|
181
|
+
styled: `${text("Run ")}${accent("/topping-setup disable-side-toppings-check")}${text(" to suppress this message")}`,
|
|
182
|
+
},
|
|
135
183
|
];
|
|
136
|
-
const
|
|
137
|
-
const
|
|
184
|
+
const titleWidth = visibleWidth(title.plain);
|
|
185
|
+
const width = Math.max(titleWidth + 6, ...body.map((line) => visibleWidth(line.plain) + 4));
|
|
186
|
+
const blankRow = `${warning("│")}${" ".repeat(width - 2)}${warning("│")}`;
|
|
138
187
|
return [
|
|
139
|
-
|
|
188
|
+
`${warning("╭─")} ${title.styled} ${warning("─".repeat(width - titleWidth - 5))}${warning("╮")}`,
|
|
140
189
|
blankRow,
|
|
141
|
-
...body.map((line) =>
|
|
190
|
+
...body.map((line) => `${warning("│")} ${line.styled}${" ".repeat(width - visibleWidth(line.plain) - 4)} ${warning("│")}`),
|
|
142
191
|
blankRow,
|
|
143
|
-
|
|
192
|
+
`${warning("╰")}${warning("─".repeat(width - 2))}${warning("╯")}`,
|
|
144
193
|
].join("\n");
|
|
145
194
|
}
|
|
146
195
|
|
|
147
|
-
export function notifyMissingToppingsOnce(pi: ExtensionAPI, ui: Pick<ExtensionUIContext, "notify">): void {
|
|
196
|
+
export function notifyMissingToppingsOnce(pi: ExtensionAPI, ui: Pick<ExtensionUIContext, "notify" | "theme">): void {
|
|
148
197
|
if (setupNoticeHandled) return;
|
|
149
198
|
setupNoticeHandled = true;
|
|
150
199
|
if (isSetupCheckDisabled()) return;
|
|
151
200
|
const missing = findMissingToppings(pi);
|
|
152
|
-
if (missing.length > 0) ui.notify(`\n${renderMissingBanner(missing)}`, "warning");
|
|
201
|
+
if (missing.length > 0) ui.notify(`\n${renderMissingBanner(missing, ui.theme)}`, "warning");
|
|
153
202
|
}
|
|
154
203
|
|
|
155
204
|
/** Test-only reset for the once-per-process session-start notice. */
|
package/src/word-packs.ts
CHANGED
|
@@ -112,7 +112,7 @@ export function selectWorkingTextSelection(enabled: Record<string, boolean>, pac
|
|
|
112
112
|
const total = WORDS.length + enabledWords.length;
|
|
113
113
|
const idx = Math.min(total - 1, Math.floor(fraction * total));
|
|
114
114
|
const entry = idx < WORDS.length ? WORDS[idx] : enabledWords[idx - WORDS.length];
|
|
115
|
-
return { text:
|
|
115
|
+
return { text: entry.present_tense, pastTense: entry.past_tense };
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
export function pickWorkingTextSelection(enabled: Record<string, boolean>, packs: readonly WordPack[]): WorkingTextSelection {
|
package/src/words.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Activity word list for the randomized "Working
|
|
2
|
+
* Activity word list for the randomized "Working" indicator.
|
|
3
3
|
*
|
|
4
4
|
* A random word is picked on prompt and on tool execution start
|
|
5
|
-
* (see
|
|
5
|
+
* (see SessionManager in src/session.ts).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export interface WordEntry {
|
|
@@ -10,14 +10,6 @@ export interface WordEntry {
|
|
|
10
10
|
past_tense: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function pickRawWord(): WordEntry {
|
|
14
|
-
return WORDS[Math.floor(Math.random() * WORDS.length)]!;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function pickRandomWord(): string {
|
|
18
|
-
return `${pickRawWord().present_tense}…`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
13
|
export const WORDS: WordEntry[] = [
|
|
22
14
|
{ present_tense: "Accomplishing", past_tense: "Accomplished" },
|
|
23
15
|
{ present_tense: "Actioning", past_tense: "Actioned" },
|