@dreb/coding-agent 2.0.7 → 2.2.0
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/dist/core/buddy/buddy-manager.d.ts.map +1 -1
- package/dist/core/buddy/buddy-manager.js +1 -1
- package/dist/core/buddy/buddy-manager.js.map +1 -1
- package/dist/core/daily-cost-tracker.d.ts +34 -0
- package/dist/core/daily-cost-tracker.d.ts.map +1 -0
- package/dist/core/daily-cost-tracker.js +156 -0
- package/dist/core/daily-cost-tracker.js.map +1 -0
- package/dist/core/footer-data-provider.d.ts +6 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +12 -0
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/modes/interactive/components/buddy-component.d.ts.map +1 -1
- package/dist/modes/interactive/components/buddy-component.js +46 -39
- package/dist/modes/interactive/components/buddy-component.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +21 -10
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +3 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/tui.md +1 -0
- package/examples/extensions/custom-footer.ts +1 -0
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer-data-provider.js","sourceRoot":"","sources":["../../src/core/footer-data-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,QAAQ,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,EAAE,UAAU,EAAkB,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,IAAI,CAAC;AAC/E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAQ9C;;;GAGG;AACH,SAAS,YAAY,GAAoB;IACxC,IAAI,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IACxB,OAAO,IAAI,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAClC,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC;gBACJ,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;gBAC/B,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;oBACnB,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;oBACrD,IAAI,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;wBACpC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;wBACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;wBACtC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;4BAAE,OAAO,IAAI,CAAC;wBACvC,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;wBAChD,MAAM,YAAY,GAAG,UAAU,CAAC,aAAa,CAAC;4BAC7C,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;4BAC7D,CAAC,CAAC,MAAM,CAAC;wBACV,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;oBACjD,CAAC;gBACF,CAAC;qBAAM,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;oBAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;oBACvC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;wBAAE,OAAO,IAAI,CAAC;oBACvC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;gBAC1D,CAAC;YACF,CAAC;YAAC,MAAM,CAAC;gBACR,OAAO,IAAI,CAAC;YACb,CAAC;QACF,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QAChC,GAAG,GAAG,MAAM,CAAC;IACd,CAAC;AAAA,CACD;AAED,8FAA8F;AAC9F,SAAS,wBAAwB,CAAC,OAAe,EAAiB;IACjE,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,EAAE,CAAC,qBAAqB,EAAE,cAAc,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE;QACtG,GAAG,EAAE,OAAO;QACZ,QAAQ,EAAE,MAAM;QAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;KACnC,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/D,OAAO,MAAM,IAAI,IAAI,CAAC;AAAA,CACtB;AAED,6GAA6G;AAC7G,SAAS,yBAAyB,CAAC,OAAe,EAA0B;IAC3E,OAAO,IAAI,OAAO,CAAC,CAAC,cAAc,EAAE,EAAE,CAAC;QACtC,QAAQ,CACP,KAAK,EACL,CAAC,qBAAqB,EAAE,cAAc,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,EACrE;YACC,GAAG,EAAE,OAAO;YACZ,QAAQ,EAAE,MAAM;SAChB,EACD,CAAC,KAA+B,EAAE,MAAc,EAAE,EAAE,CAAC;YACpD,IAAI,KAAK,EAAE,CAAC;gBACX,cAAc,CAAC,IAAI,CAAC,CAAC;gBACrB,OAAO;YACR,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;YAC7B,cAAc,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC;QAAA,CAC/B,CACD,CAAC;IAAA,CACF,CAAC,CAAC;AAAA,CACH;AAED;;;GAGG;AACH,MAAM,OAAO,kBAAkB;IACtB,MAAM,CAAU,iBAAiB,GAAG,GAAG,CAAC;IAExC,iBAAiB,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC9C,YAAY,GAA8B,SAAS,CAAC;IACpD,QAAQ,GAAgC,SAAS,CAAC;IAClD,WAAW,GAAqB,IAAI,CAAC;IACrC,eAAe,GAAqB,IAAI,CAAC;IACzC,qBAAqB,GAAG,IAAI,GAAG,EAAc,CAAC;IAC9C,sBAAsB,GAAG,CAAC,CAAC;IAC3B,YAAY,GAAyC,IAAI,CAAC;IAC1D,eAAe,GAAG,KAAK,CAAC;IACxB,cAAc,GAAG,KAAK,CAAC;IACvB,QAAQ,GAAG,KAAK,CAAC;IAEzB,cAAc;QACb,IAAI,CAAC,QAAQ,GAAG,YAAY,EAAE,CAAC;QAC/B,IAAI,CAAC,eAAe,EAAE,CAAC;IAAA,CACvB;IAED,2EAA2E;IAC3E,YAAY,GAAkB;QAC7B,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YACrC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,oBAAoB,EAAE,CAAC;QACjD,CAAC;QACD,OAAO,IAAI,CAAC,YAAY,CAAC;IAAA,CACzB;IAED,wDAAwD;IACxD,oBAAoB,GAAgC;QACnD,OAAO,IAAI,CAAC,iBAAiB,CAAC;IAAA,CAC9B;IAED,qEAAqE;IACrE,cAAc,CAAC,QAAoB,EAAc;QAChD,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACzC,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,qBAAqB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAAA,CACzD;IAED,qCAAqC;IACrC,kBAAkB,CAAC,GAAW,EAAE,IAAwB,EAAQ;QAC/D,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACxB,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACpC,CAAC;aAAM,CAAC;YACP,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACvC,CAAC;IAAA,CACD;IAED,yCAAyC;IACzC,sBAAsB,GAAS;QAC9B,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,CAAC;IAAA,CAC/B;IAED,4EAA4E;IAC5E,yBAAyB,GAAW;QACnC,OAAO,IAAI,CAAC,sBAAsB,CAAC;IAAA,CACnC;IAED,gDAAgD;IAChD,yBAAyB,CAAC,KAAa,EAAQ;QAC9C,IAAI,CAAC,sBAAsB,GAAG,KAAK,CAAC;IAAA,CACpC;IAED,wBAAwB;IACxB,OAAO,GAAS;QACf,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACvB,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAChC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QAC1B,CAAC;QACD,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACzB,CAAC;QACD,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YAC1B,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;YAC7B,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC7B,CAAC;QACD,IAAI,CAAC,qBAAqB,CAAC,KAAK,EAAE,CAAC;IAAA,CACnC;IAEO,kBAAkB,GAAS;QAClC,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,qBAAqB;YAAE,EAAE,EAAE,CAAC;IAAA,CAClD;IAEO,eAAe,GAAS;QAC/B,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC1B,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACvB,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACjC,CAAC;QACD,IAAI,CAAC,YAAY,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC;YACpC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;YACzB,KAAK,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAAA,CAClC,EAAE,kBAAkB,CAAC,iBAAiB,CAAC,CAAC;IAAA,CACzC;IAEO,KAAK,CAAC,qBAAqB,GAAkB;QACpD,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QAC1B,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YAC1B,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;YAC3B,OAAO;QACR,CAAC;QAED,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC5B,IAAI,CAAC;YACJ,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,qBAAqB,EAAE,CAAC;YACtD,IAAI,IAAI,CAAC,QAAQ;gBAAE,OAAO;YAC1B,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS,IAAI,IAAI,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;gBACzE,IAAI,CAAC,YAAY,GAAG,UAAU,CAAC;gBAC/B,IAAI,CAAC,kBAAkB,EAAE,CAAC;gBAC1B,OAAO;YACR,CAAC;YACD,IAAI,CAAC,YAAY,GAAG,UAAU,CAAC;QAChC,CAAC;gBAAS,CAAC;YACV,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;YAC7B,IAAI,IAAI,CAAC,cAAc,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC3C,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC;gBAC5B,IAAI,CAAC,eAAe,EAAE,CAAC;YACxB,CAAC;QACF,CAAC;IAAA,CACD;IAEO,oBAAoB,GAAkB;QAC7C,IAAI,CAAC;YACJ,IAAI,CAAC,IAAI,CAAC,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAChC,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;YACpE,IAAI,OAAO,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,CAAC;gBAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;gBACjC,OAAO,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YACzG,CAAC;YACD,OAAO,UAAU,CAAC;QACnB,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,IAAI,CAAC;QACb,CAAC;IAAA,CACD;IAEO,KAAK,CAAC,qBAAqB,GAA2B;QAC7D,IAAI,CAAC;YACJ,IAAI,CAAC,IAAI,CAAC,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAChC,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;YACpE,IAAI,OAAO,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,CAAC;gBAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;gBACjC,OAAO,MAAM,KAAK,UAAU;oBAC3B,CAAC,CAAC,CAAC,CAAC,MAAM,yBAAyB,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,UAAU,CAAC;oBAC1E,CAAC,CAAC,MAAM,CAAC;YACX,CAAC;YACD,OAAO,UAAU,CAAC;QACnB,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,IAAI,CAAC;QACb,CAAC;IAAA,CACD;IAEO,eAAe,GAAS;QAC/B,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE,OAAO;QAE3B,wDAAwD;QACxD,kFAAkF;QAClF,4DAA4D;QAC5D,IAAI,CAAC;YACJ,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,EAAE,CAAC;gBACnF,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,QAAQ,EAAE,KAAK,MAAM,EAAE,CAAC;oBACjD,IAAI,CAAC,eAAe,EAAE,CAAC;gBACxB,CAAC;YAAA,CACD,CAAC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACR,kCAAkC;QACnC,CAAC;QAED,4EAA4E;QAC5E,6EAA6E;QAC7E,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;QACjE,IAAI,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAC7B,IAAI,CAAC;gBACJ,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC,WAAW,EAAE,GAAG,EAAE,CAAC;oBAC/C,IAAI,CAAC,eAAe,EAAE,CAAC;gBAAA,CACvB,CAAC,CAAC;YACJ,CAAC;YAAC,MAAM,CAAC;gBACR,kCAAkC;YACnC,CAAC;QACF,CAAC;IAAA,CACD;CACD","sourcesContent":["import { type ExecFileException, execFile, spawnSync } from \"child_process\";\nimport { existsSync, type FSWatcher, readFileSync, statSync, watch } from \"fs\";\nimport { dirname, join, resolve } from \"path\";\n\ntype GitPaths = {\n\trepoDir: string;\n\tcommonGitDir: string;\n\theadPath: string;\n};\n\n/**\n * Find git metadata paths by walking up from cwd.\n * Handles both regular git repos (.git is a directory) and worktrees (.git is a file).\n */\nfunction findGitPaths(): GitPaths | null {\n\tlet dir = process.cwd();\n\twhile (true) {\n\t\tconst gitPath = join(dir, \".git\");\n\t\tif (existsSync(gitPath)) {\n\t\t\ttry {\n\t\t\t\tconst stat = statSync(gitPath);\n\t\t\t\tif (stat.isFile()) {\n\t\t\t\t\tconst content = readFileSync(gitPath, \"utf8\").trim();\n\t\t\t\t\tif (content.startsWith(\"gitdir: \")) {\n\t\t\t\t\t\tconst gitDir = resolve(dir, content.slice(8).trim());\n\t\t\t\t\t\tconst headPath = join(gitDir, \"HEAD\");\n\t\t\t\t\t\tif (!existsSync(headPath)) return null;\n\t\t\t\t\t\tconst commonDirPath = join(gitDir, \"commondir\");\n\t\t\t\t\t\tconst commonGitDir = existsSync(commonDirPath)\n\t\t\t\t\t\t\t? resolve(gitDir, readFileSync(commonDirPath, \"utf8\").trim())\n\t\t\t\t\t\t\t: gitDir;\n\t\t\t\t\t\treturn { repoDir: dir, commonGitDir, headPath };\n\t\t\t\t\t}\n\t\t\t\t} else if (stat.isDirectory()) {\n\t\t\t\t\tconst headPath = join(gitPath, \"HEAD\");\n\t\t\t\t\tif (!existsSync(headPath)) return null;\n\t\t\t\t\treturn { repoDir: dir, commonGitDir: gitPath, headPath };\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\t\tconst parent = dirname(dir);\n\t\tif (parent === dir) return null;\n\t\tdir = parent;\n\t}\n}\n\n/** Ask git for the current branch. Returns null on detached HEAD or if git is unavailable. */\nfunction resolveBranchWithGitSync(repoDir: string): string | null {\n\tconst result = spawnSync(\"git\", [\"--no-optional-locks\", \"symbolic-ref\", \"--quiet\", \"--short\", \"HEAD\"], {\n\t\tcwd: repoDir,\n\t\tencoding: \"utf8\",\n\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t});\n\tconst branch = result.status === 0 ? result.stdout.trim() : \"\";\n\treturn branch || null;\n}\n\n/** Ask git for the current branch asynchronously. Returns null on detached HEAD or if git is unavailable. */\nfunction resolveBranchWithGitAsync(repoDir: string): Promise<string | null> {\n\treturn new Promise((resolvePromise) => {\n\t\texecFile(\n\t\t\t\"git\",\n\t\t\t[\"--no-optional-locks\", \"symbolic-ref\", \"--quiet\", \"--short\", \"HEAD\"],\n\t\t\t{\n\t\t\t\tcwd: repoDir,\n\t\t\t\tencoding: \"utf8\",\n\t\t\t},\n\t\t\t(error: ExecFileException | null, stdout: string) => {\n\t\t\t\tif (error) {\n\t\t\t\t\tresolvePromise(null);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst branch = stdout.trim();\n\t\t\t\tresolvePromise(branch || null);\n\t\t\t},\n\t\t);\n\t});\n}\n\n/**\n * Provides git branch and extension statuses - data not otherwise accessible to extensions.\n * Token stats, model info available via ctx.sessionManager and ctx.model.\n */\nexport class FooterDataProvider {\n\tprivate static readonly WATCH_DEBOUNCE_MS = 500;\n\n\tprivate extensionStatuses = new Map<string, string>();\n\tprivate cachedBranch: string | null | undefined = undefined;\n\tprivate gitPaths: GitPaths | null | undefined = undefined;\n\tprivate headWatcher: FSWatcher | null = null;\n\tprivate reftableWatcher: FSWatcher | null = null;\n\tprivate branchChangeCallbacks = new Set<() => void>();\n\tprivate availableProviderCount = 0;\n\tprivate refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate refreshInFlight = false;\n\tprivate refreshPending = false;\n\tprivate disposed = false;\n\n\tconstructor() {\n\t\tthis.gitPaths = findGitPaths();\n\t\tthis.setupGitWatcher();\n\t}\n\n\t/** Current git branch, null if not in repo, \"detached\" if detached HEAD */\n\tgetGitBranch(): string | null {\n\t\tif (this.cachedBranch === undefined) {\n\t\t\tthis.cachedBranch = this.resolveGitBranchSync();\n\t\t}\n\t\treturn this.cachedBranch;\n\t}\n\n\t/** Extension status texts set via ctx.ui.setStatus() */\n\tgetExtensionStatuses(): ReadonlyMap<string, string> {\n\t\treturn this.extensionStatuses;\n\t}\n\n\t/** Subscribe to git branch changes. Returns unsubscribe function. */\n\tonBranchChange(callback: () => void): () => void {\n\t\tthis.branchChangeCallbacks.add(callback);\n\t\treturn () => this.branchChangeCallbacks.delete(callback);\n\t}\n\n\t/** Internal: set extension status */\n\tsetExtensionStatus(key: string, text: string | undefined): void {\n\t\tif (text === undefined) {\n\t\t\tthis.extensionStatuses.delete(key);\n\t\t} else {\n\t\t\tthis.extensionStatuses.set(key, text);\n\t\t}\n\t}\n\n\t/** Internal: clear extension statuses */\n\tclearExtensionStatuses(): void {\n\t\tthis.extensionStatuses.clear();\n\t}\n\n\t/** Number of unique providers with available models (for footer display) */\n\tgetAvailableProviderCount(): number {\n\t\treturn this.availableProviderCount;\n\t}\n\n\t/** Internal: update available provider count */\n\tsetAvailableProviderCount(count: number): void {\n\t\tthis.availableProviderCount = count;\n\t}\n\n\t/** Internal: cleanup */\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t\tif (this.refreshTimer) {\n\t\t\tclearTimeout(this.refreshTimer);\n\t\t\tthis.refreshTimer = null;\n\t\t}\n\t\tif (this.headWatcher) {\n\t\t\tthis.headWatcher.close();\n\t\t\tthis.headWatcher = null;\n\t\t}\n\t\tif (this.reftableWatcher) {\n\t\t\tthis.reftableWatcher.close();\n\t\t\tthis.reftableWatcher = null;\n\t\t}\n\t\tthis.branchChangeCallbacks.clear();\n\t}\n\n\tprivate notifyBranchChange(): void {\n\t\tfor (const cb of this.branchChangeCallbacks) cb();\n\t}\n\n\tprivate scheduleRefresh(): void {\n\t\tif (this.disposed) return;\n\t\tif (this.refreshTimer) {\n\t\t\tclearTimeout(this.refreshTimer);\n\t\t}\n\t\tthis.refreshTimer = setTimeout(() => {\n\t\t\tthis.refreshTimer = null;\n\t\t\tvoid this.refreshGitBranchAsync();\n\t\t}, FooterDataProvider.WATCH_DEBOUNCE_MS);\n\t}\n\n\tprivate async refreshGitBranchAsync(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tif (this.refreshInFlight) {\n\t\t\tthis.refreshPending = true;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.refreshInFlight = true;\n\t\ttry {\n\t\t\tconst nextBranch = await this.resolveGitBranchAsync();\n\t\t\tif (this.disposed) return;\n\t\t\tif (this.cachedBranch !== undefined && this.cachedBranch !== nextBranch) {\n\t\t\t\tthis.cachedBranch = nextBranch;\n\t\t\t\tthis.notifyBranchChange();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.cachedBranch = nextBranch;\n\t\t} finally {\n\t\t\tthis.refreshInFlight = false;\n\t\t\tif (this.refreshPending && !this.disposed) {\n\t\t\t\tthis.refreshPending = false;\n\t\t\t\tthis.scheduleRefresh();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate resolveGitBranchSync(): string | null {\n\t\ttry {\n\t\t\tif (!this.gitPaths) return null;\n\t\t\tconst content = readFileSync(this.gitPaths.headPath, \"utf8\").trim();\n\t\t\tif (content.startsWith(\"ref: refs/heads/\")) {\n\t\t\t\tconst branch = content.slice(16);\n\t\t\t\treturn branch === \".invalid\" ? (resolveBranchWithGitSync(this.gitPaths.repoDir) ?? \"detached\") : branch;\n\t\t\t}\n\t\t\treturn \"detached\";\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate async resolveGitBranchAsync(): Promise<string | null> {\n\t\ttry {\n\t\t\tif (!this.gitPaths) return null;\n\t\t\tconst content = readFileSync(this.gitPaths.headPath, \"utf8\").trim();\n\t\t\tif (content.startsWith(\"ref: refs/heads/\")) {\n\t\t\t\tconst branch = content.slice(16);\n\t\t\t\treturn branch === \".invalid\"\n\t\t\t\t\t? ((await resolveBranchWithGitAsync(this.gitPaths.repoDir)) ?? \"detached\")\n\t\t\t\t\t: branch;\n\t\t\t}\n\t\t\treturn \"detached\";\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate setupGitWatcher(): void {\n\t\tif (!this.gitPaths) return;\n\n\t\t// Watch the directory containing HEAD, not HEAD itself.\n\t\t// Git uses atomic writes (write temp, rename over HEAD), which changes the inode.\n\t\t// fs.watch on a file stops working after the inode changes.\n\t\ttry {\n\t\t\tthis.headWatcher = watch(dirname(this.gitPaths.headPath), (_eventType, filename) => {\n\t\t\t\tif (!filename || filename.toString() === \"HEAD\") {\n\t\t\t\t\tthis.scheduleRefresh();\n\t\t\t\t}\n\t\t\t});\n\t\t} catch {\n\t\t\t// Silently fail if we can't watch\n\t\t}\n\n\t\t// In reftable repos, branch switches update files in the reftable directory\n\t\t// instead of HEAD. Watch it separately so the footer picks up those changes.\n\t\tconst reftableDir = join(this.gitPaths.commonGitDir, \"reftable\");\n\t\tif (existsSync(reftableDir)) {\n\t\t\ttry {\n\t\t\t\tthis.reftableWatcher = watch(reftableDir, () => {\n\t\t\t\t\tthis.scheduleRefresh();\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\t// Silently fail if we can't watch\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Read-only view for extensions - excludes setExtensionStatus, setAvailableProviderCount and dispose */\nexport type ReadonlyFooterDataProvider = Pick<\n\tFooterDataProvider,\n\t\"getGitBranch\" | \"getExtensionStatuses\" | \"getAvailableProviderCount\" | \"onBranchChange\"\n>;\n"]}
|
|
1
|
+
{"version":3,"file":"footer-data-provider.js","sourceRoot":"","sources":["../../src/core/footer-data-provider.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ExecFileException, execFile, spawnSync } from \"child_process\";\nimport { existsSync, type FSWatcher, readFileSync, statSync, watch } from \"fs\";\nimport { dirname, join, resolve } from \"path\";\nimport { DailyCostTracker } from \"./daily-cost-tracker.js\";\n\ntype GitPaths = {\n\trepoDir: string;\n\tcommonGitDir: string;\n\theadPath: string;\n};\n\n/**\n * Find git metadata paths by walking up from cwd.\n * Handles both regular git repos (.git is a directory) and worktrees (.git is a file).\n */\nfunction findGitPaths(): GitPaths | null {\n\tlet dir = process.cwd();\n\twhile (true) {\n\t\tconst gitPath = join(dir, \".git\");\n\t\tif (existsSync(gitPath)) {\n\t\t\ttry {\n\t\t\t\tconst stat = statSync(gitPath);\n\t\t\t\tif (stat.isFile()) {\n\t\t\t\t\tconst content = readFileSync(gitPath, \"utf8\").trim();\n\t\t\t\t\tif (content.startsWith(\"gitdir: \")) {\n\t\t\t\t\t\tconst gitDir = resolve(dir, content.slice(8).trim());\n\t\t\t\t\t\tconst headPath = join(gitDir, \"HEAD\");\n\t\t\t\t\t\tif (!existsSync(headPath)) return null;\n\t\t\t\t\t\tconst commonDirPath = join(gitDir, \"commondir\");\n\t\t\t\t\t\tconst commonGitDir = existsSync(commonDirPath)\n\t\t\t\t\t\t\t? resolve(gitDir, readFileSync(commonDirPath, \"utf8\").trim())\n\t\t\t\t\t\t\t: gitDir;\n\t\t\t\t\t\treturn { repoDir: dir, commonGitDir, headPath };\n\t\t\t\t\t}\n\t\t\t\t} else if (stat.isDirectory()) {\n\t\t\t\t\tconst headPath = join(gitPath, \"HEAD\");\n\t\t\t\t\tif (!existsSync(headPath)) return null;\n\t\t\t\t\treturn { repoDir: dir, commonGitDir: gitPath, headPath };\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\t\tconst parent = dirname(dir);\n\t\tif (parent === dir) return null;\n\t\tdir = parent;\n\t}\n}\n\n/** Ask git for the current branch. Returns null on detached HEAD or if git is unavailable. */\nfunction resolveBranchWithGitSync(repoDir: string): string | null {\n\tconst result = spawnSync(\"git\", [\"--no-optional-locks\", \"symbolic-ref\", \"--quiet\", \"--short\", \"HEAD\"], {\n\t\tcwd: repoDir,\n\t\tencoding: \"utf8\",\n\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t});\n\tconst branch = result.status === 0 ? result.stdout.trim() : \"\";\n\treturn branch || null;\n}\n\n/** Ask git for the current branch asynchronously. Returns null on detached HEAD or if git is unavailable. */\nfunction resolveBranchWithGitAsync(repoDir: string): Promise<string | null> {\n\treturn new Promise((resolvePromise) => {\n\t\texecFile(\n\t\t\t\"git\",\n\t\t\t[\"--no-optional-locks\", \"symbolic-ref\", \"--quiet\", \"--short\", \"HEAD\"],\n\t\t\t{\n\t\t\t\tcwd: repoDir,\n\t\t\t\tencoding: \"utf8\",\n\t\t\t},\n\t\t\t(error: ExecFileException | null, stdout: string) => {\n\t\t\t\tif (error) {\n\t\t\t\t\tresolvePromise(null);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst branch = stdout.trim();\n\t\t\t\tresolvePromise(branch || null);\n\t\t\t},\n\t\t);\n\t});\n}\n\n/**\n * Provides git branch and extension statuses - data not otherwise accessible to extensions.\n * Token stats, model info available via ctx.sessionManager and ctx.model.\n */\nexport class FooterDataProvider {\n\tprivate static readonly WATCH_DEBOUNCE_MS = 500;\n\n\tprivate extensionStatuses = new Map<string, string>();\n\tprivate cachedBranch: string | null | undefined = undefined;\n\tprivate gitPaths: GitPaths | null | undefined = undefined;\n\tprivate headWatcher: FSWatcher | null = null;\n\tprivate reftableWatcher: FSWatcher | null = null;\n\tprivate branchChangeCallbacks = new Set<() => void>();\n\tprivate dailyCostTracker: DailyCostTracker;\n\tprivate availableProviderCount = 0;\n\tprivate refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate refreshInFlight = false;\n\tprivate refreshPending = false;\n\tprivate disposed = false;\n\n\tconstructor() {\n\t\tthis.gitPaths = findGitPaths();\n\t\tthis.setupGitWatcher();\n\t\tthis.dailyCostTracker = new DailyCostTracker();\n\t}\n\n\t/** Current git branch, null if not in repo, \"detached\" if detached HEAD */\n\tgetGitBranch(): string | null {\n\t\tif (this.cachedBranch === undefined) {\n\t\t\tthis.cachedBranch = this.resolveGitBranchSync();\n\t\t}\n\t\treturn this.cachedBranch;\n\t}\n\n\t/** Extension status texts set via ctx.ui.setStatus() */\n\tgetExtensionStatuses(): ReadonlyMap<string, string> {\n\t\treturn this.extensionStatuses;\n\t}\n\n\t/** Subscribe to git branch changes. Returns unsubscribe function. */\n\tonBranchChange(callback: () => void): () => void {\n\t\tthis.branchChangeCallbacks.add(callback);\n\t\treturn () => this.branchChangeCallbacks.delete(callback);\n\t}\n\n\t/** Internal: set extension status */\n\tsetExtensionStatus(key: string, text: string | undefined): void {\n\t\tif (text === undefined) {\n\t\t\tthis.extensionStatuses.delete(key);\n\t\t} else {\n\t\t\tthis.extensionStatuses.set(key, text);\n\t\t}\n\t}\n\n\t/** Internal: clear extension statuses */\n\tclearExtensionStatuses(): void {\n\t\tthis.extensionStatuses.clear();\n\t}\n\n\t/** Cached daily cost total across all sessions. O(1). */\n\tgetDailyCost(): number {\n\t\treturn this.dailyCostTracker.getDailyCost();\n\t}\n\n\t/** Force refresh of the daily cost cache. */\n\tasync refreshDailyCost(): Promise<void> {\n\t\tawait this.dailyCostTracker.refresh();\n\t}\n\n\t/** Number of unique providers with available models (for footer display) */\n\tgetAvailableProviderCount(): number {\n\t\treturn this.availableProviderCount;\n\t}\n\n\t/** Internal: update available provider count */\n\tsetAvailableProviderCount(count: number): void {\n\t\tthis.availableProviderCount = count;\n\t}\n\n\t/** Internal: cleanup */\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t\tif (this.refreshTimer) {\n\t\t\tclearTimeout(this.refreshTimer);\n\t\t\tthis.refreshTimer = null;\n\t\t}\n\t\tthis.dailyCostTracker.dispose();\n\t\tif (this.headWatcher) {\n\t\t\tthis.headWatcher.close();\n\t\t\tthis.headWatcher = null;\n\t\t}\n\t\tif (this.reftableWatcher) {\n\t\t\tthis.reftableWatcher.close();\n\t\t\tthis.reftableWatcher = null;\n\t\t}\n\t\tthis.branchChangeCallbacks.clear();\n\t}\n\n\tprivate notifyBranchChange(): void {\n\t\tfor (const cb of this.branchChangeCallbacks) cb();\n\t}\n\n\tprivate scheduleRefresh(): void {\n\t\tif (this.disposed) return;\n\t\tif (this.refreshTimer) {\n\t\t\tclearTimeout(this.refreshTimer);\n\t\t}\n\t\tthis.refreshTimer = setTimeout(() => {\n\t\t\tthis.refreshTimer = null;\n\t\t\tvoid this.refreshGitBranchAsync();\n\t\t}, FooterDataProvider.WATCH_DEBOUNCE_MS);\n\t}\n\n\tprivate async refreshGitBranchAsync(): Promise<void> {\n\t\tif (this.disposed) return;\n\t\tif (this.refreshInFlight) {\n\t\t\tthis.refreshPending = true;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.refreshInFlight = true;\n\t\ttry {\n\t\t\tconst nextBranch = await this.resolveGitBranchAsync();\n\t\t\tif (this.disposed) return;\n\t\t\tif (this.cachedBranch !== undefined && this.cachedBranch !== nextBranch) {\n\t\t\t\tthis.cachedBranch = nextBranch;\n\t\t\t\tthis.notifyBranchChange();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.cachedBranch = nextBranch;\n\t\t} finally {\n\t\t\tthis.refreshInFlight = false;\n\t\t\tif (this.refreshPending && !this.disposed) {\n\t\t\t\tthis.refreshPending = false;\n\t\t\t\tthis.scheduleRefresh();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate resolveGitBranchSync(): string | null {\n\t\ttry {\n\t\t\tif (!this.gitPaths) return null;\n\t\t\tconst content = readFileSync(this.gitPaths.headPath, \"utf8\").trim();\n\t\t\tif (content.startsWith(\"ref: refs/heads/\")) {\n\t\t\t\tconst branch = content.slice(16);\n\t\t\t\treturn branch === \".invalid\" ? (resolveBranchWithGitSync(this.gitPaths.repoDir) ?? \"detached\") : branch;\n\t\t\t}\n\t\t\treturn \"detached\";\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate async resolveGitBranchAsync(): Promise<string | null> {\n\t\ttry {\n\t\t\tif (!this.gitPaths) return null;\n\t\t\tconst content = readFileSync(this.gitPaths.headPath, \"utf8\").trim();\n\t\t\tif (content.startsWith(\"ref: refs/heads/\")) {\n\t\t\t\tconst branch = content.slice(16);\n\t\t\t\treturn branch === \".invalid\"\n\t\t\t\t\t? ((await resolveBranchWithGitAsync(this.gitPaths.repoDir)) ?? \"detached\")\n\t\t\t\t\t: branch;\n\t\t\t}\n\t\t\treturn \"detached\";\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate setupGitWatcher(): void {\n\t\tif (!this.gitPaths) return;\n\n\t\t// Watch the directory containing HEAD, not HEAD itself.\n\t\t// Git uses atomic writes (write temp, rename over HEAD), which changes the inode.\n\t\t// fs.watch on a file stops working after the inode changes.\n\t\ttry {\n\t\t\tthis.headWatcher = watch(dirname(this.gitPaths.headPath), (_eventType, filename) => {\n\t\t\t\tif (!filename || filename.toString() === \"HEAD\") {\n\t\t\t\t\tthis.scheduleRefresh();\n\t\t\t\t}\n\t\t\t});\n\t\t} catch {\n\t\t\t// Silently fail if we can't watch\n\t\t}\n\n\t\t// In reftable repos, branch switches update files in the reftable directory\n\t\t// instead of HEAD. Watch it separately so the footer picks up those changes.\n\t\tconst reftableDir = join(this.gitPaths.commonGitDir, \"reftable\");\n\t\tif (existsSync(reftableDir)) {\n\t\t\ttry {\n\t\t\t\tthis.reftableWatcher = watch(reftableDir, () => {\n\t\t\t\t\tthis.scheduleRefresh();\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\t// Silently fail if we can't watch\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Read-only view for extensions - excludes setExtensionStatus, setAvailableProviderCount and dispose */\nexport type ReadonlyFooterDataProvider = Pick<\n\tFooterDataProvider,\n\t\"getGitBranch\" | \"getExtensionStatuses\" | \"getAvailableProviderCount\" | \"onBranchChange\" | \"getDailyCost\"\n>;\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buddy-component.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/buddy-component.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAuB,GAAG,EAAE,MAAM,WAAW,CAAC;AAIrE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAUrE,qBAAa,cAAe,YAAW,SAAS;IAC/C,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,QAAQ,CAA+C;IAG/D,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,WAAW,CAAK;IAGxB,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,aAAa,CAA8C;IAGnE,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAA8C;IAChE,OAAO,CAAC,MAAM,CAAmE;IAGjF,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;IAG/C,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,aAAa,CAAM;IAC3B,OAAO,CAAC,aAAa,CAAK;IAE1B,YAAY,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,UAAU,EAKrC;IAED,6CAA6C;IAC7C,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAMnC;IAED,qCAAqC;IACrC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAW7B;IAED,4BAA4B;IAC5B,GAAG,IAAI,IAAI,CAsBV;IAED,4DAA4D;IAC5D,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAKjC;IAED,kCAAkC;IAClC,YAAY,IAAI,IAAI,CAKnB;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAc9B;IAED,OAAO,IAAI,IAAI,CAWd;IAMD,OAAO,CAAC,UAAU;IAiElB,OAAO,CAAC,YAAY;IAgCpB,OAAO,CAAC,cAAc;IAuBtB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,kBAAkB;IAwC1B,6EAA6E;IAC7E,OAAO,CAAC,oBAAoB;IAY5B,qEAAqE;IACrE,OAAO,CAAC,kBAAkB;CA0B1B","sourcesContent":["/**\n * BuddyComponent — TUI component rendering the buddy companion.\n *\n * Renders below the editor with:\n * - 3-frame idle animation cycling at 500ms\n * - Speech bubbles for reactions and name-call responses\n * - Pet hearts animation (2.5s)\n * - Narrow terminal fallback (<100 cols)\n * - Stat display and rarity badge\n */\n\nimport type { Component, MarkdownTheme as MT, TUI } from \"@dreb/tui\";\nimport { truncateToWidth, visibleWidth } from \"@dreb/tui\";\nimport { marked } from \"marked\";\nimport { applyEyes, getSpeciesFrames, getSpeciesWidth } from \"../../../core/buddy/buddy-species.js\";\nimport type { BuddyState } from \"../../../core/buddy/buddy-types.js\";\nimport { Rarity, Stat } from \"../../../core/buddy/buddy-types.js\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\n\nconst IDLE_INTERVAL_MS = 500;\nconst SPEECH_BUBBLE_DURATION_MS = 10000;\nconst PET_DURATION_MS = 2500;\nconst HEART_CHARS = [\"❤️\", \"💕\", \"💖\", \"💗\", \"✨\"];\nconst NARROW_THRESHOLD = 100;\n\nexport class BuddyComponent implements Component {\n\tprivate ui: TUI;\n\tprivate state: BuddyState;\n\tprivate interval: ReturnType<typeof setInterval> | null = null;\n\n\t// Animation state\n\tprivate currentFrame = 0;\n\tprivate totalFrames = 3;\n\n\t// Speech bubble\n\tprivate speechText: string | null = null;\n\tprivate speechTimeout: ReturnType<typeof setTimeout> | null = null;\n\n\t// Pet animation\n\tprivate isPetting = false;\n\tprivate petTimeout: ReturnType<typeof setTimeout> | null = null;\n\tprivate hearts: Array<{ x: number; y: number; char: string; life: number }> = [];\n\n\t// Thinking indicator\n\tprivate thinkingLabel: string | null = null;\n\tprivate thinkingDots = 0;\n\tprivate static readonly THINKING_DOT_COUNT = 4; // cycles 0,1,2,3 → \".\", \"..\", \"...\", \"....\"\n\n\t// Cached render\n\tprivate cachedLines: string[] = [];\n\tprivate cachedWidth = 0;\n\tprivate cachedVersion = -1;\n\tprivate renderVersion = 0;\n\n\tconstructor(ui: TUI, state: BuddyState) {\n\t\tthis.ui = ui;\n\t\tthis.state = state;\n\t\tthis.totalFrames = getSpeciesFrames(state.species).length;\n\t\tthis.startAnimation();\n\t}\n\n\t/** Update buddy state (e.g. after reroll) */\n\tupdateState(state: BuddyState): void {\n\t\tthis.state = state;\n\t\tthis.totalFrames = getSpeciesFrames(state.species).length;\n\t\tthis.currentFrame = 0;\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Show a speech bubble with text */\n\tshowSpeech(text: string): void {\n\t\tthis.speechText = text;\n\t\tif (this.speechTimeout) clearTimeout(this.speechTimeout);\n\t\tthis.speechTimeout = setTimeout(() => {\n\t\t\tthis.speechText = null;\n\t\t\tthis.speechTimeout = null;\n\t\t\tthis.bumpVersion();\n\t\t\tthis.ui.requestRender();\n\t\t}, SPEECH_BUBBLE_DURATION_MS);\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Trigger pet animation */\n\tpet(): void {\n\t\tthis.isPetting = true;\n\t\t// Spawn hearts\n\t\tconst spriteWidth = getSpeciesWidth(this.state.species);\n\t\tfor (let i = 0; i < 5; i++) {\n\t\t\tthis.hearts.push({\n\t\t\t\tx: Math.floor(Math.random() * spriteWidth),\n\t\t\t\ty: -1 - Math.floor(Math.random() * 3),\n\t\t\t\tchar: HEART_CHARS[Math.floor(Math.random() * HEART_CHARS.length)],\n\t\t\t\tlife: 5 + Math.floor(Math.random() * 5),\n\t\t\t});\n\t\t}\n\t\tif (this.petTimeout) clearTimeout(this.petTimeout);\n\t\tthis.petTimeout = setTimeout(() => {\n\t\t\tthis.isPetting = false;\n\t\t\tthis.hearts = [];\n\t\t\tthis.petTimeout = null;\n\t\t\tthis.bumpVersion();\n\t\t\tthis.ui.requestRender();\n\t\t}, PET_DURATION_MS);\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Show a pulsing thinking indicator with optional label */\n\tshowThinking(label?: string): void {\n\t\tthis.thinkingLabel = label ?? \"thinking\";\n\t\tthis.thinkingDots = 0;\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Hide the thinking indicator */\n\thideThinking(): void {\n\t\tthis.thinkingLabel = null;\n\t\tthis.thinkingDots = 0;\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedWidth = 0;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (width === this.cachedWidth && this.cachedVersion === this.renderVersion) {\n\t\t\treturn this.cachedLines;\n\t\t}\n\n\t\tif (width < NARROW_THRESHOLD) {\n\t\t\tthis.cachedLines = this.renderNarrow(width);\n\t\t} else {\n\t\t\tthis.cachedLines = this.renderFull(width);\n\t\t}\n\n\t\tthis.cachedWidth = width;\n\t\tthis.cachedVersion = this.renderVersion;\n\t\treturn this.cachedLines;\n\t}\n\n\tdispose(): void {\n\t\tthis.stopAnimation();\n\t\tthis.thinkingLabel = null;\n\t\tif (this.speechTimeout) {\n\t\t\tclearTimeout(this.speechTimeout);\n\t\t\tthis.speechTimeout = null;\n\t\t}\n\t\tif (this.petTimeout) {\n\t\t\tclearTimeout(this.petTimeout);\n\t\t\tthis.petTimeout = null;\n\t\t}\n\t}\n\n\t// =============================================================================\n\t// Full rendering (wide terminal)\n\t// =============================================================================\n\n\tprivate renderFull(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst frames = getSpeciesFrames(this.state.species);\n\t\tconst frame = frames[this.currentFrame % this.totalFrames];\n\t\tconst rendered = applyEyes(frame, this.state.eyeStyle);\n\n\t\t// Hat line (if present)\n\t\tif (this.state.hat) {\n\t\t\tconst hatPad = Math.max(0, Math.floor((getSpeciesWidth(this.state.species) - 1) / 2) - 1);\n\t\t\tlines.push(\" \".repeat(hatPad) + this.state.hat);\n\t\t}\n\n\t\t// Sprite lines\n\t\tconst spriteWidth = getSpeciesWidth(this.state.species);\n\t\tlines.push(...rendered);\n\n\t\t// Heart animation line above sprite\n\t\tif (this.isPetting && this.hearts.length > 0) {\n\t\t\tconst heartLine = \" \".repeat(spriteWidth);\n\t\t\tconst chars = heartLine.split(\"\");\n\t\t\tfor (const heart of this.hearts) {\n\t\t\t\tconst x = Math.min(heart.x, chars.length - 2);\n\t\t\t\tif (x >= 0 && heart.life > 0) {\n\t\t\t\t\tconst hChar = heart.char;\n\t\t\t\t\tchars.splice(x, hChar.length, ...hChar.split(\"\"));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Insert heart line before sprite\n\t\t\tlines.splice(this.state.hat ? 1 : 0, 0, chars.join(\"\"));\n\t\t}\n\n\t\t// Name + rarity line\n\t\tconst shinyMark = this.state.shiny ? \" ✨\" : \"\";\n\t\tconst rarityColor = this.rarityColor(this.state.rarity);\n\t\tconst nameLine = ` ${theme.bold(this.state.name)}${shinyMark} ${theme.fg(rarityColor, `[${this.state.rarity}]`)} ${theme.fg(\"muted\", this.state.species)}`;\n\t\tlines.push(nameLine);\n\n\t\t// Stats line\n\t\tconst statParts = (Object.values(Stat) as Stat[]).map((s) => {\n\t\t\tconst val = this.state.stats[s];\n\t\t\tconst bar = this.statBar(val);\n\t\t\treturn `${theme.fg(\"muted\", s[0])}:${bar}`;\n\t\t});\n\t\tlines.push(` ${statParts.join(\" \")}`);\n\n\t\t// Thinking indicator\n\t\tif (this.thinkingLabel !== null) {\n\t\t\tconst dots = \".\".repeat((this.thinkingDots % BuddyComponent.THINKING_DOT_COUNT) + 1);\n\t\t\tconst label = this.thinkingLabel || \"thinking\";\n\t\t\tlines.push(` ${theme.fg(\"muted\", `💭 ${label}${dots}`)}`);\n\t\t}\n\n\t\t// Speech bubble (beside or below sprite)\n\t\tif (this.speechText) {\n\t\t\tconst bubbleLines = this.formatSpeechBubble(this.speechText, Math.min(width - 4, 60));\n\t\t\tlines.push(...bubbleLines);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\t// =============================================================================\n\t// Narrow rendering (< 100 cols)\n\t// =============================================================================\n\n\tprivate renderNarrow(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// Single-line face\n\t\tconst eyes = this.state.eyeStyle;\n\t\tconst mouth = this.isPetting ? \"♥\" : \">\";\n\t\tconst face = `${this.state.hat}${eyes}${mouth}${eyes}`;\n\n\t\t// Name + truncated quip\n\t\tconst shinyMark = this.state.shiny ? \"✨\" : \"\";\n\t\tconst name = `${theme.bold(this.state.name)}${shinyMark}`;\n\n\t\tif (this.speechText) {\n\t\t\tconst maxQuip = Math.max(10, width - face.length - this.state.name.length - 6);\n\t\t\tconst styledQuip = this.renderInlineMarkdown(this.speechText);\n\t\t\tconst quip = visibleWidth(styledQuip) > maxQuip ? `${truncateToWidth(styledQuip, maxQuip - 1)}…` : styledQuip;\n\t\t\tlines.push(` ${face} ${name}: ${theme.fg(\"accent\", quip)}`);\n\t\t} else if (this.thinkingLabel !== null) {\n\t\t\tconst dots = \".\".repeat((this.thinkingDots % BuddyComponent.THINKING_DOT_COUNT) + 1);\n\t\t\tconst label = this.thinkingLabel || \"thinking\";\n\t\t\tlines.push(` ${face} ${name} ${theme.fg(\"muted\", `💭 ${label}${dots}`)}`);\n\t\t} else {\n\t\t\tlines.push(` ${face} ${name}`);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\t// =============================================================================\n\t// Animation\n\t// =============================================================================\n\n\tprivate startAnimation(): void {\n\t\tthis.interval = setInterval(() => {\n\t\t\tthis.currentFrame = (this.currentFrame + 1) % this.totalFrames;\n\n\t\t\t// Tick thinking dots\n\t\t\tif (this.thinkingLabel !== null) {\n\t\t\t\tthis.thinkingDots = (this.thinkingDots + 1) % BuddyComponent.THINKING_DOT_COUNT;\n\t\t\t}\n\n\t\t\t// Tick hearts\n\t\t\tif (this.isPetting) {\n\t\t\t\tfor (const heart of this.hearts) {\n\t\t\t\t\theart.life--;\n\t\t\t\t\theart.y++;\n\t\t\t\t}\n\t\t\t\tthis.hearts = this.hearts.filter((h) => h.life > 0);\n\t\t\t}\n\n\t\t\tthis.bumpVersion();\n\t\t\tthis.ui.requestRender();\n\t\t}, IDLE_INTERVAL_MS);\n\t}\n\n\tprivate stopAnimation(): void {\n\t\tif (this.interval) {\n\t\t\tclearInterval(this.interval);\n\t\t\tthis.interval = null;\n\t\t}\n\t}\n\n\tprivate bumpVersion(): void {\n\t\tthis.renderVersion++;\n\t}\n\n\t// =============================================================================\n\t// Formatting helpers\n\t// =============================================================================\n\n\tprivate statBar(value: number): string {\n\t\tconst filled = Math.round(value / 10);\n\t\tconst empty = 10 - filled;\n\t\tconst bar = \"█\".repeat(filled) + \"░\".repeat(empty);\n\t\tconst color: \"success\" | \"warning\" | \"error\" = value >= 70 ? \"success\" : value >= 40 ? \"warning\" : \"error\";\n\t\treturn theme.fg(color, bar);\n\t}\n\n\tprivate rarityColor(rarity: Rarity): \"muted\" | \"success\" | \"accent\" | \"warning\" | \"error\" {\n\t\tswitch (rarity) {\n\t\t\tcase Rarity.COMMON:\n\t\t\t\treturn \"muted\";\n\t\t\tcase Rarity.UNCOMMON:\n\t\t\t\treturn \"success\";\n\t\t\tcase Rarity.RARE:\n\t\t\t\treturn \"accent\";\n\t\t\tcase Rarity.EPIC:\n\t\t\t\treturn \"warning\";\n\t\t\tcase Rarity.LEGENDARY:\n\t\t\t\treturn \"error\";\n\t\t}\n\t}\n\n\tprivate formatSpeechBubble(text: string, maxWidth: number): string[] {\n\t\tif (!text.trim()) return [];\n\n\t\t// Render inline markdown (bold, italic, code) to styled text with ANSI codes\n\t\tconst styledText = this.renderInlineMarkdown(text);\n\n\t\t// Word-wrap using visible width (ANSI-aware)\n\t\tconst lines: string[] = [];\n\t\tconst words = styledText.split(\" \");\n\t\tlet currentLine = \"\";\n\n\t\tfor (const word of words) {\n\t\t\tconst test = currentLine ? `${currentLine} ${word}` : word;\n\t\t\tif (visibleWidth(test) > maxWidth) {\n\t\t\t\tif (currentLine) lines.push(currentLine);\n\t\t\t\tcurrentLine = word;\n\t\t\t} else {\n\t\t\t\tcurrentLine = test;\n\t\t\t}\n\t\t}\n\t\tif (currentLine) lines.push(currentLine);\n\n\t\t// Wrap in bubble border using visible width for measurement\n\t\tconst maxLineWidth = Math.max(...lines.map((l) => visibleWidth(l)));\n\t\tconst bubbleWidth = Math.max(6, Math.min(maxLineWidth + 4, maxWidth + 4));\n\t\tconst top = `╭${\"─\".repeat(bubbleWidth - 2)}╮`;\n\t\tconst bottom = `╰${\"─\".repeat(bubbleWidth - 2)}╯`;\n\n\t\tconst result: string[] = [];\n\t\tresult.push(` ${theme.fg(\"accent\", top)}`);\n\t\tfor (const line of lines) {\n\t\t\tconst padding = Math.max(0, bubbleWidth - 4 - visibleWidth(line));\n\t\t\tconst padded = line + \" \".repeat(padding);\n\t\t\tresult.push(` ${theme.fg(\"accent\", \"│\")} ${padded} ${theme.fg(\"accent\", \"│\")}`);\n\t\t}\n\t\tresult.push(` ${theme.fg(\"accent\", bottom)}`);\n\n\t\treturn result;\n\t}\n\n\t/** Render inline markdown tokens (bold, italic, code) to ANSI-styled text */\n\tprivate renderInlineMarkdown(text: string): string {\n\t\tconst mdTheme = getMarkdownTheme();\n\t\tconst tokens = marked.lexer(text);\n\n\t\t// Flatten: we expect a paragraph containing inline tokens\n\t\tconst inlineTokens = tokens.flatMap((t: any) =>\n\t\t\tt.type === \"paragraph\" ? (t.tokens ?? []) : t.type === \"text\" ? t : [],\n\t\t);\n\n\t\treturn this.renderInlineTokens(inlineTokens, mdTheme);\n\t}\n\n\t/** Recursively render marked inline tokens to ANSI-styled strings */\n\tprivate renderInlineTokens(tokens: any[], mdTheme: MT): string {\n\t\tlet result = \"\";\n\t\tfor (const token of tokens) {\n\t\t\tswitch (token.type) {\n\t\t\t\tcase \"text\":\n\t\t\t\t\tresult += token.text ?? this.renderInlineTokens(token.tokens ?? [], mdTheme);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"strong\":\n\t\t\t\t\tresult += mdTheme.bold(this.renderInlineTokens(token.tokens ?? [], mdTheme));\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"em\":\n\t\t\t\t\tresult += mdTheme.italic(this.renderInlineTokens(token.tokens ?? [], mdTheme));\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"codespan\":\n\t\t\t\t\tresult += mdTheme.code(token.text);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"escape\":\n\t\t\t\t\tresult += token.text;\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tresult += token.text ?? token.raw ?? \"\";\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"buddy-component.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/buddy-component.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAuB,GAAG,EAAE,MAAM,WAAW,CAAC;AAIrE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAYrE,qBAAa,cAAe,YAAW,SAAS;IAC/C,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,QAAQ,CAA+C;IAG/D,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,WAAW,CAAK;IAGxB,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,aAAa,CAA8C;IAGnE,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAA8C;IAChE,OAAO,CAAC,MAAM,CAAmE;IAGjF,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;IAG/C,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,aAAa,CAAM;IAC3B,OAAO,CAAC,aAAa,CAAK;IAE1B,YAAY,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,UAAU,EAKrC;IAED,6CAA6C;IAC7C,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAMnC;IAED,qCAAqC;IACrC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAW7B;IAED,4BAA4B;IAC5B,GAAG,IAAI,IAAI,CAsBV;IAED,4DAA4D;IAC5D,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAKjC;IAED,kCAAkC;IAClC,YAAY,IAAI,IAAI,CAKnB;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAc9B;IAED,OAAO,IAAI,IAAI,CAWd;IAMD,OAAO,CAAC,UAAU;IA0ElB,OAAO,CAAC,YAAY;IAgCpB,OAAO,CAAC,cAAc;IAuBtB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,kBAAkB;IAsC1B,6EAA6E;IAC7E,OAAO,CAAC,oBAAoB;IAY5B,qEAAqE;IACrE,OAAO,CAAC,kBAAkB;CA0B1B","sourcesContent":["/**\n * BuddyComponent — TUI component rendering the buddy companion.\n *\n * Renders below the editor with:\n * - 3-frame idle animation cycling at 500ms\n * - Speech bubbles for reactions and name-call responses\n * - Pet hearts animation (2.5s)\n * - Narrow terminal fallback (<100 cols)\n * - Stat display and rarity badge\n */\n\nimport type { Component, MarkdownTheme as MT, TUI } from \"@dreb/tui\";\nimport { joinColumns, truncateToWidth, visibleWidth, wrapTextWithAnsi } from \"@dreb/tui\";\nimport { marked } from \"marked\";\nimport { applyEyes, getSpeciesFrames, getSpeciesWidth } from \"../../../core/buddy/buddy-species.js\";\nimport type { BuddyState } from \"../../../core/buddy/buddy-types.js\";\nimport { Rarity, Stat } from \"../../../core/buddy/buddy-types.js\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\n\nconst IDLE_INTERVAL_MS = 500;\nconst SPEECH_BUBBLE_DURATION_MS = 10000;\nconst PET_DURATION_MS = 2500;\nconst HEART_CHARS = [\"❤️\", \"💕\", \"💖\", \"💗\", \"✨\"];\nconst NARROW_THRESHOLD = 100;\nconst SPEECH_MAX_CONTENT_LINES = 3;\nconst SIDE_PANEL_GAP = 2;\n\nexport class BuddyComponent implements Component {\n\tprivate ui: TUI;\n\tprivate state: BuddyState;\n\tprivate interval: ReturnType<typeof setInterval> | null = null;\n\n\t// Animation state\n\tprivate currentFrame = 0;\n\tprivate totalFrames = 3;\n\n\t// Speech bubble\n\tprivate speechText: string | null = null;\n\tprivate speechTimeout: ReturnType<typeof setTimeout> | null = null;\n\n\t// Pet animation\n\tprivate isPetting = false;\n\tprivate petTimeout: ReturnType<typeof setTimeout> | null = null;\n\tprivate hearts: Array<{ x: number; y: number; char: string; life: number }> = [];\n\n\t// Thinking indicator\n\tprivate thinkingLabel: string | null = null;\n\tprivate thinkingDots = 0;\n\tprivate static readonly THINKING_DOT_COUNT = 4; // cycles 0,1,2,3 → \".\", \"..\", \"...\", \"....\"\n\n\t// Cached render\n\tprivate cachedLines: string[] = [];\n\tprivate cachedWidth = 0;\n\tprivate cachedVersion = -1;\n\tprivate renderVersion = 0;\n\n\tconstructor(ui: TUI, state: BuddyState) {\n\t\tthis.ui = ui;\n\t\tthis.state = state;\n\t\tthis.totalFrames = getSpeciesFrames(state.species).length;\n\t\tthis.startAnimation();\n\t}\n\n\t/** Update buddy state (e.g. after reroll) */\n\tupdateState(state: BuddyState): void {\n\t\tthis.state = state;\n\t\tthis.totalFrames = getSpeciesFrames(state.species).length;\n\t\tthis.currentFrame = 0;\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Show a speech bubble with text */\n\tshowSpeech(text: string): void {\n\t\tthis.speechText = text;\n\t\tif (this.speechTimeout) clearTimeout(this.speechTimeout);\n\t\tthis.speechTimeout = setTimeout(() => {\n\t\t\tthis.speechText = null;\n\t\t\tthis.speechTimeout = null;\n\t\t\tthis.bumpVersion();\n\t\t\tthis.ui.requestRender();\n\t\t}, SPEECH_BUBBLE_DURATION_MS);\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Trigger pet animation */\n\tpet(): void {\n\t\tthis.isPetting = true;\n\t\t// Spawn hearts\n\t\tconst spriteWidth = getSpeciesWidth(this.state.species);\n\t\tfor (let i = 0; i < 5; i++) {\n\t\t\tthis.hearts.push({\n\t\t\t\tx: Math.floor(Math.random() * spriteWidth),\n\t\t\t\ty: -1 - Math.floor(Math.random() * 3),\n\t\t\t\tchar: HEART_CHARS[Math.floor(Math.random() * HEART_CHARS.length)],\n\t\t\t\tlife: 5 + Math.floor(Math.random() * 5),\n\t\t\t});\n\t\t}\n\t\tif (this.petTimeout) clearTimeout(this.petTimeout);\n\t\tthis.petTimeout = setTimeout(() => {\n\t\t\tthis.isPetting = false;\n\t\t\tthis.hearts = [];\n\t\t\tthis.petTimeout = null;\n\t\t\tthis.bumpVersion();\n\t\t\tthis.ui.requestRender();\n\t\t}, PET_DURATION_MS);\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Show a pulsing thinking indicator with optional label */\n\tshowThinking(label?: string): void {\n\t\tthis.thinkingLabel = label ?? \"thinking\";\n\t\tthis.thinkingDots = 0;\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Hide the thinking indicator */\n\thideThinking(): void {\n\t\tthis.thinkingLabel = null;\n\t\tthis.thinkingDots = 0;\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedWidth = 0;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (width === this.cachedWidth && this.cachedVersion === this.renderVersion) {\n\t\t\treturn this.cachedLines;\n\t\t}\n\n\t\tif (width < NARROW_THRESHOLD) {\n\t\t\tthis.cachedLines = this.renderNarrow(width);\n\t\t} else {\n\t\t\tthis.cachedLines = this.renderFull(width);\n\t\t}\n\n\t\tthis.cachedWidth = width;\n\t\tthis.cachedVersion = this.renderVersion;\n\t\treturn this.cachedLines;\n\t}\n\n\tdispose(): void {\n\t\tthis.stopAnimation();\n\t\tthis.thinkingLabel = null;\n\t\tif (this.speechTimeout) {\n\t\t\tclearTimeout(this.speechTimeout);\n\t\t\tthis.speechTimeout = null;\n\t\t}\n\t\tif (this.petTimeout) {\n\t\t\tclearTimeout(this.petTimeout);\n\t\t\tthis.petTimeout = null;\n\t\t}\n\t}\n\n\t// =============================================================================\n\t// Full rendering (wide terminal)\n\t// =============================================================================\n\n\tprivate renderFull(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst frames = getSpeciesFrames(this.state.species);\n\t\tconst frame = frames[this.currentFrame % this.totalFrames];\n\t\tconst rendered = applyEyes(frame, this.state.eyeStyle);\n\t\tconst spriteWidth = getSpeciesWidth(this.state.species);\n\n\t\t// Build LEFT block: hat + heart animation + sprite lines\n\t\tconst leftLines: string[] = [];\n\n\t\t// Hat line\n\t\tif (this.state.hat) {\n\t\t\tconst hatPad = Math.max(0, Math.floor((spriteWidth - 1) / 2) - 1);\n\t\t\tleftLines.push(\" \".repeat(hatPad) + this.state.hat);\n\t\t}\n\n\t\t// Heart animation line (above sprite, inserted at top)\n\t\tif (this.isPetting && this.hearts.length > 0) {\n\t\t\tconst heartLine = \" \".repeat(spriteWidth);\n\t\t\tconst chars = heartLine.split(\"\");\n\t\t\tfor (const heart of this.hearts) {\n\t\t\t\tconst x = Math.min(heart.x, chars.length - 2);\n\t\t\t\tif (x >= 0 && heart.life > 0) {\n\t\t\t\t\tconst hChar = heart.char;\n\t\t\t\t\tchars.splice(x, hChar.length, ...hChar.split(\"\"));\n\t\t\t\t}\n\t\t\t}\n\t\t\tleftLines.push(chars.join(\"\"));\n\t\t}\n\n\t\t// Sprite lines\n\t\tleftLines.push(...rendered);\n\n\t\t// Build RIGHT block: speech bubble or thinking indicator\n\t\tlet rightLines: string[] = [];\n\t\tif (this.speechText) {\n\t\t\tconst availableWidth = width - spriteWidth - SIDE_PANEL_GAP - 5; // 5 for leading space + bubble borders + padding\n\t\t\tconst bubbleMaxWidth = Math.max(20, availableWidth);\n\t\t\trightLines = this.formatSpeechBubble(this.speechText, bubbleMaxWidth);\n\t\t} else if (this.thinkingLabel !== null) {\n\t\t\tconst dots = \".\".repeat((this.thinkingDots % BuddyComponent.THINKING_DOT_COUNT) + 1);\n\t\t\tconst label = this.thinkingLabel || \"thinking\";\n\t\t\trightLines = [` ${theme.fg(\"muted\", `💭 ${label}${dots}`)}`];\n\t\t}\n\n\t\t// Merge left and right side-by-side\n\t\tif (rightLines.length > 0) {\n\t\t\tconst merged = joinColumns(leftLines, rightLines, SIDE_PANEL_GAP, width);\n\t\t\tlines.push(...merged);\n\t\t} else {\n\t\t\tlines.push(...leftLines);\n\t\t}\n\n\t\t// Name + rarity line (full width, below the sprite+panel area)\n\t\tconst shinyMark = this.state.shiny ? \" ✨\" : \"\";\n\t\tconst rarityColor = this.rarityColor(this.state.rarity);\n\t\tconst nameLine = ` ${theme.bold(this.state.name)}${shinyMark} ${theme.fg(rarityColor, `[${this.state.rarity}]`)} ${theme.fg(\"muted\", this.state.species)}`;\n\t\tlines.push(nameLine);\n\n\t\t// Stats line (full width)\n\t\tconst statParts = (Object.values(Stat) as Stat[]).map((s) => {\n\t\t\tconst val = this.state.stats[s];\n\t\t\tconst bar = this.statBar(val);\n\t\t\treturn `${theme.fg(\"muted\", s[0])}:${bar}`;\n\t\t});\n\t\tlines.push(` ${statParts.join(\" \")}`);\n\n\t\treturn lines;\n\t}\n\n\t// =============================================================================\n\t// Narrow rendering (< 100 cols)\n\t// =============================================================================\n\n\tprivate renderNarrow(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// Single-line face\n\t\tconst eyes = this.state.eyeStyle;\n\t\tconst mouth = this.isPetting ? \"♥\" : \">\";\n\t\tconst face = `${this.state.hat}${eyes}${mouth}${eyes}`;\n\n\t\t// Name + truncated quip\n\t\tconst shinyMark = this.state.shiny ? \"✨\" : \"\";\n\t\tconst name = `${theme.bold(this.state.name)}${shinyMark}`;\n\n\t\tif (this.speechText) {\n\t\t\tconst maxQuip = Math.max(10, width - face.length - this.state.name.length - 6);\n\t\t\tconst styledQuip = this.renderInlineMarkdown(this.speechText);\n\t\t\tconst quip = visibleWidth(styledQuip) > maxQuip ? `${truncateToWidth(styledQuip, maxQuip - 1)}…` : styledQuip;\n\t\t\tlines.push(` ${face} ${name}: ${theme.fg(\"accent\", quip)}`);\n\t\t} else if (this.thinkingLabel !== null) {\n\t\t\tconst dots = \".\".repeat((this.thinkingDots % BuddyComponent.THINKING_DOT_COUNT) + 1);\n\t\t\tconst label = this.thinkingLabel || \"thinking\";\n\t\t\tlines.push(` ${face} ${name} ${theme.fg(\"muted\", `💭 ${label}${dots}`)}`);\n\t\t} else {\n\t\t\tlines.push(` ${face} ${name}`);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\t// =============================================================================\n\t// Animation\n\t// =============================================================================\n\n\tprivate startAnimation(): void {\n\t\tthis.interval = setInterval(() => {\n\t\t\tthis.currentFrame = (this.currentFrame + 1) % this.totalFrames;\n\n\t\t\t// Tick thinking dots\n\t\t\tif (this.thinkingLabel !== null) {\n\t\t\t\tthis.thinkingDots = (this.thinkingDots + 1) % BuddyComponent.THINKING_DOT_COUNT;\n\t\t\t}\n\n\t\t\t// Tick hearts\n\t\t\tif (this.isPetting) {\n\t\t\t\tfor (const heart of this.hearts) {\n\t\t\t\t\theart.life--;\n\t\t\t\t\theart.y++;\n\t\t\t\t}\n\t\t\t\tthis.hearts = this.hearts.filter((h) => h.life > 0);\n\t\t\t}\n\n\t\t\tthis.bumpVersion();\n\t\t\tthis.ui.requestRender();\n\t\t}, IDLE_INTERVAL_MS);\n\t}\n\n\tprivate stopAnimation(): void {\n\t\tif (this.interval) {\n\t\t\tclearInterval(this.interval);\n\t\t\tthis.interval = null;\n\t\t}\n\t}\n\n\tprivate bumpVersion(): void {\n\t\tthis.renderVersion++;\n\t}\n\n\t// =============================================================================\n\t// Formatting helpers\n\t// =============================================================================\n\n\tprivate statBar(value: number): string {\n\t\tconst filled = Math.round(value / 10);\n\t\tconst empty = 10 - filled;\n\t\tconst bar = \"█\".repeat(filled) + \"░\".repeat(empty);\n\t\tconst color: \"success\" | \"warning\" | \"error\" = value >= 70 ? \"success\" : value >= 40 ? \"warning\" : \"error\";\n\t\treturn theme.fg(color, bar);\n\t}\n\n\tprivate rarityColor(rarity: Rarity): \"muted\" | \"success\" | \"accent\" | \"warning\" | \"error\" {\n\t\tswitch (rarity) {\n\t\t\tcase Rarity.COMMON:\n\t\t\t\treturn \"muted\";\n\t\t\tcase Rarity.UNCOMMON:\n\t\t\t\treturn \"success\";\n\t\t\tcase Rarity.RARE:\n\t\t\t\treturn \"accent\";\n\t\t\tcase Rarity.EPIC:\n\t\t\t\treturn \"warning\";\n\t\t\tcase Rarity.LEGENDARY:\n\t\t\t\treturn \"error\";\n\t\t}\n\t}\n\n\tprivate formatSpeechBubble(text: string, maxWidth: number): string[] {\n\t\tif (!text.trim()) return [];\n\n\t\t// Render inline markdown (bold, italic, code) to styled text with ANSI codes\n\t\tconst styledText = this.renderInlineMarkdown(text);\n\n\t\t// Word-wrap using visible width (ANSI-aware, handles long words)\n\t\tconst lines = wrapTextWithAnsi(styledText, maxWidth);\n\n\t\t// Enforce hard line cap\n\t\tif (lines.length > SPEECH_MAX_CONTENT_LINES) {\n\t\t\tconst kept = lines.slice(0, SPEECH_MAX_CONTENT_LINES - 1);\n\t\t\tconst lastLine = lines[SPEECH_MAX_CONTENT_LINES - 1];\n\t\t\t// Truncate the last kept line with ellipsis\n\t\t\tconst truncated = truncateToWidth(lastLine, maxWidth - 1, \"…\");\n\t\t\tkept.push(truncated);\n\t\t\tlines.length = 0;\n\t\t\tlines.push(...kept);\n\t\t}\n\n\t\t// Wrap in bubble border using visible width for measurement\n\t\tconst maxLineWidth = Math.max(...lines.map((l) => visibleWidth(l)));\n\t\tconst bubbleWidth = Math.max(6, Math.min(maxLineWidth + 4, maxWidth + 4));\n\t\tconst top = `╭${\"─\".repeat(bubbleWidth - 2)}╮`;\n\t\tconst bottom = `╰${\"─\".repeat(bubbleWidth - 2)}╯`;\n\n\t\tconst result: string[] = [];\n\t\tresult.push(` ${theme.fg(\"accent\", top)}`);\n\t\tfor (const line of lines) {\n\t\t\tconst padding = Math.max(0, bubbleWidth - 4 - visibleWidth(line));\n\t\t\tconst padded = line + \" \".repeat(padding);\n\t\t\tresult.push(` ${theme.fg(\"accent\", \"│\")} ${padded} ${theme.fg(\"accent\", \"│\")}`);\n\t\t}\n\t\tresult.push(` ${theme.fg(\"accent\", bottom)}`);\n\n\t\treturn result;\n\t}\n\n\t/** Render inline markdown tokens (bold, italic, code) to ANSI-styled text */\n\tprivate renderInlineMarkdown(text: string): string {\n\t\tconst mdTheme = getMarkdownTheme();\n\t\tconst tokens = marked.lexer(text);\n\n\t\t// Flatten: we expect a paragraph containing inline tokens\n\t\tconst inlineTokens = tokens.flatMap((t: any) =>\n\t\t\tt.type === \"paragraph\" ? (t.tokens ?? []) : t.type === \"text\" ? t : [],\n\t\t);\n\n\t\treturn this.renderInlineTokens(inlineTokens, mdTheme);\n\t}\n\n\t/** Recursively render marked inline tokens to ANSI-styled strings */\n\tprivate renderInlineTokens(tokens: any[], mdTheme: MT): string {\n\t\tlet result = \"\";\n\t\tfor (const token of tokens) {\n\t\t\tswitch (token.type) {\n\t\t\t\tcase \"text\":\n\t\t\t\t\tresult += token.text ?? this.renderInlineTokens(token.tokens ?? [], mdTheme);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"strong\":\n\t\t\t\t\tresult += mdTheme.bold(this.renderInlineTokens(token.tokens ?? [], mdTheme));\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"em\":\n\t\t\t\t\tresult += mdTheme.italic(this.renderInlineTokens(token.tokens ?? [], mdTheme));\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"codespan\":\n\t\t\t\t\tresult += mdTheme.code(token.text);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"escape\":\n\t\t\t\t\tresult += token.text;\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tresult += token.text ?? token.raw ?? \"\";\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n}\n"]}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Narrow terminal fallback (<100 cols)
|
|
9
9
|
* - Stat display and rarity badge
|
|
10
10
|
*/
|
|
11
|
-
import { truncateToWidth, visibleWidth } from "@dreb/tui";
|
|
11
|
+
import { joinColumns, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@dreb/tui";
|
|
12
12
|
import { marked } from "marked";
|
|
13
13
|
import { applyEyes, getSpeciesFrames, getSpeciesWidth } from "../../../core/buddy/buddy-species.js";
|
|
14
14
|
import { Rarity, Stat } from "../../../core/buddy/buddy-types.js";
|
|
@@ -18,6 +18,8 @@ const SPEECH_BUBBLE_DURATION_MS = 10000;
|
|
|
18
18
|
const PET_DURATION_MS = 2500;
|
|
19
19
|
const HEART_CHARS = ["❤️", "💕", "💖", "💗", "✨"];
|
|
20
20
|
const NARROW_THRESHOLD = 100;
|
|
21
|
+
const SPEECH_MAX_CONTENT_LINES = 3;
|
|
22
|
+
const SIDE_PANEL_GAP = 2;
|
|
21
23
|
export class BuddyComponent {
|
|
22
24
|
ui;
|
|
23
25
|
state;
|
|
@@ -145,15 +147,15 @@ export class BuddyComponent {
|
|
|
145
147
|
const frames = getSpeciesFrames(this.state.species);
|
|
146
148
|
const frame = frames[this.currentFrame % this.totalFrames];
|
|
147
149
|
const rendered = applyEyes(frame, this.state.eyeStyle);
|
|
148
|
-
|
|
150
|
+
const spriteWidth = getSpeciesWidth(this.state.species);
|
|
151
|
+
// Build LEFT block: hat + heart animation + sprite lines
|
|
152
|
+
const leftLines = [];
|
|
153
|
+
// Hat line
|
|
149
154
|
if (this.state.hat) {
|
|
150
|
-
const hatPad = Math.max(0, Math.floor((
|
|
151
|
-
|
|
155
|
+
const hatPad = Math.max(0, Math.floor((spriteWidth - 1) / 2) - 1);
|
|
156
|
+
leftLines.push(" ".repeat(hatPad) + this.state.hat);
|
|
152
157
|
}
|
|
153
|
-
//
|
|
154
|
-
const spriteWidth = getSpeciesWidth(this.state.species);
|
|
155
|
-
lines.push(...rendered);
|
|
156
|
-
// Heart animation line above sprite
|
|
158
|
+
// Heart animation line (above sprite, inserted at top)
|
|
157
159
|
if (this.isPetting && this.hearts.length > 0) {
|
|
158
160
|
const heartLine = " ".repeat(spriteWidth);
|
|
159
161
|
const chars = heartLine.split("");
|
|
@@ -164,32 +166,42 @@ export class BuddyComponent {
|
|
|
164
166
|
chars.splice(x, hChar.length, ...hChar.split(""));
|
|
165
167
|
}
|
|
166
168
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
leftLines.push(chars.join(""));
|
|
170
|
+
}
|
|
171
|
+
// Sprite lines
|
|
172
|
+
leftLines.push(...rendered);
|
|
173
|
+
// Build RIGHT block: speech bubble or thinking indicator
|
|
174
|
+
let rightLines = [];
|
|
175
|
+
if (this.speechText) {
|
|
176
|
+
const availableWidth = width - spriteWidth - SIDE_PANEL_GAP - 5; // 5 for leading space + bubble borders + padding
|
|
177
|
+
const bubbleMaxWidth = Math.max(20, availableWidth);
|
|
178
|
+
rightLines = this.formatSpeechBubble(this.speechText, bubbleMaxWidth);
|
|
179
|
+
}
|
|
180
|
+
else if (this.thinkingLabel !== null) {
|
|
181
|
+
const dots = ".".repeat((this.thinkingDots % BuddyComponent.THINKING_DOT_COUNT) + 1);
|
|
182
|
+
const label = this.thinkingLabel || "thinking";
|
|
183
|
+
rightLines = [` ${theme.fg("muted", `💭 ${label}${dots}`)}`];
|
|
184
|
+
}
|
|
185
|
+
// Merge left and right side-by-side
|
|
186
|
+
if (rightLines.length > 0) {
|
|
187
|
+
const merged = joinColumns(leftLines, rightLines, SIDE_PANEL_GAP, width);
|
|
188
|
+
lines.push(...merged);
|
|
169
189
|
}
|
|
170
|
-
|
|
190
|
+
else {
|
|
191
|
+
lines.push(...leftLines);
|
|
192
|
+
}
|
|
193
|
+
// Name + rarity line (full width, below the sprite+panel area)
|
|
171
194
|
const shinyMark = this.state.shiny ? " ✨" : "";
|
|
172
195
|
const rarityColor = this.rarityColor(this.state.rarity);
|
|
173
196
|
const nameLine = ` ${theme.bold(this.state.name)}${shinyMark} ${theme.fg(rarityColor, `[${this.state.rarity}]`)} ${theme.fg("muted", this.state.species)}`;
|
|
174
197
|
lines.push(nameLine);
|
|
175
|
-
// Stats line
|
|
198
|
+
// Stats line (full width)
|
|
176
199
|
const statParts = Object.values(Stat).map((s) => {
|
|
177
200
|
const val = this.state.stats[s];
|
|
178
201
|
const bar = this.statBar(val);
|
|
179
202
|
return `${theme.fg("muted", s[0])}:${bar}`;
|
|
180
203
|
});
|
|
181
204
|
lines.push(` ${statParts.join(" ")}`);
|
|
182
|
-
// Thinking indicator
|
|
183
|
-
if (this.thinkingLabel !== null) {
|
|
184
|
-
const dots = ".".repeat((this.thinkingDots % BuddyComponent.THINKING_DOT_COUNT) + 1);
|
|
185
|
-
const label = this.thinkingLabel || "thinking";
|
|
186
|
-
lines.push(` ${theme.fg("muted", `💭 ${label}${dots}`)}`);
|
|
187
|
-
}
|
|
188
|
-
// Speech bubble (beside or below sprite)
|
|
189
|
-
if (this.speechText) {
|
|
190
|
-
const bubbleLines = this.formatSpeechBubble(this.speechText, Math.min(width - 4, 60));
|
|
191
|
-
lines.push(...bubbleLines);
|
|
192
|
-
}
|
|
193
205
|
return lines;
|
|
194
206
|
}
|
|
195
207
|
// =============================================================================
|
|
@@ -280,23 +292,18 @@ export class BuddyComponent {
|
|
|
280
292
|
return [];
|
|
281
293
|
// Render inline markdown (bold, italic, code) to styled text with ANSI codes
|
|
282
294
|
const styledText = this.renderInlineMarkdown(text);
|
|
283
|
-
// Word-wrap using visible width (ANSI-aware)
|
|
284
|
-
const lines =
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
else {
|
|
295
|
-
currentLine = test;
|
|
296
|
-
}
|
|
295
|
+
// Word-wrap using visible width (ANSI-aware, handles long words)
|
|
296
|
+
const lines = wrapTextWithAnsi(styledText, maxWidth);
|
|
297
|
+
// Enforce hard line cap
|
|
298
|
+
if (lines.length > SPEECH_MAX_CONTENT_LINES) {
|
|
299
|
+
const kept = lines.slice(0, SPEECH_MAX_CONTENT_LINES - 1);
|
|
300
|
+
const lastLine = lines[SPEECH_MAX_CONTENT_LINES - 1];
|
|
301
|
+
// Truncate the last kept line with ellipsis
|
|
302
|
+
const truncated = truncateToWidth(lastLine, maxWidth - 1, "…");
|
|
303
|
+
kept.push(truncated);
|
|
304
|
+
lines.length = 0;
|
|
305
|
+
lines.push(...kept);
|
|
297
306
|
}
|
|
298
|
-
if (currentLine)
|
|
299
|
-
lines.push(currentLine);
|
|
300
307
|
// Wrap in bubble border using visible width for measurement
|
|
301
308
|
const maxLineWidth = Math.max(...lines.map((l) => visibleWidth(l)));
|
|
302
309
|
const bubbleWidth = Math.max(6, Math.min(maxLineWidth + 4, maxWidth + 4));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buddy-component.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/buddy-component.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AAEpG,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE5D,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,yBAAyB,GAAG,KAAK,CAAC;AACxC,MAAM,eAAe,GAAG,IAAI,CAAC;AAC7B,MAAM,WAAW,GAAG,CAAC,QAAI,EAAE,MAAG,EAAE,MAAG,EAAE,MAAG,EAAE,KAAG,CAAC,CAAC;AAC/C,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B,MAAM,OAAO,cAAc;IAClB,EAAE,CAAM;IACR,KAAK,CAAa;IAClB,QAAQ,GAA0C,IAAI,CAAC;IAE/D,kBAAkB;IACV,YAAY,GAAG,CAAC,CAAC;IACjB,WAAW,GAAG,CAAC,CAAC;IAExB,gBAAgB;IACR,UAAU,GAAkB,IAAI,CAAC;IACjC,aAAa,GAAyC,IAAI,CAAC;IAEnE,gBAAgB;IACR,SAAS,GAAG,KAAK,CAAC;IAClB,UAAU,GAAyC,IAAI,CAAC;IACxD,MAAM,GAAgE,EAAE,CAAC;IAEjF,qBAAqB;IACb,aAAa,GAAkB,IAAI,CAAC;IACpC,YAAY,GAAG,CAAC,CAAC;IACjB,MAAM,CAAU,kBAAkB,GAAG,CAAC,CAAC,CAAC,8CAA4C;IAE5F,gBAAgB;IACR,WAAW,GAAa,EAAE,CAAC;IAC3B,WAAW,GAAG,CAAC,CAAC;IAChB,aAAa,GAAG,CAAC,CAAC,CAAC;IACnB,aAAa,GAAG,CAAC,CAAC;IAE1B,YAAY,EAAO,EAAE,KAAiB,EAAE;QACvC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,WAAW,GAAG,gBAAgB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;QAC1D,IAAI,CAAC,cAAc,EAAE,CAAC;IAAA,CACtB;IAED,6CAA6C;IAC7C,WAAW,CAAC,KAAiB,EAAQ;QACpC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,WAAW,GAAG,gBAAgB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;QAC1D,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,IAAI,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC;IAAA,CACxB;IAED,qCAAqC;IACrC,UAAU,CAAC,IAAY,EAAQ;QAC9B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,IAAI,IAAI,CAAC,aAAa;YAAE,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACzD,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC;YACrC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YACvB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;YAC1B,IAAI,CAAC,WAAW,EAAE,CAAC;YACnB,IAAI,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC;QAAA,CACxB,EAAE,yBAAyB,CAAC,CAAC;QAC9B,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,IAAI,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC;IAAA,CACxB;IAED,4BAA4B;IAC5B,GAAG,GAAS;QACX,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,eAAe;QACf,MAAM,WAAW,GAAG,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACxD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;gBAChB,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,WAAW,CAAC;gBAC1C,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;gBACrC,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;gBACjE,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;aACvC,CAAC,CAAC;QACJ,CAAC;QACD,IAAI,IAAI,CAAC,UAAU;YAAE,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACnD,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC;YAClC,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;YACvB,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC;YACjB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YACvB,IAAI,CAAC,WAAW,EAAE,CAAC;YACnB,IAAI,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC;QAAA,CACxB,EAAE,eAAe,CAAC,CAAC;QACpB,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,IAAI,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC;IAAA,CACxB;IAED,4DAA4D;IAC5D,YAAY,CAAC,KAAc,EAAQ;QAClC,IAAI,CAAC,aAAa,GAAG,KAAK,IAAI,UAAU,CAAC;QACzC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,IAAI,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC;IAAA,CACxB;IAED,kCAAkC;IAClC,YAAY,GAAS;QACpB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC1B,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,IAAI,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC;IAAA,CACxB;IAED,UAAU,GAAS;QAClB,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;IAAA,CACrB;IAED,MAAM,CAAC,KAAa,EAAY;QAC/B,IAAI,KAAK,KAAK,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,aAAa,KAAK,IAAI,CAAC,aAAa,EAAE,CAAC;YAC7E,OAAO,IAAI,CAAC,WAAW,CAAC;QACzB,CAAC;QAED,IAAI,KAAK,GAAG,gBAAgB,EAAE,CAAC;YAC9B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QAC7C,CAAC;aAAM,CAAC;YACP,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAC3C,CAAC;QAED,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;QACzB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC;QACxC,OAAO,IAAI,CAAC,WAAW,CAAC;IAAA,CACxB;IAED,OAAO,GAAS;QACf,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC1B,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACxB,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YACjC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC3B,CAAC;QACD,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC9B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACxB,CAAC;IAAA,CACD;IAED,gFAAgF;IAChF,iCAAiC;IACjC,gFAAgF;IAExE,UAAU,CAAC,KAAa,EAAY;QAC3C,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACpD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;QAC3D,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAEvD,wBAAwB;QACxB,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;YACpB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1F,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACjD,CAAC;QAED,eAAe;QACf,MAAM,WAAW,GAAG,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACxD,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC;QAExB,oCAAoC;QACpC,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9C,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YAC1C,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAClC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBACjC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBAC9C,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;oBAC9B,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC;oBACzB,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;gBACnD,CAAC;YACF,CAAC;YACD,kCAAkC;YAClC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QACzD,CAAC;QAED,qBAAqB;QACrB,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,MAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACxD,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,SAAS,IAAI,KAAK,CAAC,EAAE,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3J,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAErB,aAAa;QACb,MAAM,SAAS,GAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAChC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAC9B,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;QAAA,CAC3C,CAAC,CAAC;QACH,KAAK,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAEtC,qBAAqB;QACrB,IAAI,IAAI,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;YACjC,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,YAAY,GAAG,cAAc,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC;YACrF,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,IAAI,UAAU,CAAC;YAC/C,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,QAAK,KAAK,GAAG,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;QAC1D,CAAC;QAED,yCAAyC;QACzC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,MAAM,WAAW,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;YACtF,KAAK,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,CAAC;QAC5B,CAAC;QAED,OAAO,KAAK,CAAC;IAAA,CACb;IAED,gFAAgF;IAChF,gCAAgC;IAChC,gFAAgF;IAExE,YAAY,CAAC,KAAa,EAAY;QAC7C,MAAM,KAAK,GAAa,EAAE,CAAC;QAE3B,mBAAmB;QACnB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAG,CAAC,CAAC,CAAC,GAAG,CAAC;QACzC,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,EAAE,CAAC;QAEvD,wBAAwB;QACxB,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,SAAS,EAAE,CAAC;QAE1D,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC/E,MAAM,UAAU,GAAG,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC9D,MAAM,IAAI,GAAG,YAAY,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,UAAU,EAAE,OAAO,GAAG,CAAC,CAAC,KAAG,CAAC,CAAC,CAAC,UAAU,CAAC;YAC9G,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;QAC7D,CAAC;aAAM,IAAI,IAAI,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;YACxC,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,YAAY,GAAG,cAAc,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC;YACrF,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,IAAI,UAAU,CAAC;YAC/C,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,QAAK,KAAK,GAAG,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;QAC1E,CAAC;aAAM,CAAC;YACP,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC;QAChC,CAAC;QAED,OAAO,KAAK,CAAC;IAAA,CACb;IAED,gFAAgF;IAChF,YAAY;IACZ,gFAAgF;IAExE,cAAc,GAAS;QAC9B,IAAI,CAAC,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;YACjC,IAAI,CAAC,YAAY,GAAG,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC;YAE/D,qBAAqB;YACrB,IAAI,IAAI,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;gBACjC,IAAI,CAAC,YAAY,GAAG,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,GAAG,cAAc,CAAC,kBAAkB,CAAC;YACjF,CAAC;YAED,cAAc;YACd,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBACpB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;oBACjC,KAAK,CAAC,IAAI,EAAE,CAAC;oBACb,KAAK,CAAC,CAAC,EAAE,CAAC;gBACX,CAAC;gBACD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;YACrD,CAAC;YAED,IAAI,CAAC,WAAW,EAAE,CAAC;YACnB,IAAI,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC;QAAA,CACxB,EAAE,gBAAgB,CAAC,CAAC;IAAA,CACrB;IAEO,aAAa,GAAS;QAC7B,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC7B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACtB,CAAC;IAAA,CACD;IAEO,WAAW,GAAS;QAC3B,IAAI,CAAC,aAAa,EAAE,CAAC;IAAA,CACrB;IAED,gFAAgF;IAChF,qBAAqB;IACrB,gFAAgF;IAExE,OAAO,CAAC,KAAa,EAAU;QACtC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;QACtC,MAAM,KAAK,GAAG,EAAE,GAAG,MAAM,CAAC;QAC1B,MAAM,GAAG,GAAG,KAAG,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,KAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACnD,MAAM,KAAK,GAAoC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;QAC3G,OAAO,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAAA,CAC5B;IAEO,WAAW,CAAC,MAAc,EAAwD;QACzF,QAAQ,MAAM,EAAE,CAAC;YAChB,KAAK,MAAM,CAAC,MAAM;gBACjB,OAAO,OAAO,CAAC;YAChB,KAAK,MAAM,CAAC,QAAQ;gBACnB,OAAO,SAAS,CAAC;YAClB,KAAK,MAAM,CAAC,IAAI;gBACf,OAAO,QAAQ,CAAC;YACjB,KAAK,MAAM,CAAC,IAAI;gBACf,OAAO,SAAS,CAAC;YAClB,KAAK,MAAM,CAAC,SAAS;gBACpB,OAAO,OAAO,CAAC;QACjB,CAAC;IAAA,CACD;IAEO,kBAAkB,CAAC,IAAY,EAAE,QAAgB,EAAY;QACpE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,OAAO,EAAE,CAAC;QAE5B,6EAA6E;QAC7E,MAAM,UAAU,GAAG,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;QAEnD,6CAA6C;QAC7C,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,WAAW,GAAG,EAAE,CAAC;QAErB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YAC3D,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,QAAQ,EAAE,CAAC;gBACnC,IAAI,WAAW;oBAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;gBACzC,WAAW,GAAG,IAAI,CAAC;YACpB,CAAC;iBAAM,CAAC;gBACP,WAAW,GAAG,IAAI,CAAC;YACpB,CAAC;QACF,CAAC;QACD,IAAI,WAAW;YAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAEzC,4DAA4D;QAC5D,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACpE,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY,GAAG,CAAC,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC;QAC1E,MAAM,GAAG,GAAG,MAAI,KAAG,CAAC,MAAM,CAAC,WAAW,GAAG,CAAC,CAAC,KAAG,CAAC;QAC/C,MAAM,MAAM,GAAG,MAAI,KAAG,CAAC,MAAM,CAAC,WAAW,GAAG,CAAC,CAAC,KAAG,CAAC;QAElD,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC3C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,GAAG,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;YAClE,MAAM,MAAM,GAAG,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC1C,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAG,CAAC,IAAI,MAAM,IAAI,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAG,CAAC,EAAE,CAAC,CAAC;QACjF,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QAE9C,OAAO,MAAM,CAAC;IAAA,CACd;IAED,6EAA6E;IACrE,oBAAoB,CAAC,IAAY,EAAU;QAClD,MAAM,OAAO,GAAG,gBAAgB,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAElC,0DAA0D;QAC1D,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAM,EAAE,EAAE,CAC9C,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CACtE,CAAC;QAEF,OAAO,IAAI,CAAC,kBAAkB,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IAAA,CACtD;IAED,qEAAqE;IAC7D,kBAAkB,CAAC,MAAa,EAAE,OAAW,EAAU;QAC9D,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC5B,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;gBACpB,KAAK,MAAM;oBACV,MAAM,IAAI,KAAK,CAAC,IAAI,IAAI,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;oBAC7E,MAAM;gBACP,KAAK,QAAQ;oBACZ,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;oBAC7E,MAAM;gBACP,KAAK,IAAI;oBACR,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;oBAC/E,MAAM;gBACP,KAAK,UAAU;oBACd,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBACnC,MAAM;gBACP,KAAK,QAAQ;oBACZ,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC;oBACrB,MAAM;gBACP;oBACC,MAAM,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,GAAG,IAAI,EAAE,CAAC;oBACxC,MAAM;YACR,CAAC;QACF,CAAC;QACD,OAAO,MAAM,CAAC;IAAA,CACd;CACD","sourcesContent":["/**\n * BuddyComponent — TUI component rendering the buddy companion.\n *\n * Renders below the editor with:\n * - 3-frame idle animation cycling at 500ms\n * - Speech bubbles for reactions and name-call responses\n * - Pet hearts animation (2.5s)\n * - Narrow terminal fallback (<100 cols)\n * - Stat display and rarity badge\n */\n\nimport type { Component, MarkdownTheme as MT, TUI } from \"@dreb/tui\";\nimport { truncateToWidth, visibleWidth } from \"@dreb/tui\";\nimport { marked } from \"marked\";\nimport { applyEyes, getSpeciesFrames, getSpeciesWidth } from \"../../../core/buddy/buddy-species.js\";\nimport type { BuddyState } from \"../../../core/buddy/buddy-types.js\";\nimport { Rarity, Stat } from \"../../../core/buddy/buddy-types.js\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\n\nconst IDLE_INTERVAL_MS = 500;\nconst SPEECH_BUBBLE_DURATION_MS = 10000;\nconst PET_DURATION_MS = 2500;\nconst HEART_CHARS = [\"❤️\", \"💕\", \"💖\", \"💗\", \"✨\"];\nconst NARROW_THRESHOLD = 100;\n\nexport class BuddyComponent implements Component {\n\tprivate ui: TUI;\n\tprivate state: BuddyState;\n\tprivate interval: ReturnType<typeof setInterval> | null = null;\n\n\t// Animation state\n\tprivate currentFrame = 0;\n\tprivate totalFrames = 3;\n\n\t// Speech bubble\n\tprivate speechText: string | null = null;\n\tprivate speechTimeout: ReturnType<typeof setTimeout> | null = null;\n\n\t// Pet animation\n\tprivate isPetting = false;\n\tprivate petTimeout: ReturnType<typeof setTimeout> | null = null;\n\tprivate hearts: Array<{ x: number; y: number; char: string; life: number }> = [];\n\n\t// Thinking indicator\n\tprivate thinkingLabel: string | null = null;\n\tprivate thinkingDots = 0;\n\tprivate static readonly THINKING_DOT_COUNT = 4; // cycles 0,1,2,3 → \".\", \"..\", \"...\", \"....\"\n\n\t// Cached render\n\tprivate cachedLines: string[] = [];\n\tprivate cachedWidth = 0;\n\tprivate cachedVersion = -1;\n\tprivate renderVersion = 0;\n\n\tconstructor(ui: TUI, state: BuddyState) {\n\t\tthis.ui = ui;\n\t\tthis.state = state;\n\t\tthis.totalFrames = getSpeciesFrames(state.species).length;\n\t\tthis.startAnimation();\n\t}\n\n\t/** Update buddy state (e.g. after reroll) */\n\tupdateState(state: BuddyState): void {\n\t\tthis.state = state;\n\t\tthis.totalFrames = getSpeciesFrames(state.species).length;\n\t\tthis.currentFrame = 0;\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Show a speech bubble with text */\n\tshowSpeech(text: string): void {\n\t\tthis.speechText = text;\n\t\tif (this.speechTimeout) clearTimeout(this.speechTimeout);\n\t\tthis.speechTimeout = setTimeout(() => {\n\t\t\tthis.speechText = null;\n\t\t\tthis.speechTimeout = null;\n\t\t\tthis.bumpVersion();\n\t\t\tthis.ui.requestRender();\n\t\t}, SPEECH_BUBBLE_DURATION_MS);\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Trigger pet animation */\n\tpet(): void {\n\t\tthis.isPetting = true;\n\t\t// Spawn hearts\n\t\tconst spriteWidth = getSpeciesWidth(this.state.species);\n\t\tfor (let i = 0; i < 5; i++) {\n\t\t\tthis.hearts.push({\n\t\t\t\tx: Math.floor(Math.random() * spriteWidth),\n\t\t\t\ty: -1 - Math.floor(Math.random() * 3),\n\t\t\t\tchar: HEART_CHARS[Math.floor(Math.random() * HEART_CHARS.length)],\n\t\t\t\tlife: 5 + Math.floor(Math.random() * 5),\n\t\t\t});\n\t\t}\n\t\tif (this.petTimeout) clearTimeout(this.petTimeout);\n\t\tthis.petTimeout = setTimeout(() => {\n\t\t\tthis.isPetting = false;\n\t\t\tthis.hearts = [];\n\t\t\tthis.petTimeout = null;\n\t\t\tthis.bumpVersion();\n\t\t\tthis.ui.requestRender();\n\t\t}, PET_DURATION_MS);\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Show a pulsing thinking indicator with optional label */\n\tshowThinking(label?: string): void {\n\t\tthis.thinkingLabel = label ?? \"thinking\";\n\t\tthis.thinkingDots = 0;\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Hide the thinking indicator */\n\thideThinking(): void {\n\t\tthis.thinkingLabel = null;\n\t\tthis.thinkingDots = 0;\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedWidth = 0;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (width === this.cachedWidth && this.cachedVersion === this.renderVersion) {\n\t\t\treturn this.cachedLines;\n\t\t}\n\n\t\tif (width < NARROW_THRESHOLD) {\n\t\t\tthis.cachedLines = this.renderNarrow(width);\n\t\t} else {\n\t\t\tthis.cachedLines = this.renderFull(width);\n\t\t}\n\n\t\tthis.cachedWidth = width;\n\t\tthis.cachedVersion = this.renderVersion;\n\t\treturn this.cachedLines;\n\t}\n\n\tdispose(): void {\n\t\tthis.stopAnimation();\n\t\tthis.thinkingLabel = null;\n\t\tif (this.speechTimeout) {\n\t\t\tclearTimeout(this.speechTimeout);\n\t\t\tthis.speechTimeout = null;\n\t\t}\n\t\tif (this.petTimeout) {\n\t\t\tclearTimeout(this.petTimeout);\n\t\t\tthis.petTimeout = null;\n\t\t}\n\t}\n\n\t// =============================================================================\n\t// Full rendering (wide terminal)\n\t// =============================================================================\n\n\tprivate renderFull(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst frames = getSpeciesFrames(this.state.species);\n\t\tconst frame = frames[this.currentFrame % this.totalFrames];\n\t\tconst rendered = applyEyes(frame, this.state.eyeStyle);\n\n\t\t// Hat line (if present)\n\t\tif (this.state.hat) {\n\t\t\tconst hatPad = Math.max(0, Math.floor((getSpeciesWidth(this.state.species) - 1) / 2) - 1);\n\t\t\tlines.push(\" \".repeat(hatPad) + this.state.hat);\n\t\t}\n\n\t\t// Sprite lines\n\t\tconst spriteWidth = getSpeciesWidth(this.state.species);\n\t\tlines.push(...rendered);\n\n\t\t// Heart animation line above sprite\n\t\tif (this.isPetting && this.hearts.length > 0) {\n\t\t\tconst heartLine = \" \".repeat(spriteWidth);\n\t\t\tconst chars = heartLine.split(\"\");\n\t\t\tfor (const heart of this.hearts) {\n\t\t\t\tconst x = Math.min(heart.x, chars.length - 2);\n\t\t\t\tif (x >= 0 && heart.life > 0) {\n\t\t\t\t\tconst hChar = heart.char;\n\t\t\t\t\tchars.splice(x, hChar.length, ...hChar.split(\"\"));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Insert heart line before sprite\n\t\t\tlines.splice(this.state.hat ? 1 : 0, 0, chars.join(\"\"));\n\t\t}\n\n\t\t// Name + rarity line\n\t\tconst shinyMark = this.state.shiny ? \" ✨\" : \"\";\n\t\tconst rarityColor = this.rarityColor(this.state.rarity);\n\t\tconst nameLine = ` ${theme.bold(this.state.name)}${shinyMark} ${theme.fg(rarityColor, `[${this.state.rarity}]`)} ${theme.fg(\"muted\", this.state.species)}`;\n\t\tlines.push(nameLine);\n\n\t\t// Stats line\n\t\tconst statParts = (Object.values(Stat) as Stat[]).map((s) => {\n\t\t\tconst val = this.state.stats[s];\n\t\t\tconst bar = this.statBar(val);\n\t\t\treturn `${theme.fg(\"muted\", s[0])}:${bar}`;\n\t\t});\n\t\tlines.push(` ${statParts.join(\" \")}`);\n\n\t\t// Thinking indicator\n\t\tif (this.thinkingLabel !== null) {\n\t\t\tconst dots = \".\".repeat((this.thinkingDots % BuddyComponent.THINKING_DOT_COUNT) + 1);\n\t\t\tconst label = this.thinkingLabel || \"thinking\";\n\t\t\tlines.push(` ${theme.fg(\"muted\", `💭 ${label}${dots}`)}`);\n\t\t}\n\n\t\t// Speech bubble (beside or below sprite)\n\t\tif (this.speechText) {\n\t\t\tconst bubbleLines = this.formatSpeechBubble(this.speechText, Math.min(width - 4, 60));\n\t\t\tlines.push(...bubbleLines);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\t// =============================================================================\n\t// Narrow rendering (< 100 cols)\n\t// =============================================================================\n\n\tprivate renderNarrow(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// Single-line face\n\t\tconst eyes = this.state.eyeStyle;\n\t\tconst mouth = this.isPetting ? \"♥\" : \">\";\n\t\tconst face = `${this.state.hat}${eyes}${mouth}${eyes}`;\n\n\t\t// Name + truncated quip\n\t\tconst shinyMark = this.state.shiny ? \"✨\" : \"\";\n\t\tconst name = `${theme.bold(this.state.name)}${shinyMark}`;\n\n\t\tif (this.speechText) {\n\t\t\tconst maxQuip = Math.max(10, width - face.length - this.state.name.length - 6);\n\t\t\tconst styledQuip = this.renderInlineMarkdown(this.speechText);\n\t\t\tconst quip = visibleWidth(styledQuip) > maxQuip ? `${truncateToWidth(styledQuip, maxQuip - 1)}…` : styledQuip;\n\t\t\tlines.push(` ${face} ${name}: ${theme.fg(\"accent\", quip)}`);\n\t\t} else if (this.thinkingLabel !== null) {\n\t\t\tconst dots = \".\".repeat((this.thinkingDots % BuddyComponent.THINKING_DOT_COUNT) + 1);\n\t\t\tconst label = this.thinkingLabel || \"thinking\";\n\t\t\tlines.push(` ${face} ${name} ${theme.fg(\"muted\", `💭 ${label}${dots}`)}`);\n\t\t} else {\n\t\t\tlines.push(` ${face} ${name}`);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\t// =============================================================================\n\t// Animation\n\t// =============================================================================\n\n\tprivate startAnimation(): void {\n\t\tthis.interval = setInterval(() => {\n\t\t\tthis.currentFrame = (this.currentFrame + 1) % this.totalFrames;\n\n\t\t\t// Tick thinking dots\n\t\t\tif (this.thinkingLabel !== null) {\n\t\t\t\tthis.thinkingDots = (this.thinkingDots + 1) % BuddyComponent.THINKING_DOT_COUNT;\n\t\t\t}\n\n\t\t\t// Tick hearts\n\t\t\tif (this.isPetting) {\n\t\t\t\tfor (const heart of this.hearts) {\n\t\t\t\t\theart.life--;\n\t\t\t\t\theart.y++;\n\t\t\t\t}\n\t\t\t\tthis.hearts = this.hearts.filter((h) => h.life > 0);\n\t\t\t}\n\n\t\t\tthis.bumpVersion();\n\t\t\tthis.ui.requestRender();\n\t\t}, IDLE_INTERVAL_MS);\n\t}\n\n\tprivate stopAnimation(): void {\n\t\tif (this.interval) {\n\t\t\tclearInterval(this.interval);\n\t\t\tthis.interval = null;\n\t\t}\n\t}\n\n\tprivate bumpVersion(): void {\n\t\tthis.renderVersion++;\n\t}\n\n\t// =============================================================================\n\t// Formatting helpers\n\t// =============================================================================\n\n\tprivate statBar(value: number): string {\n\t\tconst filled = Math.round(value / 10);\n\t\tconst empty = 10 - filled;\n\t\tconst bar = \"█\".repeat(filled) + \"░\".repeat(empty);\n\t\tconst color: \"success\" | \"warning\" | \"error\" = value >= 70 ? \"success\" : value >= 40 ? \"warning\" : \"error\";\n\t\treturn theme.fg(color, bar);\n\t}\n\n\tprivate rarityColor(rarity: Rarity): \"muted\" | \"success\" | \"accent\" | \"warning\" | \"error\" {\n\t\tswitch (rarity) {\n\t\t\tcase Rarity.COMMON:\n\t\t\t\treturn \"muted\";\n\t\t\tcase Rarity.UNCOMMON:\n\t\t\t\treturn \"success\";\n\t\t\tcase Rarity.RARE:\n\t\t\t\treturn \"accent\";\n\t\t\tcase Rarity.EPIC:\n\t\t\t\treturn \"warning\";\n\t\t\tcase Rarity.LEGENDARY:\n\t\t\t\treturn \"error\";\n\t\t}\n\t}\n\n\tprivate formatSpeechBubble(text: string, maxWidth: number): string[] {\n\t\tif (!text.trim()) return [];\n\n\t\t// Render inline markdown (bold, italic, code) to styled text with ANSI codes\n\t\tconst styledText = this.renderInlineMarkdown(text);\n\n\t\t// Word-wrap using visible width (ANSI-aware)\n\t\tconst lines: string[] = [];\n\t\tconst words = styledText.split(\" \");\n\t\tlet currentLine = \"\";\n\n\t\tfor (const word of words) {\n\t\t\tconst test = currentLine ? `${currentLine} ${word}` : word;\n\t\t\tif (visibleWidth(test) > maxWidth) {\n\t\t\t\tif (currentLine) lines.push(currentLine);\n\t\t\t\tcurrentLine = word;\n\t\t\t} else {\n\t\t\t\tcurrentLine = test;\n\t\t\t}\n\t\t}\n\t\tif (currentLine) lines.push(currentLine);\n\n\t\t// Wrap in bubble border using visible width for measurement\n\t\tconst maxLineWidth = Math.max(...lines.map((l) => visibleWidth(l)));\n\t\tconst bubbleWidth = Math.max(6, Math.min(maxLineWidth + 4, maxWidth + 4));\n\t\tconst top = `╭${\"─\".repeat(bubbleWidth - 2)}╮`;\n\t\tconst bottom = `╰${\"─\".repeat(bubbleWidth - 2)}╯`;\n\n\t\tconst result: string[] = [];\n\t\tresult.push(` ${theme.fg(\"accent\", top)}`);\n\t\tfor (const line of lines) {\n\t\t\tconst padding = Math.max(0, bubbleWidth - 4 - visibleWidth(line));\n\t\t\tconst padded = line + \" \".repeat(padding);\n\t\t\tresult.push(` ${theme.fg(\"accent\", \"│\")} ${padded} ${theme.fg(\"accent\", \"│\")}`);\n\t\t}\n\t\tresult.push(` ${theme.fg(\"accent\", bottom)}`);\n\n\t\treturn result;\n\t}\n\n\t/** Render inline markdown tokens (bold, italic, code) to ANSI-styled text */\n\tprivate renderInlineMarkdown(text: string): string {\n\t\tconst mdTheme = getMarkdownTheme();\n\t\tconst tokens = marked.lexer(text);\n\n\t\t// Flatten: we expect a paragraph containing inline tokens\n\t\tconst inlineTokens = tokens.flatMap((t: any) =>\n\t\t\tt.type === \"paragraph\" ? (t.tokens ?? []) : t.type === \"text\" ? t : [],\n\t\t);\n\n\t\treturn this.renderInlineTokens(inlineTokens, mdTheme);\n\t}\n\n\t/** Recursively render marked inline tokens to ANSI-styled strings */\n\tprivate renderInlineTokens(tokens: any[], mdTheme: MT): string {\n\t\tlet result = \"\";\n\t\tfor (const token of tokens) {\n\t\t\tswitch (token.type) {\n\t\t\t\tcase \"text\":\n\t\t\t\t\tresult += token.text ?? this.renderInlineTokens(token.tokens ?? [], mdTheme);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"strong\":\n\t\t\t\t\tresult += mdTheme.bold(this.renderInlineTokens(token.tokens ?? [], mdTheme));\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"em\":\n\t\t\t\t\tresult += mdTheme.italic(this.renderInlineTokens(token.tokens ?? [], mdTheme));\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"codespan\":\n\t\t\t\t\tresult += mdTheme.code(token.text);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"escape\":\n\t\t\t\t\tresult += token.text;\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tresult += token.text ?? token.raw ?? \"\";\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"buddy-component.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/buddy-component.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BuddyComponent — TUI component rendering the buddy companion.\n *\n * Renders below the editor with:\n * - 3-frame idle animation cycling at 500ms\n * - Speech bubbles for reactions and name-call responses\n * - Pet hearts animation (2.5s)\n * - Narrow terminal fallback (<100 cols)\n * - Stat display and rarity badge\n */\n\nimport type { Component, MarkdownTheme as MT, TUI } from \"@dreb/tui\";\nimport { joinColumns, truncateToWidth, visibleWidth, wrapTextWithAnsi } from \"@dreb/tui\";\nimport { marked } from \"marked\";\nimport { applyEyes, getSpeciesFrames, getSpeciesWidth } from \"../../../core/buddy/buddy-species.js\";\nimport type { BuddyState } from \"../../../core/buddy/buddy-types.js\";\nimport { Rarity, Stat } from \"../../../core/buddy/buddy-types.js\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.js\";\n\nconst IDLE_INTERVAL_MS = 500;\nconst SPEECH_BUBBLE_DURATION_MS = 10000;\nconst PET_DURATION_MS = 2500;\nconst HEART_CHARS = [\"❤️\", \"💕\", \"💖\", \"💗\", \"✨\"];\nconst NARROW_THRESHOLD = 100;\nconst SPEECH_MAX_CONTENT_LINES = 3;\nconst SIDE_PANEL_GAP = 2;\n\nexport class BuddyComponent implements Component {\n\tprivate ui: TUI;\n\tprivate state: BuddyState;\n\tprivate interval: ReturnType<typeof setInterval> | null = null;\n\n\t// Animation state\n\tprivate currentFrame = 0;\n\tprivate totalFrames = 3;\n\n\t// Speech bubble\n\tprivate speechText: string | null = null;\n\tprivate speechTimeout: ReturnType<typeof setTimeout> | null = null;\n\n\t// Pet animation\n\tprivate isPetting = false;\n\tprivate petTimeout: ReturnType<typeof setTimeout> | null = null;\n\tprivate hearts: Array<{ x: number; y: number; char: string; life: number }> = [];\n\n\t// Thinking indicator\n\tprivate thinkingLabel: string | null = null;\n\tprivate thinkingDots = 0;\n\tprivate static readonly THINKING_DOT_COUNT = 4; // cycles 0,1,2,3 → \".\", \"..\", \"...\", \"....\"\n\n\t// Cached render\n\tprivate cachedLines: string[] = [];\n\tprivate cachedWidth = 0;\n\tprivate cachedVersion = -1;\n\tprivate renderVersion = 0;\n\n\tconstructor(ui: TUI, state: BuddyState) {\n\t\tthis.ui = ui;\n\t\tthis.state = state;\n\t\tthis.totalFrames = getSpeciesFrames(state.species).length;\n\t\tthis.startAnimation();\n\t}\n\n\t/** Update buddy state (e.g. after reroll) */\n\tupdateState(state: BuddyState): void {\n\t\tthis.state = state;\n\t\tthis.totalFrames = getSpeciesFrames(state.species).length;\n\t\tthis.currentFrame = 0;\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Show a speech bubble with text */\n\tshowSpeech(text: string): void {\n\t\tthis.speechText = text;\n\t\tif (this.speechTimeout) clearTimeout(this.speechTimeout);\n\t\tthis.speechTimeout = setTimeout(() => {\n\t\t\tthis.speechText = null;\n\t\t\tthis.speechTimeout = null;\n\t\t\tthis.bumpVersion();\n\t\t\tthis.ui.requestRender();\n\t\t}, SPEECH_BUBBLE_DURATION_MS);\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Trigger pet animation */\n\tpet(): void {\n\t\tthis.isPetting = true;\n\t\t// Spawn hearts\n\t\tconst spriteWidth = getSpeciesWidth(this.state.species);\n\t\tfor (let i = 0; i < 5; i++) {\n\t\t\tthis.hearts.push({\n\t\t\t\tx: Math.floor(Math.random() * spriteWidth),\n\t\t\t\ty: -1 - Math.floor(Math.random() * 3),\n\t\t\t\tchar: HEART_CHARS[Math.floor(Math.random() * HEART_CHARS.length)],\n\t\t\t\tlife: 5 + Math.floor(Math.random() * 5),\n\t\t\t});\n\t\t}\n\t\tif (this.petTimeout) clearTimeout(this.petTimeout);\n\t\tthis.petTimeout = setTimeout(() => {\n\t\t\tthis.isPetting = false;\n\t\t\tthis.hearts = [];\n\t\t\tthis.petTimeout = null;\n\t\t\tthis.bumpVersion();\n\t\t\tthis.ui.requestRender();\n\t\t}, PET_DURATION_MS);\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Show a pulsing thinking indicator with optional label */\n\tshowThinking(label?: string): void {\n\t\tthis.thinkingLabel = label ?? \"thinking\";\n\t\tthis.thinkingDots = 0;\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Hide the thinking indicator */\n\thideThinking(): void {\n\t\tthis.thinkingLabel = null;\n\t\tthis.thinkingDots = 0;\n\t\tthis.bumpVersion();\n\t\tthis.ui.requestRender();\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedWidth = 0;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (width === this.cachedWidth && this.cachedVersion === this.renderVersion) {\n\t\t\treturn this.cachedLines;\n\t\t}\n\n\t\tif (width < NARROW_THRESHOLD) {\n\t\t\tthis.cachedLines = this.renderNarrow(width);\n\t\t} else {\n\t\t\tthis.cachedLines = this.renderFull(width);\n\t\t}\n\n\t\tthis.cachedWidth = width;\n\t\tthis.cachedVersion = this.renderVersion;\n\t\treturn this.cachedLines;\n\t}\n\n\tdispose(): void {\n\t\tthis.stopAnimation();\n\t\tthis.thinkingLabel = null;\n\t\tif (this.speechTimeout) {\n\t\t\tclearTimeout(this.speechTimeout);\n\t\t\tthis.speechTimeout = null;\n\t\t}\n\t\tif (this.petTimeout) {\n\t\t\tclearTimeout(this.petTimeout);\n\t\t\tthis.petTimeout = null;\n\t\t}\n\t}\n\n\t// =============================================================================\n\t// Full rendering (wide terminal)\n\t// =============================================================================\n\n\tprivate renderFull(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst frames = getSpeciesFrames(this.state.species);\n\t\tconst frame = frames[this.currentFrame % this.totalFrames];\n\t\tconst rendered = applyEyes(frame, this.state.eyeStyle);\n\t\tconst spriteWidth = getSpeciesWidth(this.state.species);\n\n\t\t// Build LEFT block: hat + heart animation + sprite lines\n\t\tconst leftLines: string[] = [];\n\n\t\t// Hat line\n\t\tif (this.state.hat) {\n\t\t\tconst hatPad = Math.max(0, Math.floor((spriteWidth - 1) / 2) - 1);\n\t\t\tleftLines.push(\" \".repeat(hatPad) + this.state.hat);\n\t\t}\n\n\t\t// Heart animation line (above sprite, inserted at top)\n\t\tif (this.isPetting && this.hearts.length > 0) {\n\t\t\tconst heartLine = \" \".repeat(spriteWidth);\n\t\t\tconst chars = heartLine.split(\"\");\n\t\t\tfor (const heart of this.hearts) {\n\t\t\t\tconst x = Math.min(heart.x, chars.length - 2);\n\t\t\t\tif (x >= 0 && heart.life > 0) {\n\t\t\t\t\tconst hChar = heart.char;\n\t\t\t\t\tchars.splice(x, hChar.length, ...hChar.split(\"\"));\n\t\t\t\t}\n\t\t\t}\n\t\t\tleftLines.push(chars.join(\"\"));\n\t\t}\n\n\t\t// Sprite lines\n\t\tleftLines.push(...rendered);\n\n\t\t// Build RIGHT block: speech bubble or thinking indicator\n\t\tlet rightLines: string[] = [];\n\t\tif (this.speechText) {\n\t\t\tconst availableWidth = width - spriteWidth - SIDE_PANEL_GAP - 5; // 5 for leading space + bubble borders + padding\n\t\t\tconst bubbleMaxWidth = Math.max(20, availableWidth);\n\t\t\trightLines = this.formatSpeechBubble(this.speechText, bubbleMaxWidth);\n\t\t} else if (this.thinkingLabel !== null) {\n\t\t\tconst dots = \".\".repeat((this.thinkingDots % BuddyComponent.THINKING_DOT_COUNT) + 1);\n\t\t\tconst label = this.thinkingLabel || \"thinking\";\n\t\t\trightLines = [` ${theme.fg(\"muted\", `💭 ${label}${dots}`)}`];\n\t\t}\n\n\t\t// Merge left and right side-by-side\n\t\tif (rightLines.length > 0) {\n\t\t\tconst merged = joinColumns(leftLines, rightLines, SIDE_PANEL_GAP, width);\n\t\t\tlines.push(...merged);\n\t\t} else {\n\t\t\tlines.push(...leftLines);\n\t\t}\n\n\t\t// Name + rarity line (full width, below the sprite+panel area)\n\t\tconst shinyMark = this.state.shiny ? \" ✨\" : \"\";\n\t\tconst rarityColor = this.rarityColor(this.state.rarity);\n\t\tconst nameLine = ` ${theme.bold(this.state.name)}${shinyMark} ${theme.fg(rarityColor, `[${this.state.rarity}]`)} ${theme.fg(\"muted\", this.state.species)}`;\n\t\tlines.push(nameLine);\n\n\t\t// Stats line (full width)\n\t\tconst statParts = (Object.values(Stat) as Stat[]).map((s) => {\n\t\t\tconst val = this.state.stats[s];\n\t\t\tconst bar = this.statBar(val);\n\t\t\treturn `${theme.fg(\"muted\", s[0])}:${bar}`;\n\t\t});\n\t\tlines.push(` ${statParts.join(\" \")}`);\n\n\t\treturn lines;\n\t}\n\n\t// =============================================================================\n\t// Narrow rendering (< 100 cols)\n\t// =============================================================================\n\n\tprivate renderNarrow(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// Single-line face\n\t\tconst eyes = this.state.eyeStyle;\n\t\tconst mouth = this.isPetting ? \"♥\" : \">\";\n\t\tconst face = `${this.state.hat}${eyes}${mouth}${eyes}`;\n\n\t\t// Name + truncated quip\n\t\tconst shinyMark = this.state.shiny ? \"✨\" : \"\";\n\t\tconst name = `${theme.bold(this.state.name)}${shinyMark}`;\n\n\t\tif (this.speechText) {\n\t\t\tconst maxQuip = Math.max(10, width - face.length - this.state.name.length - 6);\n\t\t\tconst styledQuip = this.renderInlineMarkdown(this.speechText);\n\t\t\tconst quip = visibleWidth(styledQuip) > maxQuip ? `${truncateToWidth(styledQuip, maxQuip - 1)}…` : styledQuip;\n\t\t\tlines.push(` ${face} ${name}: ${theme.fg(\"accent\", quip)}`);\n\t\t} else if (this.thinkingLabel !== null) {\n\t\t\tconst dots = \".\".repeat((this.thinkingDots % BuddyComponent.THINKING_DOT_COUNT) + 1);\n\t\t\tconst label = this.thinkingLabel || \"thinking\";\n\t\t\tlines.push(` ${face} ${name} ${theme.fg(\"muted\", `💭 ${label}${dots}`)}`);\n\t\t} else {\n\t\t\tlines.push(` ${face} ${name}`);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\t// =============================================================================\n\t// Animation\n\t// =============================================================================\n\n\tprivate startAnimation(): void {\n\t\tthis.interval = setInterval(() => {\n\t\t\tthis.currentFrame = (this.currentFrame + 1) % this.totalFrames;\n\n\t\t\t// Tick thinking dots\n\t\t\tif (this.thinkingLabel !== null) {\n\t\t\t\tthis.thinkingDots = (this.thinkingDots + 1) % BuddyComponent.THINKING_DOT_COUNT;\n\t\t\t}\n\n\t\t\t// Tick hearts\n\t\t\tif (this.isPetting) {\n\t\t\t\tfor (const heart of this.hearts) {\n\t\t\t\t\theart.life--;\n\t\t\t\t\theart.y++;\n\t\t\t\t}\n\t\t\t\tthis.hearts = this.hearts.filter((h) => h.life > 0);\n\t\t\t}\n\n\t\t\tthis.bumpVersion();\n\t\t\tthis.ui.requestRender();\n\t\t}, IDLE_INTERVAL_MS);\n\t}\n\n\tprivate stopAnimation(): void {\n\t\tif (this.interval) {\n\t\t\tclearInterval(this.interval);\n\t\t\tthis.interval = null;\n\t\t}\n\t}\n\n\tprivate bumpVersion(): void {\n\t\tthis.renderVersion++;\n\t}\n\n\t// =============================================================================\n\t// Formatting helpers\n\t// =============================================================================\n\n\tprivate statBar(value: number): string {\n\t\tconst filled = Math.round(value / 10);\n\t\tconst empty = 10 - filled;\n\t\tconst bar = \"█\".repeat(filled) + \"░\".repeat(empty);\n\t\tconst color: \"success\" | \"warning\" | \"error\" = value >= 70 ? \"success\" : value >= 40 ? \"warning\" : \"error\";\n\t\treturn theme.fg(color, bar);\n\t}\n\n\tprivate rarityColor(rarity: Rarity): \"muted\" | \"success\" | \"accent\" | \"warning\" | \"error\" {\n\t\tswitch (rarity) {\n\t\t\tcase Rarity.COMMON:\n\t\t\t\treturn \"muted\";\n\t\t\tcase Rarity.UNCOMMON:\n\t\t\t\treturn \"success\";\n\t\t\tcase Rarity.RARE:\n\t\t\t\treturn \"accent\";\n\t\t\tcase Rarity.EPIC:\n\t\t\t\treturn \"warning\";\n\t\t\tcase Rarity.LEGENDARY:\n\t\t\t\treturn \"error\";\n\t\t}\n\t}\n\n\tprivate formatSpeechBubble(text: string, maxWidth: number): string[] {\n\t\tif (!text.trim()) return [];\n\n\t\t// Render inline markdown (bold, italic, code) to styled text with ANSI codes\n\t\tconst styledText = this.renderInlineMarkdown(text);\n\n\t\t// Word-wrap using visible width (ANSI-aware, handles long words)\n\t\tconst lines = wrapTextWithAnsi(styledText, maxWidth);\n\n\t\t// Enforce hard line cap\n\t\tif (lines.length > SPEECH_MAX_CONTENT_LINES) {\n\t\t\tconst kept = lines.slice(0, SPEECH_MAX_CONTENT_LINES - 1);\n\t\t\tconst lastLine = lines[SPEECH_MAX_CONTENT_LINES - 1];\n\t\t\t// Truncate the last kept line with ellipsis\n\t\t\tconst truncated = truncateToWidth(lastLine, maxWidth - 1, \"…\");\n\t\t\tkept.push(truncated);\n\t\t\tlines.length = 0;\n\t\t\tlines.push(...kept);\n\t\t}\n\n\t\t// Wrap in bubble border using visible width for measurement\n\t\tconst maxLineWidth = Math.max(...lines.map((l) => visibleWidth(l)));\n\t\tconst bubbleWidth = Math.max(6, Math.min(maxLineWidth + 4, maxWidth + 4));\n\t\tconst top = `╭${\"─\".repeat(bubbleWidth - 2)}╮`;\n\t\tconst bottom = `╰${\"─\".repeat(bubbleWidth - 2)}╯`;\n\n\t\tconst result: string[] = [];\n\t\tresult.push(` ${theme.fg(\"accent\", top)}`);\n\t\tfor (const line of lines) {\n\t\t\tconst padding = Math.max(0, bubbleWidth - 4 - visibleWidth(line));\n\t\t\tconst padded = line + \" \".repeat(padding);\n\t\t\tresult.push(` ${theme.fg(\"accent\", \"│\")} ${padded} ${theme.fg(\"accent\", \"│\")}`);\n\t\t}\n\t\tresult.push(` ${theme.fg(\"accent\", bottom)}`);\n\n\t\treturn result;\n\t}\n\n\t/** Render inline markdown tokens (bold, italic, code) to ANSI-styled text */\n\tprivate renderInlineMarkdown(text: string): string {\n\t\tconst mdTheme = getMarkdownTheme();\n\t\tconst tokens = marked.lexer(text);\n\n\t\t// Flatten: we expect a paragraph containing inline tokens\n\t\tconst inlineTokens = tokens.flatMap((t: any) =>\n\t\t\tt.type === \"paragraph\" ? (t.tokens ?? []) : t.type === \"text\" ? t : [],\n\t\t);\n\n\t\treturn this.renderInlineTokens(inlineTokens, mdTheme);\n\t}\n\n\t/** Recursively render marked inline tokens to ANSI-styled strings */\n\tprivate renderInlineTokens(tokens: any[], mdTheme: MT): string {\n\t\tlet result = \"\";\n\t\tfor (const token of tokens) {\n\t\t\tswitch (token.type) {\n\t\t\t\tcase \"text\":\n\t\t\t\t\tresult += token.text ?? this.renderInlineTokens(token.tokens ?? [], mdTheme);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"strong\":\n\t\t\t\t\tresult += mdTheme.bold(this.renderInlineTokens(token.tokens ?? [], mdTheme));\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"em\":\n\t\t\t\t\tresult += mdTheme.italic(this.renderInlineTokens(token.tokens ?? [], mdTheme));\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"codespan\":\n\t\t\t\t\tresult += mdTheme.code(token.text);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"escape\":\n\t\t\t\t\tresult += token.text;\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tresult += token.text ?? token.raw ?? \"\";\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,WAAW,CAAC;AAC1E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AA0BxF;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAI/C,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,UAAU;IAJnB,OAAO,CAAC,kBAAkB,CAAQ;IAElC,YACS,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B,EAC3C;IAEJ,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,WAAW,CAAC;AAC1E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AA0BxF;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAI/C,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,UAAU;IAJnB,OAAO,CAAC,kBAAkB,CAAQ;IAElC,YACS,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B,EAC3C;IAEJ,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAoK9B;CACD","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@dreb/tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\n/**\n * Format token counts (similar to web-ui)\n */\nfunction formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = process.cwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line as sections separated by ·\n\t\tconst tokenParts = [];\n\t\tif (totalInput) tokenParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) tokenParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) tokenParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) tokenParts.push(`W${formatTokens(totalCacheWrite)}`);\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tlet costStr = \"\";\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tcostStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\t// Append daily total when there's cross-session spend\n\t\t\tconst dailyCost = this.footerData.getDailyCost();\n\t\t\tif (dailyCost > totalCost) {\n\t\t\t\tcostStr += `, today: $${dailyCost.toFixed(2)}`;\n\t\t\t}\n\t\t}\n\n\t\t// Colorize context percentage based on usage\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\n\t\t// Join sections with · separator\n\t\tconst sections: string[] = [];\n\t\tif (tokenParts.length > 0) sections.push(tokenParts.join(\" \"));\n\t\tif (costStr) sections.push(costStr);\n\t\tsections.push(contextPercentStr);\n\n\t\tlet statsLeft = sections.join(\" · \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
|
|
@@ -93,21 +93,26 @@ export class FooterComponent {
|
|
|
93
93
|
if (sessionName) {
|
|
94
94
|
pwd = `${pwd} • ${sessionName}`;
|
|
95
95
|
}
|
|
96
|
-
// Build stats line
|
|
97
|
-
const
|
|
96
|
+
// Build stats line as sections separated by ·
|
|
97
|
+
const tokenParts = [];
|
|
98
98
|
if (totalInput)
|
|
99
|
-
|
|
99
|
+
tokenParts.push(`↑${formatTokens(totalInput)}`);
|
|
100
100
|
if (totalOutput)
|
|
101
|
-
|
|
101
|
+
tokenParts.push(`↓${formatTokens(totalOutput)}`);
|
|
102
102
|
if (totalCacheRead)
|
|
103
|
-
|
|
103
|
+
tokenParts.push(`R${formatTokens(totalCacheRead)}`);
|
|
104
104
|
if (totalCacheWrite)
|
|
105
|
-
|
|
105
|
+
tokenParts.push(`W${formatTokens(totalCacheWrite)}`);
|
|
106
106
|
// Show cost with "(sub)" indicator if using OAuth subscription
|
|
107
|
+
let costStr = "";
|
|
107
108
|
const usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;
|
|
108
109
|
if (totalCost || usingSubscription) {
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
costStr = `$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
|
|
111
|
+
// Append daily total when there's cross-session spend
|
|
112
|
+
const dailyCost = this.footerData.getDailyCost();
|
|
113
|
+
if (dailyCost > totalCost) {
|
|
114
|
+
costStr += `, today: $${dailyCost.toFixed(2)}`;
|
|
115
|
+
}
|
|
111
116
|
}
|
|
112
117
|
// Colorize context percentage based on usage
|
|
113
118
|
let contextPercentStr;
|
|
@@ -124,8 +129,14 @@ export class FooterComponent {
|
|
|
124
129
|
else {
|
|
125
130
|
contextPercentStr = contextPercentDisplay;
|
|
126
131
|
}
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
// Join sections with · separator
|
|
133
|
+
const sections = [];
|
|
134
|
+
if (tokenParts.length > 0)
|
|
135
|
+
sections.push(tokenParts.join(" "));
|
|
136
|
+
if (costStr)
|
|
137
|
+
sections.push(costStr);
|
|
138
|
+
sections.push(contextPercentStr);
|
|
139
|
+
let statsLeft = sections.join(" · ");
|
|
129
140
|
// Add model name on the right side, plus thinking level if model supports it
|
|
130
141
|
const modelName = state.model?.id || "no-model";
|
|
131
142
|
let statsLeftWidth = visibleWidth(statsLeft);
|