{"version":3,"file":"tips.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/tips.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAIH,+DAA+D;AAC/D,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7C,MAAM,WAAW,GAAG;IACnB;;;OAGG;IACH,EAAE,EAAE,MAAM,CAAC;IACX,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC;IACnC,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,IAAI,EAAE,SAAS,GAAG,EAkL9B,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,EAAE,GAKxB,CAAC;AAEF,yDAAyD;AACzD,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAWlC,MAAM,WAAW,kBAAkB;IAClC,6BAA6B;IAC7B,IAAI,CAAC,EAAE,SAAS,GAAG,EAAE,CAAC;IACtB,mDAAmD;IACnD,IAAI,EAAE,MAAM,SAAS,MAAM,EAAE,CAAC;IAC9B,4CAA4C;IAC5C,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,0EAA0E;IAC1E,cAAc,EAAE,MAAM,MAAM,CAAC;IAC7B,kCAAkC;IAClC,aAAa,EAAE,MAAM,IAAI,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,qBAAa,WAAW;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAiB;IACtC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAqB;IAC1C,oFAAkF;IAClF,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,qEAAqE;IACrE,OAAO,CAAC,cAAc,CAAK;IAC3B,sEAAsE;IACtE,OAAO,CAAC,qBAAqB,CAAS;IAEtC,YAAY,OAAO,EAAE,kBAAkB,EAGtC;IAED;;;;;;;;;OASG;IACH,IAAI,CAAC,MAAM,EAAE,SAAS,GAAG,GAAG,GAAG,SAAS,CAevC;IAED,OAAO,CAAC,IAAI;IAQZ,OAAO,CAAC,eAAe;CAQvB;AAMD,oDAAoD;AACpD,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAGpF","sourcesContent":["/**\n * Tips, and the one place they are written down.\n *\n * ## Why this exists\n *\n * HooCode has four modes, ~40 slash commands, a rebindable key for most dials,\n * skills, plugins, canvases and a subagent runner. None of that is discoverable\n * by using the product: the prompt looks like a prompt, so people find `/help`,\n * find three things, and use those three things forever. The features that\n * would have saved them the most time are exactly the ones they never learn\n * exist.\n *\n * The fix is not a longer help page — it is putting one small thing in front of\n * someone at a moment when they are not busy. There are two such moments in a\n * session and the band above the prompt already owns both of them:\n *\n *   - **idle**: the prompt is up, nothing is streaming, and the user has not\n *     touched a key in a while. They are reading, or thinking, or away.\n *   - **streaming**: the agent has been working for long enough that the user is\n *     watching a spinner. That time is already spent; spending it on one line\n *     costs nothing.\n *\n * ## The rules a tip obeys\n *\n * A tip is the lowest-priority thing on the screen, and the rules below all come\n * from that one fact:\n *\n *   - It never interrupts. `TipsController` posts only when the band is empty,\n *     so a tip can never push aside, delay, or replace something the user\n *     actually caused.\n *   - It never repeats until everything else has been said. The rotation walks\n *     unseen tips first and remembers across sessions, so a returning user does\n *     not get taught `alt+a` five times.\n *   - It is short. Headline plus at most two rows, the same budget any glimpse\n *     gets, because it fades on the same clock.\n *   - It can be turned off, in one place, forever (`/settings`, or\n *     `tips.enabled: false`).\n *\n * ## Keys are resolved late\n *\n * A tip that names a key reads it out of the live keybinding manager at display\n * time rather than baking a string in here. Someone who rebound the mode dial\n * should be taught *their* key, and a tip that teaches the wrong one is worse\n * than no tip.\n *\n * ## Adding a tip\n *\n * Add a row to {@link TIPS}. Give it an id that will never be reused (the id is\n * what \"already seen\" is stored against, so renaming one re-teaches it to\n * everybody). Keep the headline under ~50 columns so it survives a narrow\n * terminal, and put anything longer in `body`.\n */\n\nimport { keyText } from \"./components/keybinding-hints.js\";\n\n/** Where a tip is allowed to appear. Most are fine in both. */\nexport type TipMoment = \"idle\" | \"streaming\";\n\nexport interface Tip {\n\t/**\n\t * Stable forever. \"Seen\" is recorded against this, so changing an id\n\t * re-teaches the tip to every existing user.\n\t */\n\tid: string;\n\t/** One line. Keep it short enough to survive a narrow terminal. */\n\ttitle: string;\n\t/**\n\t * Rows under the headline. A function when the text names a key or anything\n\t * else that is read from live config — it is called at display time.\n\t */\n\tbody?: string[] | (() => string[]);\n\t/** Right-aligned afterword, usually the key or command being taught. */\n\tnote?: string;\n\t/**\n\t * Which moments this tip suits. Defaults to both. A tip about interrupting a\n\t * turn is only useful while a turn is running; one about starting a session\n\t * is only useful when nothing is.\n\t */\n\tmoments?: readonly TipMoment[];\n}\n\n/**\n * The tips.\n *\n * Ordered roughly by how soon a new user benefits: the dials and the two\n * commands that answer \"what else is there\" first, then session shape, then the\n * extension surfaces, then the things you only want once you are living in it.\n * The rotation walks this order for anyone who has seen nothing.\n */\nexport const TIPS: readonly Tip[] = [\n\t{\n\t\tid: \"modes\",\n\t\ttitle: \"Four modes, one key\",\n\t\tbody: () => [\n\t\t\t`${keyText(\"app.mode.cycleForward\")} cycles ask → plan → build → debug.`,\n\t\t\t\"Ask never edits. Plan writes a plan, not code.\",\n\t\t],\n\t\tnote: \"/mode\",\n\t},\n\t{\n\t\tid: \"thinking\",\n\t\ttitle: \"Turn the thinking dial up for hard problems\",\n\t\tbody: () => [`${keyText(\"app.thinking.cycleForward\")} steps off → minimal → low → medium → high.`],\n\t\tnote: \"/settings\",\n\t},\n\t{\n\t\tid: \"hotkeys\",\n\t\ttitle: \"Every shortcut, on one screen\",\n\t\tbody: () => [`${keyText(\"app.hotkeys.open\")} or /hotkeys. They are all rebindable.`],\n\t\tnote: \"/hotkeys\",\n\t},\n\t{\n\t\tid: \"settings\",\n\t\ttitle: \"Settings live behind one key\",\n\t\tbody: () => [`${keyText(\"app.settings.open\")} or /settings — models, chrome, search, voice, tools.`],\n\t\tnote: \"/settings\",\n\t},\n\t{\n\t\tid: \"at-files\",\n\t\ttitle: \"Type @ to point at a file\",\n\t\tbody: [\"Fuzzy path completion, so you never paste a path again.\"],\n\t\tnote: \"@\",\n\t},\n\t{\n\t\tid: \"interrupt\",\n\t\ttitle: \"Going the wrong way? Stop it.\",\n\t\tbody: () => [`${keyText(\"app.interrupt\")} aborts the turn. What it already did stays.`],\n\t\tmoments: [\"streaming\"],\n\t},\n\t{\n\t\tid: \"queue\",\n\t\ttitle: \"You can type while it works\",\n\t\tbody: [\"Messages you send mid-turn queue up and go next.\"],\n\t\tmoments: [\"streaming\"],\n\t},\n\t{\n\t\tid: \"hoo-alias\",\n\t\ttitle: \"`hoo` is the same thing as `hoocode`\",\n\t\tbody: [\"Four fewer keys, several times a day.\"],\n\t},\n\t{\n\t\tid: \"fork\",\n\t\ttitle: \"Fork rather than re-explain\",\n\t\tbody: [\"/fork rewinds to an earlier message and branches from there.\"],\n\t\tnote: \"/fork\",\n\t},\n\t{\n\t\tid: \"tree\",\n\t\ttitle: \"A session is a tree, not a line\",\n\t\tbody: [\"/tree walks the branches you have made and switches between them.\"],\n\t\tnote: \"/tree\",\n\t},\n\t{\n\t\tid: \"resume\",\n\t\ttitle: \"Yesterday's session is still there\",\n\t\tbody: () => [`${keyText(\"app.session.resume\")} or /resume picks it back up where you left it.`],\n\t\tnote: \"/resume\",\n\t},\n\t{\n\t\tid: \"compact\",\n\t\ttitle: \"Long session slowing down?\",\n\t\tbody: [\"/compact summarises the context and keeps going.\"],\n\t\tnote: \"/compact\",\n\t\tmoments: [\"idle\"],\n\t},\n\t{\n\t\tid: \"search\",\n\t\ttitle: \"Search ranks by meaning, not just by keyword\",\n\t\tbody: [\"The search tool fuses lexical and semantic hits when embsearch is installed.\"],\n\t},\n\t{\n\t\tid: \"agents-md\",\n\t\ttitle: \"Teach it your project once\",\n\t\tbody: [\"An AGENTS.md at the repo root is read every session. Rules, conventions, gotchas.\"],\n\t\tnote: \"AGENTS.md\",\n\t},\n\t{\n\t\tid: \"learn\",\n\t\ttitle: \"It can write its own AGENTS.md\",\n\t\tbody: [\"/learn reads your past sessions and proposes rules from what actually kept happening.\"],\n\t\tnote: \"/learn\",\n\t},\n\t{\n\t\tid: \"plugins\",\n\t\ttitle: \"One-click plugins\",\n\t\tbody: [\"/plugin browses the marketplaces and installs into this project or your user config.\"],\n\t\tnote: \"/plugin\",\n\t},\n\t{\n\t\tid: \"skills\",\n\t\ttitle: \"Skills are just a folder and a SKILL.md\",\n\t\tbody: [\"/new-skill scaffolds one. /reload picks it up without restarting.\"],\n\t\tnote: \"/new-skill\",\n\t},\n\t{\n\t\tid: \"subagent\",\n\t\ttitle: \"Hand a side-quest to a subagent\",\n\t\tbody: [\"/subagent <mode> <task> runs it in its own context and reports back.\"],\n\t\tnote: \"/subagent\",\n\t},\n\t{\n\t\tid: \"canvas\",\n\t\ttitle: \"Canvases put a UI in the terminal\",\n\t\tbody: [\"/canvas opens one. /new-canvas starts your own.\"],\n\t\tnote: \"/canvas\",\n\t},\n\t{\n\t\tid: \"export\",\n\t\ttitle: \"Take the session with you\",\n\t\tbody: [\"/export writes styled HTML (or .jsonl). /share puts it in a secret gist.\"],\n\t\tnote: \"/export\",\n\t},\n\t{\n\t\tid: \"copy\",\n\t\ttitle: \"Copy the reply, not a picture of it\",\n\t\tbody: [\"/copy gives you real markdown — /copy all, or /copy <turns>.\"],\n\t\tnote: \"/copy\",\n\t},\n\t{\n\t\tid: \"models\",\n\t\ttitle: \"Swap models mid-session\",\n\t\tbody: [\"/model picks one. /scoped-models chooses which ones the cycle key walks.\"],\n\t\tnote: \"/model\",\n\t},\n\t{\n\t\tid: \"cd\",\n\t\ttitle: \"Move without leaving\",\n\t\tbody: [\"/cd <path> starts a session there. Bare /cd goes home, /cd - goes back.\"],\n\t\tnote: \"/cd\",\n\t},\n\t{\n\t\tid: \"chrome\",\n\t\ttitle: \"Small terminal? Take the chrome back\",\n\t\tbody: [\"/chrome compact, or /chrome bare to get every row for the conversation.\"],\n\t\tnote: \"/chrome\",\n\t},\n\t{\n\t\tid: \"offline\",\n\t\ttitle: \"It works with no network\",\n\t\tbody: [\"HOOCODE_OFFLINE=1 skips every startup fetch; search and completion fall back to pure JS.\"],\n\t},\n\t{\n\t\tid: \"external-tools\",\n\t\ttitle: \"fd and rg make everything faster\",\n\t\tbody: [\"Already on your PATH? HooCode uses them. Otherwise it fetches them once, quietly.\"],\n\t},\n\t{\n\t\tid: \"reload\",\n\t\ttitle: \"No need to restart\",\n\t\tbody: [\"/reload re-reads keybindings, extensions, skills, prompts and themes.\"],\n\t\tnote: \"/reload\",\n\t},\n\t{\n\t\tid: \"cost\",\n\t\ttitle: \"Know what a turn cost\",\n\t\tbody: [\"/cost breaks down tokens and spend for this session.\"],\n\t\tnote: \"/cost\",\n\t},\n\t{\n\t\tid: \"issues\",\n\t\ttitle: \"Something broken or missing?\",\n\t\tbody: [\n\t\t\t\"Open an issue — bug reports and feature ideas are both welcome.\",\n\t\t\t\"github.com/kolisachint/hoocode/issues\",\n\t\t],\n\t\tmoments: [\"idle\"],\n\t},\n];\n\n/**\n * The star nudge.\n *\n * Not in {@link TIPS} because it is not a tip and must not be rationed like\n * one: it is an ask, it is the only line here that wants something rather than\n * gives something, and so it gets its own budget — rare, capped, and never\n * twice in a session. Treating it as one more row in the rotation would have it\n * come round as often as `/copy`, which is how a nudge becomes an advert.\n */\nexport const STAR_NUDGE: Tip = {\n\tid: \"star\",\n\ttitle: \"★ Enjoying HooCode? Star it.\",\n\tbody: [\"It is the cheapest way to help people find it.\", \"github.com/kolisachint/hoocode\"],\n\tmoments: [\"idle\"],\n};\n\n/** How many times, ever, the star nudge may be shown. */\nexport const STAR_NUDGE_LIMIT = 3;\n\n/**\n * How many tips go by between star nudges.\n *\n * Together with the per-session cap of one, this is what keeps the ask on the\n * right side of the line: someone who uses HooCode daily sees it three times in\n * their life, and someone who tries it once sees it at most once.\n */\nconst STAR_NUDGE_EVERY = 6;\n\nexport interface TipRotationOptions {\n\t/** Overridable for tests. */\n\ttips?: readonly Tip[];\n\t/** Tip ids already shown, across every session. */\n\tseen: () => readonly string[];\n\t/** Record that a tip has now been shown. */\n\tmarkSeen: (id: string) => void;\n\t/** How many times the star nudge has been shown, across every session. */\n\tstarNudgeCount: () => number;\n\t/** Record one more star nudge. */\n\tmarkStarNudge: () => void;\n}\n\n/**\n * Picks what to say next, and remembers what it has said.\n *\n * Kept apart from the controller that schedules it because *what* to show and\n * *when* to show it are different problems with different tests: this half is\n * pure, has no timers, and can be walked end to end in a unit test.\n */\nexport class TipRotation {\n\tprivate readonly tips: readonly Tip[];\n\tprivate readonly opts: TipRotationOptions;\n\t/** Shown since this process started — the rotation never repeats within a run. */\n\tprivate readonly shownThisSession = new Set<string>();\n\t/** Tips emitted since the last star nudge, for the cadence above. */\n\tprivate sinceStarNudge = 0;\n\t/** The ask is once per session, whatever the lifetime budget says. */\n\tprivate starNudgedThisSession = false;\n\n\tconstructor(options: TipRotationOptions) {\n\t\tthis.opts = options;\n\t\tthis.tips = options.tips ?? TIPS;\n\t}\n\n\t/**\n\t * The next thing to show at this moment, or undefined when there is nothing\n\t * left worth saying.\n\t *\n\t * Unseen tips come first and in declaration order, which is roughly \"what\n\t * helps a new user soonest\". Once everything has been seen the rotation\n\t * starts over, skipping only what this run has already shown — a session\n\t * long enough to exhaust the list has earned a repeat, and silence would\n\t * read as the feature having broken.\n\t */\n\tnext(moment: TipMoment): Tip | undefined {\n\t\tif (this.shouldNudgeStar(moment)) {\n\t\t\tthis.sinceStarNudge = 0;\n\t\t\tthis.starNudgedThisSession = true;\n\t\t\tthis.opts.markStarNudge();\n\t\t\treturn STAR_NUDGE;\n\t\t}\n\n\t\tconst tip = this.pick(moment);\n\t\tif (!tip) return undefined;\n\n\t\tthis.shownThisSession.add(tip.id);\n\t\tthis.sinceStarNudge += 1;\n\t\tthis.opts.markSeen(tip.id);\n\t\treturn tip;\n\t}\n\n\tprivate pick(moment: TipMoment): Tip | undefined {\n\t\tconst eligible = this.tips.filter((tip) => suitsMoment(tip, moment) && !this.shownThisSession.has(tip.id));\n\t\tif (eligible.length === 0) return undefined;\n\n\t\tconst seen = new Set(this.opts.seen());\n\t\treturn eligible.find((tip) => !seen.has(tip.id)) ?? eligible[0];\n\t}\n\n\tprivate shouldNudgeStar(moment: TipMoment): boolean {\n\t\tif (this.starNudgedThisSession) return false;\n\t\tif (!suitsMoment(STAR_NUDGE, moment)) return false;\n\t\tif (this.opts.starNudgeCount() >= STAR_NUDGE_LIMIT) return false;\n\t\t// Never the first thing someone sees. Asking for a star before having\n\t\t// been useful once is the whole reason these prompts are resented.\n\t\treturn this.sinceStarNudge >= STAR_NUDGE_EVERY;\n\t}\n}\n\nfunction suitsMoment(tip: Tip, moment: TipMoment): boolean {\n\treturn tip.moments === undefined || tip.moments.includes(moment);\n}\n\n/** Resolve a tip's late-bound parts for display. */\nexport function renderTip(tip: Tip): { title: string; body: string[]; note?: string } {\n\tconst body = typeof tip.body === \"function\" ? tip.body() : (tip.body ?? []);\n\treturn { title: tip.title, body, note: tip.note };\n}\n"]}