{"version":3,"file":"chrome-layout.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/chrome-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAEpF,OAAO,EAAE,gBAAgB,EAAE,KAAK,aAAa,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAErG;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,aAAa,CAAC;IACvB,+DAA+D;IAC/D,gBAAgB,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC5B,sEAAsE;IACtE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;IACnC,4EAA0E;IAC1E,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;CACrC;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAAE,OAAO,EAAE,gBAAgB,EAAE,EAAE,YAAY,GAAG,YAAY,CA2BvF;AAED,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC9B,UAAU,EAAE,IAAI,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACjD,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;CACnD;AAED;;;;;;GAMG;AACH,qBAAa,sBAAsB;IAKjC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAJ1B,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,OAAO,CAA6B;IAE5C,YACkB,QAAQ,EAAE,cAAc,EACzC,OAAO,EAAE,aAAa,EAGtB;IAED,IAAI,OAAO,IAAI,aAAa,CAE3B;IAED,wEAAwE;IACxE,IAAI,MAAM,IAAI,YAAY,CAEzB;IAED,UAAU,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAI1C;IAED,0EAA0E;IAC1E,YAAY,CAAC,SAAS,EAAE,SAAS,GAAG,UAAU,GAAG,aAAa,CAM7D;IAED,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAI1C;IAED,wEAAwE;IACxE,KAAK,IAAI,OAAO,CAaf;CACD","sourcesContent":["/**\n * How much of the screen the chrome is allowed to have.\n *\n * ## The problem\n *\n * Nine things hang off the TUI root, and seven of them sit *below* the\n * transcript: queued messages, status rows, two widget containers, the task\n * ledger, the prompt, and the footer. On a thirty-row terminal that routinely\n * comes to eight or twelve rows — a third of the screen spent on furniture,\n * taken from the conversation. Each of those components also decided its own\n * visibility, in its own way, in its own file: the ledger self-hides when it has\n * no tasks, the footer always draws, the header is cleared by whoever remembers\n * to. There was no one place to ask \"what is on screen right now\", and so no\n * place to change it.\n *\n * ## The shape\n *\n * One dial and one table. `ChromeDensity` is the dial — an ordered set of stops\n * the way the other six are, so `alt+z` steps it and `shift+alt+z` steps back,\n * and the answer is always reachable in one more press. `resolveChrome` is the\n * table: a pure function from (dial, what is happening) to what each slot\n * shows. Adding a stop, or a new reason to hide something, is an edit to that\n * one function rather than a hunt through seven components.\n *\n * The prompt is deliberately not in the table. Everything else can go, but a\n * screen you can type into and not see is the worst possible failure here, and\n * a dial that can reach that state will eventually be left in it.\n *\n * ## Speed\n *\n * Two rules, both about not doing work:\n *\n * - A hidden slot returns one frozen array and never renders its child (see\n *   `Slot`). Hiding the footer does not make it cheaper to draw; it makes it\n *   free.\n * - `apply` compares the resolved layout against the last one and returns\n *   whether anything moved. The inputs change on events that fire *constantly* —\n *   an autocomplete opens and closes on keystrokes — so the callers push state\n *   in on every one of those and this decides whether a frame is owed. Recomputing a layout is a few comparisons; re-rendering\n *   because you did not check is a frame.\n */\n\nimport type { Slot } from \"@kolisachint/hoocode-tui\";\nimport { CHROME_DENSITIES, type ChromeDensity } from \"../../core/chrome-density.js\";\n\nexport { CHROME_DENSITIES, type ChromeDensity, isChromeDensity } from \"../../core/chrome-density.js\";\n\n/**\n * Below this many rows the dial starts at `compact` rather than `full`.\n *\n * A starting point, never a correction: it is read once, when nothing is stored\n * yet, so a small terminal opens sensibly and the dial still does exactly what\n * it is told from then on. Re-deciding this on resize would move the layout\n * under someone dragging a pane divider, which is the kind of thing that makes\n * a UI feel like it is arguing with you.\n */\nexport const SMALL_TERMINAL_ROWS = 25;\n\nexport interface ChromeInputs {\n\tdensity: ChromeDensity;\n\t/** The prompt's completion list is open and wants the room. */\n\tautocompleteOpen: boolean;\n}\n\nexport interface ChromeLayout {\n\t/** `full` is every row it has; `line` is the one-row vitals strip. */\n\tfooter: \"full\" | \"line\" | \"hidden\";\n\t/** `summary` is the ledger's header strip alone — the counts, no rows. */\n\ttasks: \"full\" | \"summary\" | \"hidden\";\n}\n\n/**\n * The whole policy, in one place.\n *\n * Read it as: the dial says what you asked for, and the transient input says\n * what is happening. Where they disagree the transient one wins, because it is\n * the one that ends on its own — an autocomplete closes, and the dial's answer\n * comes back without anyone pressing anything.\n */\nexport function resolveChrome({ density, autocompleteOpen }: ChromeInputs): ChromeLayout {\n\t// The completion list is the reason the prompt grew; the footer is the\n\t// nearest thing with rows to give. It comes straight back on dismissal, so\n\t// this can never strand anyone somewhere they have to key their way out of.\n\tconst footer: ChromeLayout[\"footer\"] = autocompleteOpen\n\t\t? \"hidden\"\n\t\t: density === \"full\"\n\t\t\t? \"full\"\n\t\t\t: density === \"compact\"\n\t\t\t\t? \"line\"\n\t\t\t\t: \"hidden\";\n\n\t// `bare` is the stop that means nothing below the prompt, so the ledger goes\n\t// with the footer. `compact` keeps the counts and gives up the rows, which is\n\t// the same trade the dial is already making for the footer one line up — a\n\t// stop where the footer shrinks and the ledger vanishes outright was the odd\n\t// one out, and it made the middle stop feel like a cliff rather than a step.\n\tif (density === \"bare\") return { footer, tasks: \"hidden\" };\n\tif (density === \"compact\") return { footer, tasks: \"summary\" };\n\n\t// `full` means the whole ledger, mid-turn included. It used to fall back to\n\t// the counts while the agent was streaming, on the theory that transcript rows\n\t// are worth more than ledger rows during a turn. That has it backwards: a turn\n\t// is exactly when you want to know which item the model is on, and a count\n\t// cannot say. The dial already has a stop for people who would rather have the\n\t// rows back — it is called `compact`.\n\treturn { footer, tasks: \"full\" };\n}\n\n/** What the controller needs of the footer and the ledger, and nothing more. */\nexport interface ChromeSurfaces {\n\tfooterSlot: Slot;\n\ttasksSlot: Slot;\n\tsetFooterDensity(density: \"full\" | \"line\"): void;\n\tsetTasksDensity(density: \"full\" | \"summary\"): void;\n}\n\n/**\n * Holds the dial, takes the transient inputs, and moves the slots.\n *\n * Every setter returns nothing and instead reports through `changed`, so a\n * caller that pushes state in on a hot path (a keystroke, a stream event) can\n * ask once whether a render is owed.\n */\nexport class ChromeLayoutController {\n\tprivate inputs: ChromeInputs;\n\tprivate applied: ChromeLayout | null = null;\n\n\tconstructor(\n\t\tprivate readonly surfaces: ChromeSurfaces,\n\t\tdensity: ChromeDensity,\n\t) {\n\t\tthis.inputs = { density, autocompleteOpen: false };\n\t}\n\n\tget density(): ChromeDensity {\n\t\treturn this.inputs.density;\n\t}\n\n\t/** The layout currently on screen, for tests and for the hint strip. */\n\tget layout(): ChromeLayout {\n\t\treturn this.applied ?? resolveChrome(this.inputs);\n\t}\n\n\tsetDensity(density: ChromeDensity): boolean {\n\t\tif (this.inputs.density === density) return false;\n\t\tthis.inputs = { ...this.inputs, density };\n\t\treturn this.apply();\n\t}\n\n\t/** Step the dial, wrapping, the way every other dial in the app steps. */\n\tcycleDensity(direction: \"forward\" | \"backward\"): ChromeDensity {\n\t\tconst at = CHROME_DENSITIES.indexOf(this.inputs.density);\n\t\tconst step = direction === \"forward\" ? 1 : -1;\n\t\tconst next = CHROME_DENSITIES[(at + step + CHROME_DENSITIES.length) % CHROME_DENSITIES.length];\n\t\tthis.setDensity(next);\n\t\treturn next;\n\t}\n\n\tsetAutocompleteOpen(open: boolean): boolean {\n\t\tif (this.inputs.autocompleteOpen === open) return false;\n\t\tthis.inputs = { ...this.inputs, autocompleteOpen: open };\n\t\treturn this.apply();\n\t}\n\n\t/** Push the current layout onto the slots; true when anything moved. */\n\tapply(): boolean {\n\t\tconst next = resolveChrome(this.inputs);\n\t\tconst previous = this.applied;\n\t\tif (previous && previous.footer === next.footer && previous.tasks === next.tasks) return false;\n\t\tthis.applied = next;\n\n\t\t// Density before visibility: a slot that is about to be shown should\n\t\t// render at the size it is meant to be, not at the last size it had.\n\t\tif (next.footer !== \"hidden\") this.surfaces.setFooterDensity(next.footer);\n\t\tif (next.tasks !== \"hidden\") this.surfaces.setTasksDensity(next.tasks);\n\t\tthis.surfaces.footerSlot.setVisible(next.footer !== \"hidden\");\n\t\tthis.surfaces.tasksSlot.setVisible(next.tasks !== \"hidden\");\n\t\treturn true;\n\t}\n}\n"]}