/** * Tips, and the one place they are written down. * * ## Why this exists * * HooCode has four modes, ~40 slash commands, a rebindable key for most dials, * skills, plugins, canvases and a subagent runner. None of that is discoverable * by using the product: the prompt looks like a prompt, so people find `/help`, * find three things, and use those three things forever. The features that * would have saved them the most time are exactly the ones they never learn * exist. * * The fix is not a longer help page — it is putting one small thing in front of * someone at a moment when they are not busy. There are two such moments in a * session and the band above the prompt already owns both of them: * * - **idle**: the prompt is up, nothing is streaming, and the user has not * touched a key in a while. They are reading, or thinking, or away. * - **streaming**: the agent has been working for long enough that the user is * watching a spinner. That time is already spent; spending it on one line * costs nothing. * * ## The rules a tip obeys * * A tip is the lowest-priority thing on the screen, and the rules below all come * from that one fact: * * - It never interrupts. `TipsController` posts only when the band is empty, * so a tip can never push aside, delay, or replace something the user * actually caused. * - It never repeats until everything else has been said. The rotation walks * unseen tips first and remembers across sessions, so a returning user does * not get taught `alt+a` five times. * - It is short. Headline plus at most two rows, the same budget any glimpse * gets, because it fades on the same clock. * - It can be turned off, in one place, forever (`/settings`, or * `tips.enabled: false`). * * ## Keys are resolved late * * A tip that names a key reads it out of the live keybinding manager at display * time rather than baking a string in here. Someone who rebound the mode dial * should be taught *their* key, and a tip that teaches the wrong one is worse * than no tip. * * ## Adding a tip * * Add a row to {@link TIPS}. Give it an id that will never be reused (the id is * what "already seen" is stored against, so renaming one re-teaches it to * everybody). Keep the headline under ~50 columns so it survives a narrow * terminal, and put anything longer in `body`. */ /** Where a tip is allowed to appear. Most are fine in both. */ export type TipMoment = "idle" | "streaming"; export interface Tip { /** * Stable forever. "Seen" is recorded against this, so changing an id * re-teaches the tip to every existing user. */ id: string; /** One line. Keep it short enough to survive a narrow terminal. */ title: string; /** * Rows under the headline. A function when the text names a key or anything * else that is read from live config — it is called at display time. */ body?: string[] | (() => string[]); /** Right-aligned afterword, usually the key or command being taught. */ note?: string; /** * Which moments this tip suits. Defaults to both. A tip about interrupting a * turn is only useful while a turn is running; one about starting a session * is only useful when nothing is. */ moments?: readonly TipMoment[]; } /** * The tips. * * Ordered roughly by how soon a new user benefits: the dials and the two * commands that answer "what else is there" first, then session shape, then the * extension surfaces, then the things you only want once you are living in it. * The rotation walks this order for anyone who has seen nothing. */ export declare const TIPS: readonly Tip[]; /** * The star nudge. * * Not in {@link TIPS} because it is not a tip and must not be rationed like * one: it is an ask, it is the only line here that wants something rather than * gives something, and so it gets its own budget — rare, capped, and never * twice in a session. Treating it as one more row in the rotation would have it * come round as often as `/copy`, which is how a nudge becomes an advert. */ export declare const STAR_NUDGE: Tip; /** How many times, ever, the star nudge may be shown. */ export declare const STAR_NUDGE_LIMIT = 3; export interface TipRotationOptions { /** Overridable for tests. */ tips?: readonly Tip[]; /** Tip ids already shown, across every session. */ seen: () => readonly string[]; /** Record that a tip has now been shown. */ markSeen: (id: string) => void; /** How many times the star nudge has been shown, across every session. */ starNudgeCount: () => number; /** Record one more star nudge. */ markStarNudge: () => void; } /** * Picks what to say next, and remembers what it has said. * * Kept apart from the controller that schedules it because *what* to show and * *when* to show it are different problems with different tests: this half is * pure, has no timers, and can be walked end to end in a unit test. */ export declare class TipRotation { private readonly tips; private readonly opts; /** Shown since this process started — the rotation never repeats within a run. */ private readonly shownThisSession; /** Tips emitted since the last star nudge, for the cadence above. */ private sinceStarNudge; /** The ask is once per session, whatever the lifetime budget says. */ private starNudgedThisSession; constructor(options: TipRotationOptions); /** * The next thing to show at this moment, or undefined when there is nothing * left worth saying. * * Unseen tips come first and in declaration order, which is roughly "what * helps a new user soonest". Once everything has been seen the rotation * starts over, skipping only what this run has already shown — a session * long enough to exhaust the list has earned a repeat, and silence would * read as the feature having broken. */ next(moment: TipMoment): Tip | undefined; private pick; private shouldNudgeStar; } /** Resolve a tip's late-bound parts for display. */ export declare function renderTip(tip: Tip): { title: string; body: string[]; note?: string; }; //# sourceMappingURL=tips.d.ts.map