@agimon-ai/doompi-log 0.0.1-alpha.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/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/_virtual/_rolldown/runtime.cjs +1 -0
- package/dist/extension.cjs +2 -0
- package/dist/extension.cjs.map +1 -0
- package/dist/extension.d.cts +33 -0
- package/dist/extension.d.cts.map +1 -0
- package/dist/extension.d.mts +33 -0
- package/dist/extension.d.mts.map +1 -0
- package/dist/extension.mjs +2 -0
- package/dist/extension.mjs.map +1 -0
- package/dist/extensions/pi.cjs +2 -0
- package/dist/extensions/pi.cjs.map +1 -0
- package/dist/extensions/pi.d.cts +4 -0
- package/dist/extensions/pi.d.cts.map +1 -0
- package/dist/extensions/pi.d.mts +5 -0
- package/dist/extensions/pi.d.mts.map +1 -0
- package/dist/extensions/pi.mjs +2 -0
- package/dist/extensions/pi.mjs.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +1 -0
- package/dist/metrics.cjs +2 -0
- package/dist/metrics.cjs.map +1 -0
- package/dist/metrics.d.cts +94 -0
- package/dist/metrics.d.cts.map +1 -0
- package/dist/metrics.d.mts +94 -0
- package/dist/metrics.d.mts.map +1 -0
- package/dist/metrics.mjs +2 -0
- package/dist/metrics.mjs.map +1 -0
- package/dist/metricsSource.cjs +2 -0
- package/dist/metricsSource.cjs.map +1 -0
- package/dist/metricsSource.d.cts +25 -0
- package/dist/metricsSource.d.cts.map +1 -0
- package/dist/metricsSource.d.mts +25 -0
- package/dist/metricsSource.d.mts.map +1 -0
- package/dist/metricsSource.mjs +2 -0
- package/dist/metricsSource.mjs.map +1 -0
- package/dist/tui/doomOverlay.cjs +2 -0
- package/dist/tui/doomOverlay.cjs.map +1 -0
- package/dist/tui/doomOverlay.d.cts +63 -0
- package/dist/tui/doomOverlay.d.cts.map +1 -0
- package/dist/tui/doomOverlay.d.mts +63 -0
- package/dist/tui/doomOverlay.d.mts.map +1 -0
- package/dist/tui/doomOverlay.mjs +2 -0
- package/dist/tui/doomOverlay.mjs.map +1 -0
- package/dist/tui/metricsOverlay.cjs +2 -0
- package/dist/tui/metricsOverlay.cjs.map +1 -0
- package/dist/tui/metricsOverlay.d.cts +71 -0
- package/dist/tui/metricsOverlay.d.cts.map +1 -0
- package/dist/tui/metricsOverlay.d.mts +71 -0
- package/dist/tui/metricsOverlay.d.mts.map +1 -0
- package/dist/tui/metricsOverlay.mjs +2 -0
- package/dist/tui/metricsOverlay.mjs.map +1 -0
- package/dist/tui/rendering.cjs +2 -0
- package/dist/tui/rendering.cjs.map +1 -0
- package/dist/tui/rendering.mjs +2 -0
- package/dist/tui/rendering.mjs.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.mjs","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["/**\n * Session-local aggregation of the log records the Pi telemetry extension\n * already emits.\n *\n * The aggregator is fed `(recordName, attributes)` pairs rather than Pi events\n * so it can only ever see what the sink sees: no extra instrumentation is added\n * for metrics, and nothing here depends on a live sink. Everything is scoped to\n * the current process, so there is no persistence and no query API.\n */\n\n/** Record names the aggregator derives numbers from. */\nexport const TOOL_RESULT_RECORD = 'pi.tool_result';\nexport const API_ERROR_RECORD = 'pi.api_error';\nexport const TURN_FINISHED_RECORD = 'pi.turn.finished';\n\nconst UNKNOWN_TOOL = 'unknown';\nconst TOOL_ERROR_CODE = 'tool';\nconst RECORD_PREFIX = 'pi.';\nconst DEFAULT_MAX_RECENT_ERRORS = 8;\nconst P95_QUANTILE = 0.95;\n\n/**\n * Latency is kept as a fixed logarithmic histogram rather than a sample array,\n * so a long session cannot grow the retained state without bound. Each bucket\n * is 10% wider than the last, which keeps the reported p95 within about 10% of\n * the true value while capping the state at one small map per tool.\n */\nconst LATENCY_BUCKET_GROWTH = 1.1;\nconst LATENCY_BUCKET_LOG_BASE = Math.log(LATENCY_BUCKET_GROWTH);\nconst MINIMUM_SAMPLE_MS = 1;\n\n/** Bucket ceiling per tool: covers 1ms to roughly an hour at 10% resolution. */\nexport const MAX_RETAINED_STATE_PER_TOOL = 160;\n\nexport interface LogMetricsTokenTotals {\n input: number;\n output: number;\n total: number;\n}\n\nexport interface LogMetricsToolLatency {\n name: string;\n calls: number;\n p95Ms?: number;\n /** Buckets currently held for this tool, bounded by MAX_RETAINED_STATE_PER_TOOL. */\n retainedStateSize: number;\n}\n\nexport interface LogMetricsEventCount {\n name: string;\n count: number;\n}\n\nexport interface LogMetricsOperationDuration {\n name: string;\n calls: number;\n p95Ms?: number;\n retainedStateSize: number;\n}\n\nexport interface LogMetricsError {\n at: number;\n event: string;\n message: string;\n code: string;\n}\n\nexport interface LogMetricsSnapshot {\n events: number;\n errors: number;\n toolCalls: number;\n failedToolCalls: number;\n tokens: LogMetricsTokenTotals;\n cost: number;\n toolLatency: LogMetricsToolLatency[];\n eventVolume: LogMetricsEventCount[];\n packageEvents: LogMetricsEventCount[];\n sanitizedFailures: number;\n operationDuration: LogMetricsOperationDuration[];\n recentErrors: LogMetricsError[];\n}\n\nexport interface LogMetricsAggregatorOptions {\n now?: () => number;\n maxRecentErrors?: number;\n}\n\n/** The slice of the aggregator the telemetry extension needs. */\nexport interface LogMetricsRecorder {\n record(name: string, attributes: Record<string, unknown>): void;\n}\n\ninterface ToolLatencyState {\n calls: number;\n samples: number;\n buckets: Map<number, number>;\n}\n\nfunction readNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction readString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nfunction safeMetricName(value: string): string {\n const normalized = value.replace(/[^a-zA-Z0-9@/._:-]/g, '_').slice(0, 128);\n return normalized || 'unknown';\n}\n\nfunction latencyBucket(durationMs: number): number {\n const index = Math.ceil(Math.log(Math.max(durationMs, MINIMUM_SAMPLE_MS)) / LATENCY_BUCKET_LOG_BASE);\n return Math.min(Math.max(index, 0), MAX_RETAINED_STATE_PER_TOOL - 1);\n}\n\nfunction bucketUpperBoundMs(index: number): number {\n return LATENCY_BUCKET_GROWTH ** index;\n}\n\n/**\n * The upper edge of the first bucket whose cumulative count crosses the\n * quantile. Reporting the edge rather than a bucket midpoint keeps the answer\n * an upper bound, so a single 250ms call never reports faster than 250ms.\n */\nfunction bucketedP95(state: ToolLatencyState): number | undefined {\n if (state.samples === 0) return undefined;\n\n const target = state.samples * P95_QUANTILE;\n const indexes = [...state.buckets.keys()].sort((left, right) => left - right);\n let cumulative = 0;\n let selected = indexes[0] as number;\n for (const index of indexes) {\n cumulative += state.buckets.get(index) ?? 0;\n selected = index;\n if (cumulative >= target) break;\n }\n return Math.round(bucketUpperBoundMs(selected));\n}\n\n/** Strips the `pi.` prefix so an error entry reads as the event it came from. */\nfunction errorEventName(recordName: string): string {\n return recordName.startsWith(RECORD_PREFIX) ? recordName.slice(RECORD_PREFIX.length) : recordName;\n}\n\nexport class LogMetricsAggregator implements LogMetricsRecorder {\n private events = 0;\n private errors = 0;\n private toolCalls = 0;\n private failedToolCalls = 0;\n private cost = 0;\n private readonly tokens: LogMetricsTokenTotals = { input: 0, output: 0, total: 0 };\n private readonly eventCounts = new Map<string, number>();\n private readonly packageCounts = new Map<string, number>();\n private readonly tools = new Map<string, ToolLatencyState>();\n private readonly operations = new Map<string, ToolLatencyState>();\n private sanitizedFailures = 0;\n private readonly errorLog: LogMetricsError[] = [];\n private readonly now: () => number;\n private readonly maxRecentErrors: number;\n\n constructor(options: LogMetricsAggregatorOptions = {}) {\n this.now = options.now ?? Date.now;\n this.maxRecentErrors = options.maxRecentErrors ?? DEFAULT_MAX_RECENT_ERRORS;\n }\n\n record(name: string, attributes: Record<string, unknown>): void {\n const metricName = safeMetricName(name);\n this.events += 1;\n this.eventCounts.set(metricName, (this.eventCounts.get(metricName) ?? 0) + 1);\n const packageName = readString(attributes['telemetry.package']);\n if (packageName && !packageName.startsWith('/') && !packageName.includes('\\\\')) {\n const safePackageName = safeMetricName(packageName);\n this.packageCounts.set(safePackageName, (this.packageCounts.get(safePackageName) ?? 0) + 1);\n }\n this.recordOperationDuration(metricName, attributes);\n this.recordSanitizedFailure(metricName, attributes);\n\n if (metricName === TOOL_RESULT_RECORD) this.recordToolResult(metricName, attributes);\n else if (metricName === API_ERROR_RECORD) this.recordApiError(metricName, attributes);\n else if (metricName === TURN_FINISHED_RECORD) this.recordTurnUsage(attributes);\n }\n\n snapshot(): LogMetricsSnapshot {\n const toolLatency = [...this.tools.entries()]\n .map(([name, state]) => {\n const p95Ms = bucketedP95(state);\n return {\n name,\n calls: state.calls,\n retainedStateSize: state.buckets.size,\n ...(p95Ms === undefined ? {} : { p95Ms }),\n };\n })\n .sort((left, right) => (right.p95Ms ?? -1) - (left.p95Ms ?? -1));\n\n const eventVolume = [...this.eventCounts.entries()]\n .map(([name, count]) => ({ name, count }))\n .sort((left, right) => right.count - left.count);\n const packageEvents = [...this.packageCounts.entries()]\n .map(([name, count]) => ({ name, count }))\n .sort((left, right) => right.count - left.count);\n const operationDuration = [...this.operations.entries()]\n .map(([name, state]) => {\n const p95Ms = bucketedP95(state);\n return {\n name,\n calls: state.calls,\n retainedStateSize: state.buckets.size,\n ...(p95Ms === undefined ? {} : { p95Ms }),\n };\n })\n .sort((left, right) => (right.p95Ms ?? -1) - (left.p95Ms ?? -1));\n\n return {\n events: this.events,\n errors: this.errors,\n toolCalls: this.toolCalls,\n failedToolCalls: this.failedToolCalls,\n tokens: { ...this.tokens },\n cost: this.cost,\n toolLatency,\n eventVolume,\n packageEvents,\n sanitizedFailures: this.sanitizedFailures,\n operationDuration,\n recentErrors: this.errorLog.map((entry) => ({ ...entry })),\n };\n }\n\n private recordOperationDuration(recordName: string, attributes: Record<string, unknown>): void {\n const duration = Object.entries(attributes).find(\n ([key, value]) =>\n key !== 'tool.duration_ms' &&\n (key === 'duration_ms' || key.endsWith('.duration_ms')) &&\n readNumber(value) !== undefined,\n );\n if (!duration) return;\n const operationName = safeMetricName(readString(attributes['operation.name']) ?? recordName);\n const state = this.operations.get(operationName) ?? { calls: 0, samples: 0, buckets: new Map<number, number>() };\n this.operations.set(operationName, state);\n state.calls += 1;\n const durationMs = readNumber(duration[1]);\n if (durationMs === undefined) return;\n const bucket = latencyBucket(durationMs);\n state.buckets.set(bucket, (state.buckets.get(bucket) ?? 0) + 1);\n state.samples += 1;\n }\n\n private recordSanitizedFailure(recordName: string, attributes: Record<string, unknown>): void {\n const outcome = readString(attributes.outcome);\n const failed =\n attributes['tool.result.error'] === true ||\n recordName === API_ERROR_RECORD ||\n outcome === 'error' ||\n outcome === 'failed' ||\n outcome === 'failure';\n if (!failed) return;\n this.sanitizedFailures += 1;\n if (recordName === TOOL_RESULT_RECORD || recordName === API_ERROR_RECORD) return;\n const code = readString(attributes['error.code']) ?? outcome ?? 'error';\n this.pushError(recordName, `${readString(attributes['error.type']) ?? 'operation'} failed`, safeMetricName(code));\n }\n\n private recordToolResult(recordName: string, attributes: Record<string, unknown>): void {\n const toolName = readString(attributes['tool.name']) ?? UNKNOWN_TOOL;\n const state = this.tools.get(toolName) ?? { calls: 0, samples: 0, buckets: new Map<number, number>() };\n this.tools.set(toolName, state);\n state.calls += 1;\n this.toolCalls += 1;\n\n // A call whose start was never seen carries no duration, so it counts as a\n // call but contributes no latency sample.\n const durationMs = readNumber(attributes['tool.duration_ms']);\n if (durationMs !== undefined) {\n const bucket = latencyBucket(durationMs);\n state.buckets.set(bucket, (state.buckets.get(bucket) ?? 0) + 1);\n state.samples += 1;\n }\n\n if (attributes['tool.result.error'] === true) {\n this.failedToolCalls += 1;\n // Redaction is on by default, so the tool's own output is unavailable and\n // the tool name is the only detail this entry can carry.\n this.pushError(recordName, `${toolName}: tool call failed`, TOOL_ERROR_CODE);\n }\n }\n\n private recordApiError(recordName: string, attributes: Record<string, unknown>): void {\n this.errors += 1;\n const status = String(attributes['http.response.status_code']);\n this.pushError(recordName, `provider responded ${status}`, status);\n }\n\n private recordTurnUsage(attributes: Record<string, unknown>): void {\n this.tokens.input += readNumber(attributes['gen_ai.usage.input_tokens']) ?? 0;\n this.tokens.output += readNumber(attributes['gen_ai.usage.output_tokens']) ?? 0;\n this.tokens.total += readNumber(attributes['gen_ai.usage.total_tokens']) ?? 0;\n this.cost += readNumber(attributes['gen_ai.usage.cost']) ?? 0;\n }\n\n private pushError(recordName: string, message: string, code: string): void {\n this.errorLog.unshift({ at: this.now(), event: errorEventName(recordName), message, code });\n if (this.errorLog.length > this.maxRecentErrors) this.errorLog.length = this.maxRecentErrors;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./_virtual/_rolldown/runtime.cjs");let t=require("node:child_process"),n=require("node:module"),r=require("node:path");r=e.__toESM(r,1);let i=require("@agimon-ai/log-sink-mcp");const a=`total-tokens`;function o(){let e=(0,n.createRequire)(require("url").pathToFileURL(__filename).href).resolve(`@agimon-ai/log-sink-mcp/package.json`);return r.default.join(r.default.dirname(e),`dist`,`cli.mjs`)}function s(e){return new URLSearchParams({groupBy:e.groupBy,period:e.period,sort:a,limit:String(e.limit),toolLimit:`1`}).toString()}async function c(e,t,n){let r=await n(`${e}/api/metrics?${s(t)}`,{signal:AbortSignal.timeout(5e3)});if(!r.ok)throw Error(`sink responded ${r.status}`);let i=await r.json();if(i.error||!Array.isArray(i.timeline))throw Error(i.error??`sink is an older version`);return i}function l(e){return new Promise((n,r)=>{(0,t.execFile)(process.execPath,[o(),`logs`,`metrics`,`--group-by`,e.groupBy,`--period`,e.period,`--sort`,a,`--limit`,String(e.limit),`--tool-limit`,`1`],{timeout:3e4,maxBuffer:33554432},(e,t,i)=>{if(e){r(Error(i.trim()||e.message));return}try{n(JSON.parse(t))}catch{r(Error(`metrics CLI returned unparsable output`))}})})}function u(e={}){let t=e.fetchImpl??globalThis.fetch,n=e.cliRunner??l,r,a,o=async()=>{if(a!==void 0)return a??void 0;try{a=(await(0,i.resolveLogSinkPort)({env:e.env,cwd:e.cwd,healthCheck:!0}))?.endpoint??null}catch{a=null}return a??void 0};return{lastTransport:()=>r,async query(e){let i=await o();if(i&&t)try{let n=await c(i,e,t);return r=`http`,n}catch{a=null}let s=await n(e);return r=`cli`,s}}}exports.createMetricsSource=u;
|
|
2
|
+
//# sourceMappingURL=metricsSource.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricsSource.cjs","names":["createRequire","path","resolveLogSinkPort"],"sources":["../src/metricsSource.ts"],"sourcesContent":["/**\n * Historical metrics access for the `SPC h l` overlay.\n *\n * Two transports, in order: the running sink's HTTP API, then the installed\n * CLI as a subprocess. The sink daemon is long-lived and often lags the\n * installed package, so a daemon that rejects newer query parameters is treated\n * as unavailable rather than fatal. The reader exported by log-sink-mcp is\n * deliberately not used in-process: better-sqlite3 is synchronous and a\n * one-day window blocks for seconds, which would freeze the TUI.\n */\nimport { execFile } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { resolveLogSinkPort } from '@agimon-ai/log-sink-mcp';\nimport type { LogMetricsGroupBy, LogMetricsPeriod, LogMetricsReport } from '@agimon-ai/log-sink-mcp';\n\nconst PACKAGE_NAME = '@agimon-ai/log-sink-mcp';\nconst HTTP_TIMEOUT_MS = 5000;\nconst CLI_TIMEOUT_MS = 30000;\n/** A day of dev telemetry is already ~140k records; keep the payload bounded. */\nconst MAX_CLI_BUFFER = 32 * 1024 * 1024;\nconst TOOL_LIMIT = '1';\n/** The panel ranks consumers, so ask for the biggest rather than the most recent. */\nconst TOKEN_SORT = 'total-tokens';\n\nexport interface MetricsQueryParams {\n groupBy: LogMetricsGroupBy;\n period: LogMetricsPeriod;\n limit: number;\n}\n\nexport type MetricsQuery = (params: MetricsQueryParams) => Promise<LogMetricsReport>;\n\n/** Where the last successful answer came from, so the overlay can say so. */\nexport type MetricsTransport = 'http' | 'cli';\n\nexport interface MetricsSourceOptions {\n env?: NodeJS.ProcessEnv;\n cwd?: string;\n /** Overrides transport discovery in tests. */\n fetchImpl?: typeof fetch;\n cliRunner?: (params: MetricsQueryParams) => Promise<LogMetricsReport>;\n}\n\nexport interface MetricsSource {\n query: MetricsQuery;\n lastTransport(): MetricsTransport | undefined;\n}\n\nfunction cliEntryPoint(): string {\n const require = createRequire(import.meta.url);\n // The published bin is CJS; the ESM build sits beside it and starts faster.\n const packageJson = require.resolve(`${PACKAGE_NAME}/package.json`);\n return path.join(path.dirname(packageJson), 'dist', 'cli.mjs');\n}\n\nfunction searchParams(params: MetricsQueryParams): string {\n return new URLSearchParams({\n groupBy: params.groupBy,\n period: params.period,\n sort: TOKEN_SORT,\n limit: String(params.limit),\n toolLimit: TOOL_LIMIT,\n }).toString();\n}\n\nasync function queryHttp(\n endpoint: string,\n params: MetricsQueryParams,\n fetchImpl: typeof fetch,\n): Promise<LogMetricsReport> {\n const response = await fetchImpl(`${endpoint}/api/metrics?${searchParams(params)}`, {\n signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),\n });\n if (!response.ok) throw new Error(`sink responded ${response.status}`);\n\n const body = (await response.json()) as LogMetricsReport & { error?: string };\n // An older daemon rejects unknown group-by values and omits the timeline.\n if (body.error || !Array.isArray(body.timeline)) throw new Error(body.error ?? 'sink is an older version');\n return body;\n}\n\nfunction runCli(params: MetricsQueryParams): Promise<LogMetricsReport> {\n return new Promise((resolve, reject) => {\n execFile(\n process.execPath,\n [\n cliEntryPoint(),\n 'logs',\n 'metrics',\n '--group-by',\n params.groupBy,\n '--period',\n params.period,\n '--sort',\n TOKEN_SORT,\n '--limit',\n String(params.limit),\n '--tool-limit',\n TOOL_LIMIT,\n ],\n { timeout: CLI_TIMEOUT_MS, maxBuffer: MAX_CLI_BUFFER },\n (error, stdout, stderr) => {\n if (error) {\n reject(new Error(stderr.trim() || error.message));\n return;\n }\n try {\n resolve(JSON.parse(stdout) as LogMetricsReport);\n } catch {\n reject(new Error('metrics CLI returned unparsable output'));\n }\n },\n );\n });\n}\n\nexport function createMetricsSource(options: MetricsSourceOptions = {}): MetricsSource {\n const fetchImpl = options.fetchImpl ?? globalThis.fetch;\n const cliRunner = options.cliRunner ?? runCli;\n let transport: MetricsTransport | undefined;\n let endpoint: string | undefined | null;\n\n const resolveEndpoint = async (): Promise<string | undefined> => {\n if (endpoint !== undefined) return endpoint ?? undefined;\n try {\n const resolved = await resolveLogSinkPort({ env: options.env, cwd: options.cwd, healthCheck: true });\n endpoint = resolved?.endpoint ?? null;\n } catch {\n endpoint = null;\n }\n return endpoint ?? undefined;\n };\n\n return {\n lastTransport: () => transport,\n async query(params) {\n const httpEndpoint = await resolveEndpoint();\n if (httpEndpoint && fetchImpl) {\n try {\n const report = await queryHttp(httpEndpoint, params, fetchImpl);\n transport = 'http';\n return report;\n } catch {\n // A stale or unhealthy daemon must not hide data the CLI can still read.\n endpoint = null;\n }\n }\n\n const report = await cliRunner(params);\n transport = 'cli';\n return report;\n },\n };\n}\n"],"mappings":"qQAgBA,MAOM,EAAa,eA0BnB,SAAS,GAAwB,CAG/B,IAAM,GAAA,EAFUA,EAAAA,cAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,IAEU,CAAC,CAAC,QAAQ,sCAA8B,EAClE,OAAOC,EAAAA,QAAK,KAAKA,EAAAA,QAAK,QAAQ,CAAW,EAAG,OAAQ,SAAS,CAC/D,CAEA,SAAS,EAAa,EAAoC,CACxD,OAAO,IAAI,gBAAgB,CACzB,QAAS,EAAO,QAChB,OAAQ,EAAO,OACf,KAAM,EACN,MAAO,OAAO,EAAO,KAAK,EAC1B,UAAW,GACb,CAAC,CAAC,CAAC,SAAS,CACd,CAEA,eAAe,EACb,EACA,EACA,EAC2B,CAC3B,IAAM,EAAW,MAAM,EAAU,GAAG,EAAS,eAAe,EAAa,CAAM,IAAK,CAClF,OAAQ,YAAY,QAAQ,GAAe,CAC7C,CAAC,EACD,GAAI,CAAC,EAAS,GAAI,MAAU,MAAM,kBAAkB,EAAS,QAAQ,EAErE,IAAM,EAAQ,MAAM,EAAS,KAAK,EAElC,GAAI,EAAK,OAAS,CAAC,MAAM,QAAQ,EAAK,QAAQ,EAAG,MAAU,MAAM,EAAK,OAAS,0BAA0B,EACzG,OAAO,CACT,CAEA,SAAS,EAAO,EAAuD,CACrE,OAAO,IAAI,SAAS,EAAS,IAAW,EACtC,EAAA,EAAA,SAAA,CACE,QAAQ,SACR,CACE,EAAc,EACd,OACA,UACA,aACA,EAAO,QACP,WACA,EAAO,OACP,SACA,EACA,UACA,OAAO,EAAO,KAAK,EACnB,eACA,GACF,EACA,CAAE,QAAS,IAAgB,UAAW,QAAe,GACpD,EAAO,EAAQ,IAAW,CACzB,GAAI,EAAO,CACT,EAAW,MAAM,EAAO,KAAK,GAAK,EAAM,OAAO,CAAC,EAChD,MACF,CACA,GAAI,CACF,EAAQ,KAAK,MAAM,CAAM,CAAqB,CAChD,MAAQ,CACN,EAAW,MAAM,wCAAwC,CAAC,CAC5D,CACF,CACF,CACF,CAAC,CACH,CAEA,SAAgB,EAAoB,EAAgC,CAAC,EAAkB,CACrF,IAAM,EAAY,EAAQ,WAAa,WAAW,MAC5C,EAAY,EAAQ,WAAa,EACnC,EACA,EAEE,EAAkB,SAAyC,CAC/D,GAAI,IAAa,IAAA,GAAW,OAAO,GAAY,IAAA,GAC/C,GAAI,CAEF,GAAW,MAAA,EADYC,EAAAA,mBAAAA,CAAmB,CAAE,IAAK,EAAQ,IAAK,IAAK,EAAQ,IAAK,YAAa,EAAK,CAAC,EAAA,EAC9E,UAAY,IACnC,MAAQ,CACN,EAAW,IACb,CACA,OAAO,GAAY,IAAA,EACrB,EAEA,MAAO,CACL,kBAAqB,EACrB,MAAM,MAAM,EAAQ,CAClB,IAAM,EAAe,MAAM,EAAgB,EAC3C,GAAI,GAAgB,EAClB,GAAI,CACF,IAAM,EAAS,MAAM,EAAU,EAAc,EAAQ,CAAS,EAE9D,MADA,GAAY,OACL,CACT,MAAQ,CAEN,EAAW,IACb,CAGF,IAAM,EAAS,MAAM,EAAU,CAAM,EAErC,MADA,GAAY,MACL,CACT,CACF,CACF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LogMetricsGroupBy, LogMetricsPeriod, LogMetricsReport } from "@agimon-ai/log-sink-mcp";
|
|
2
|
+
//#region src/metricsSource.d.ts
|
|
3
|
+
interface MetricsQueryParams {
|
|
4
|
+
groupBy: LogMetricsGroupBy;
|
|
5
|
+
period: LogMetricsPeriod;
|
|
6
|
+
limit: number;
|
|
7
|
+
}
|
|
8
|
+
type MetricsQuery = (params: MetricsQueryParams) => Promise<LogMetricsReport>;
|
|
9
|
+
/** Where the last successful answer came from, so the overlay can say so. */
|
|
10
|
+
type MetricsTransport = 'http' | 'cli';
|
|
11
|
+
interface MetricsSourceOptions {
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
cwd?: string;
|
|
14
|
+
/** Overrides transport discovery in tests. */
|
|
15
|
+
fetchImpl?: typeof fetch;
|
|
16
|
+
cliRunner?: (params: MetricsQueryParams) => Promise<LogMetricsReport>;
|
|
17
|
+
}
|
|
18
|
+
interface MetricsSource {
|
|
19
|
+
query: MetricsQuery;
|
|
20
|
+
lastTransport(): MetricsTransport | undefined;
|
|
21
|
+
}
|
|
22
|
+
declare function createMetricsSource(options?: MetricsSourceOptions): MetricsSource;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { MetricsQuery, MetricsQueryParams, MetricsSource, MetricsSourceOptions, MetricsTransport, createMetricsSource };
|
|
25
|
+
//# sourceMappingURL=metricsSource.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricsSource.d.cts","names":[],"sources":["../src/metricsSource.ts"],"mappings":";;UAyBiB;EACf,SAAS;EACT,QAAQ;EACR;;KAGU,gBAAgB,QAAQ,uBAAuB,QAAQ;;KAGvD;UAEK;EACf,MAAM,OAAO;EACb;;EAEA,mBAAmB;EACnB,aAAa,QAAQ,uBAAuB,QAAQ;;UAGrC;EACf,OAAO;EACP,iBAAiB;;iBAuEH,oBAAoB,UAAS,uBAA4B"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LogMetricsGroupBy, LogMetricsPeriod, LogMetricsReport } from "@agimon-ai/log-sink-mcp";
|
|
2
|
+
//#region src/metricsSource.d.ts
|
|
3
|
+
interface MetricsQueryParams {
|
|
4
|
+
groupBy: LogMetricsGroupBy;
|
|
5
|
+
period: LogMetricsPeriod;
|
|
6
|
+
limit: number;
|
|
7
|
+
}
|
|
8
|
+
type MetricsQuery = (params: MetricsQueryParams) => Promise<LogMetricsReport>;
|
|
9
|
+
/** Where the last successful answer came from, so the overlay can say so. */
|
|
10
|
+
type MetricsTransport = 'http' | 'cli';
|
|
11
|
+
interface MetricsSourceOptions {
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
cwd?: string;
|
|
14
|
+
/** Overrides transport discovery in tests. */
|
|
15
|
+
fetchImpl?: typeof fetch;
|
|
16
|
+
cliRunner?: (params: MetricsQueryParams) => Promise<LogMetricsReport>;
|
|
17
|
+
}
|
|
18
|
+
interface MetricsSource {
|
|
19
|
+
query: MetricsQuery;
|
|
20
|
+
lastTransport(): MetricsTransport | undefined;
|
|
21
|
+
}
|
|
22
|
+
declare function createMetricsSource(options?: MetricsSourceOptions): MetricsSource;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { MetricsQuery, MetricsQueryParams, MetricsSource, MetricsSourceOptions, MetricsTransport, createMetricsSource };
|
|
25
|
+
//# sourceMappingURL=metricsSource.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricsSource.d.mts","names":[],"sources":["../src/metricsSource.ts"],"mappings":";;UAyBiB;EACf,SAAS;EACT,QAAQ;EACR;;KAGU,gBAAgB,QAAQ,uBAAuB,QAAQ;;KAGvD;UAEK;EACf,MAAM,OAAO;EACb;;EAEA,mBAAmB;EACnB,aAAa,QAAQ,uBAAuB,QAAQ;;UAGrC;EACf,OAAO;EACP,iBAAiB;;iBAuEH,oBAAoB,UAAS,uBAA4B"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createRequire as e}from"node:module";import{execFile as t}from"node:child_process";import n from"node:path";import{resolveLogSinkPort as r}from"@agimon-ai/log-sink-mcp";const i=`total-tokens`;function a(){let t=e(import.meta.url).resolve(`@agimon-ai/log-sink-mcp/package.json`);return n.join(n.dirname(t),`dist`,`cli.mjs`)}function o(e){return new URLSearchParams({groupBy:e.groupBy,period:e.period,sort:i,limit:String(e.limit),toolLimit:`1`}).toString()}async function s(e,t,n){let r=await n(`${e}/api/metrics?${o(t)}`,{signal:AbortSignal.timeout(5e3)});if(!r.ok)throw Error(`sink responded ${r.status}`);let i=await r.json();if(i.error||!Array.isArray(i.timeline))throw Error(i.error??`sink is an older version`);return i}function c(e){return new Promise((n,r)=>{t(process.execPath,[a(),`logs`,`metrics`,`--group-by`,e.groupBy,`--period`,e.period,`--sort`,i,`--limit`,String(e.limit),`--tool-limit`,`1`],{timeout:3e4,maxBuffer:33554432},(e,t,i)=>{if(e){r(Error(i.trim()||e.message));return}try{n(JSON.parse(t))}catch{r(Error(`metrics CLI returned unparsable output`))}})})}function l(e={}){let t=e.fetchImpl??globalThis.fetch,n=e.cliRunner??c,i,a,o=async()=>{if(a!==void 0)return a??void 0;try{a=(await r({env:e.env,cwd:e.cwd,healthCheck:!0}))?.endpoint??null}catch{a=null}return a??void 0};return{lastTransport:()=>i,async query(e){let r=await o();if(r&&t)try{let n=await s(r,e,t);return i=`http`,n}catch{a=null}let c=await n(e);return i=`cli`,c}}}export{l as createMetricsSource};
|
|
2
|
+
//# sourceMappingURL=metricsSource.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricsSource.mjs","names":[],"sources":["../src/metricsSource.ts"],"sourcesContent":["/**\n * Historical metrics access for the `SPC h l` overlay.\n *\n * Two transports, in order: the running sink's HTTP API, then the installed\n * CLI as a subprocess. The sink daemon is long-lived and often lags the\n * installed package, so a daemon that rejects newer query parameters is treated\n * as unavailable rather than fatal. The reader exported by log-sink-mcp is\n * deliberately not used in-process: better-sqlite3 is synchronous and a\n * one-day window blocks for seconds, which would freeze the TUI.\n */\nimport { execFile } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { resolveLogSinkPort } from '@agimon-ai/log-sink-mcp';\nimport type { LogMetricsGroupBy, LogMetricsPeriod, LogMetricsReport } from '@agimon-ai/log-sink-mcp';\n\nconst PACKAGE_NAME = '@agimon-ai/log-sink-mcp';\nconst HTTP_TIMEOUT_MS = 5000;\nconst CLI_TIMEOUT_MS = 30000;\n/** A day of dev telemetry is already ~140k records; keep the payload bounded. */\nconst MAX_CLI_BUFFER = 32 * 1024 * 1024;\nconst TOOL_LIMIT = '1';\n/** The panel ranks consumers, so ask for the biggest rather than the most recent. */\nconst TOKEN_SORT = 'total-tokens';\n\nexport interface MetricsQueryParams {\n groupBy: LogMetricsGroupBy;\n period: LogMetricsPeriod;\n limit: number;\n}\n\nexport type MetricsQuery = (params: MetricsQueryParams) => Promise<LogMetricsReport>;\n\n/** Where the last successful answer came from, so the overlay can say so. */\nexport type MetricsTransport = 'http' | 'cli';\n\nexport interface MetricsSourceOptions {\n env?: NodeJS.ProcessEnv;\n cwd?: string;\n /** Overrides transport discovery in tests. */\n fetchImpl?: typeof fetch;\n cliRunner?: (params: MetricsQueryParams) => Promise<LogMetricsReport>;\n}\n\nexport interface MetricsSource {\n query: MetricsQuery;\n lastTransport(): MetricsTransport | undefined;\n}\n\nfunction cliEntryPoint(): string {\n const require = createRequire(import.meta.url);\n // The published bin is CJS; the ESM build sits beside it and starts faster.\n const packageJson = require.resolve(`${PACKAGE_NAME}/package.json`);\n return path.join(path.dirname(packageJson), 'dist', 'cli.mjs');\n}\n\nfunction searchParams(params: MetricsQueryParams): string {\n return new URLSearchParams({\n groupBy: params.groupBy,\n period: params.period,\n sort: TOKEN_SORT,\n limit: String(params.limit),\n toolLimit: TOOL_LIMIT,\n }).toString();\n}\n\nasync function queryHttp(\n endpoint: string,\n params: MetricsQueryParams,\n fetchImpl: typeof fetch,\n): Promise<LogMetricsReport> {\n const response = await fetchImpl(`${endpoint}/api/metrics?${searchParams(params)}`, {\n signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),\n });\n if (!response.ok) throw new Error(`sink responded ${response.status}`);\n\n const body = (await response.json()) as LogMetricsReport & { error?: string };\n // An older daemon rejects unknown group-by values and omits the timeline.\n if (body.error || !Array.isArray(body.timeline)) throw new Error(body.error ?? 'sink is an older version');\n return body;\n}\n\nfunction runCli(params: MetricsQueryParams): Promise<LogMetricsReport> {\n return new Promise((resolve, reject) => {\n execFile(\n process.execPath,\n [\n cliEntryPoint(),\n 'logs',\n 'metrics',\n '--group-by',\n params.groupBy,\n '--period',\n params.period,\n '--sort',\n TOKEN_SORT,\n '--limit',\n String(params.limit),\n '--tool-limit',\n TOOL_LIMIT,\n ],\n { timeout: CLI_TIMEOUT_MS, maxBuffer: MAX_CLI_BUFFER },\n (error, stdout, stderr) => {\n if (error) {\n reject(new Error(stderr.trim() || error.message));\n return;\n }\n try {\n resolve(JSON.parse(stdout) as LogMetricsReport);\n } catch {\n reject(new Error('metrics CLI returned unparsable output'));\n }\n },\n );\n });\n}\n\nexport function createMetricsSource(options: MetricsSourceOptions = {}): MetricsSource {\n const fetchImpl = options.fetchImpl ?? globalThis.fetch;\n const cliRunner = options.cliRunner ?? runCli;\n let transport: MetricsTransport | undefined;\n let endpoint: string | undefined | null;\n\n const resolveEndpoint = async (): Promise<string | undefined> => {\n if (endpoint !== undefined) return endpoint ?? undefined;\n try {\n const resolved = await resolveLogSinkPort({ env: options.env, cwd: options.cwd, healthCheck: true });\n endpoint = resolved?.endpoint ?? null;\n } catch {\n endpoint = null;\n }\n return endpoint ?? undefined;\n };\n\n return {\n lastTransport: () => transport,\n async query(params) {\n const httpEndpoint = await resolveEndpoint();\n if (httpEndpoint && fetchImpl) {\n try {\n const report = await queryHttp(httpEndpoint, params, fetchImpl);\n transport = 'http';\n return report;\n } catch {\n // A stale or unhealthy daemon must not hide data the CLI can still read.\n endpoint = null;\n }\n }\n\n const report = await cliRunner(params);\n transport = 'cli';\n return report;\n },\n };\n}\n"],"mappings":"gLAgBA,MAOM,EAAa,eA0BnB,SAAS,GAAwB,CAG/B,IAAM,EAFU,EAAc,YAAY,GAEhB,CAAC,CAAC,QAAQ,sCAA8B,EAClE,OAAO,EAAK,KAAK,EAAK,QAAQ,CAAW,EAAG,OAAQ,SAAS,CAC/D,CAEA,SAAS,EAAa,EAAoC,CACxD,OAAO,IAAI,gBAAgB,CACzB,QAAS,EAAO,QAChB,OAAQ,EAAO,OACf,KAAM,EACN,MAAO,OAAO,EAAO,KAAK,EAC1B,UAAW,GACb,CAAC,CAAC,CAAC,SAAS,CACd,CAEA,eAAe,EACb,EACA,EACA,EAC2B,CAC3B,IAAM,EAAW,MAAM,EAAU,GAAG,EAAS,eAAe,EAAa,CAAM,IAAK,CAClF,OAAQ,YAAY,QAAQ,GAAe,CAC7C,CAAC,EACD,GAAI,CAAC,EAAS,GAAI,MAAU,MAAM,kBAAkB,EAAS,QAAQ,EAErE,IAAM,EAAQ,MAAM,EAAS,KAAK,EAElC,GAAI,EAAK,OAAS,CAAC,MAAM,QAAQ,EAAK,QAAQ,EAAG,MAAU,MAAM,EAAK,OAAS,0BAA0B,EACzG,OAAO,CACT,CAEA,SAAS,EAAO,EAAuD,CACrE,OAAO,IAAI,SAAS,EAAS,IAAW,CACtC,EACE,QAAQ,SACR,CACE,EAAc,EACd,OACA,UACA,aACA,EAAO,QACP,WACA,EAAO,OACP,SACA,EACA,UACA,OAAO,EAAO,KAAK,EACnB,eACA,GACF,EACA,CAAE,QAAS,IAAgB,UAAW,QAAe,GACpD,EAAO,EAAQ,IAAW,CACzB,GAAI,EAAO,CACT,EAAW,MAAM,EAAO,KAAK,GAAK,EAAM,OAAO,CAAC,EAChD,MACF,CACA,GAAI,CACF,EAAQ,KAAK,MAAM,CAAM,CAAqB,CAChD,MAAQ,CACN,EAAW,MAAM,wCAAwC,CAAC,CAC5D,CACF,CACF,CACF,CAAC,CACH,CAEA,SAAgB,EAAoB,EAAgC,CAAC,EAAkB,CACrF,IAAM,EAAY,EAAQ,WAAa,WAAW,MAC5C,EAAY,EAAQ,WAAa,EACnC,EACA,EAEE,EAAkB,SAAyC,CAC/D,GAAI,IAAa,IAAA,GAAW,OAAO,GAAY,IAAA,GAC/C,GAAI,CAEF,GAAW,MADY,EAAmB,CAAE,IAAK,EAAQ,IAAK,IAAK,EAAQ,IAAK,YAAa,EAAK,CAAC,EAAA,EAC9E,UAAY,IACnC,MAAQ,CACN,EAAW,IACb,CACA,OAAO,GAAY,IAAA,EACrB,EAEA,MAAO,CACL,kBAAqB,EACrB,MAAM,MAAM,EAAQ,CAClB,IAAM,EAAe,MAAM,EAAgB,EAC3C,GAAI,GAAgB,EAClB,GAAI,CACF,IAAM,EAAS,MAAM,EAAU,EAAc,EAAQ,CAAS,EAE9D,MADA,GAAY,OACL,CACT,MAAQ,CAEN,EAAW,IACb,CAGF,IAAM,EAAS,MAAM,EAAU,CAAM,EAErC,MADA,GAAY,MACL,CACT,CACF,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./rendering.cjs");let t=require("@earendil-works/pi-tui");const n=`borderMuted`,r=`accent`,i=[`╭`,`╮`],a=[`├`,`┤`],o=[`╰`,`╯`];function s(e,n){return!e||n<=0||n<Math.min((0,t.visibleWidth)(e),8)?``:(0,t.truncateToWidth)(e,n,`…`)}var c=class{tui;theme;constructor(e,t){this.tui=e,this.theme=t}invalidate(){this.tui.requestRender()}render(t){let r=Math.max(0,Math.floor(t)),s=Math.max(0,Math.floor(this.tui.terminal?.rows??24));if(r===0||s===0)return[];let c=this.getChrome();if(s<6||r<8)return this.renderCompact(c,r,s);let l=r-2,u=l-2,d=s-6,f=this.renderBody(u,d).slice(0,d);for(;f.length<d;)f.push(``);let p=c.accent??n;return[this.borderRow(i,l,p),this.contentRow(this.headerRow(c,u),u,r,p),this.borderRow(a,l,p,n),...f.map(e=>this.contentRow(e,u,r,p)),this.borderRow(a,l,p,n),this.contentRow(this.footerRow(c,u),u,r,p),this.borderRow(o,l,p)].map(t=>e.padLine(t,r))}borderRow([e,t],r,i=n,a=i){let o=i??n;return[this.theme.fg(o,e),this.theme.fg(a??o,`─`.repeat(r)),this.theme.fg(o,t)].join(``)}contentRow(t,r,i,a=n){let o=this.theme.fg(a??n,`│`);return e.frameLine(` ${e.padLine(t,r)} `,i,o,o)}headerRow(e,n){let i=e.accent,a=i?n-2:n,o=(0,t.truncateToWidth)(e.title,Math.max(0,a),`…`),c=i?this.theme.inverse(this.theme.bold(this.theme.fg(i,` ${o} `))):this.theme.bold(this.theme.fg(r,o)),l=(0,t.visibleWidth)(o)+(i?2:0),u=s(e.breadcrumb,n-l-2);return u&&(c+=`${` `.repeat(2)}${this.theme.fg(`dim`,u)}`,l+=2+(0,t.visibleWidth)(u)),this.withRightStatus(c,l,e.headerRight,n)}footerRow(e,n){let r=e.footerHints?.length?this.hintLegend(e.footerHints,n):this.theme.fg(`dim`,(0,t.truncateToWidth)(e.footer,n,`…`));return this.withRightStatus(r,(0,t.visibleWidth)(r),e.footerRight,n)}hintLegend(e,n){let r=``,i=0;for(let[a,o]of e){let e=(0,t.visibleWidth)(a)+(0,t.visibleWidth)(o)+6;if(i+e>n)break;let s=this.theme.bg(`selectedBg`,this.theme.fg(`text`,` ${a} `));r+=`${r?` `:``}${s}${this.theme.fg(`dim`,` ${o}`)}`,i+=e}return r}withRightStatus(e,n,r,i){let a=r?(0,t.visibleWidth)(r):0;return!r||a===0||n+2+a>i?e:`${e}${` `.repeat(i-n-a)}${this.theme.fg(`dim`,r)}`}renderCompact(t,n,i){let a=Array.from({length:i},()=>``);return a[0]=this.theme.bold(this.theme.fg(r,t.title)),i>1&&(a[i-1]=this.theme.fg(`dim`,t.footer)),a.map(t=>e.padLine(e.fitLine(t,n),n))}};exports.DOOM_OVERLAY_ACCENT=`mdHeading`,exports.DoomOverlay=c;
|
|
2
|
+
//# sourceMappingURL=doomOverlay.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doomOverlay.cjs","names":["visibleWidth","truncateToWidth","padLine","frameLine","fitLine"],"sources":["../../src/tui/doomOverlay.ts"],"sourcesContent":["import type { Theme } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport { fitLine, frameLine, padLine } from './rendering.ts';\n\nconst DEFAULT_TERMINAL_ROWS = 24;\nconst FULL_CHROME_ROWS = 6;\n/** Two frame columns and two gutter columns, leaving content worth framing. */\nconst MIN_FRAMED_WIDTH = 8;\nconst GUTTER = ' ';\nconst GUTTER_COLUMNS = 2;\n/** Blank columns kept between a row's left cluster and its right-aligned status. */\nconst ROW_GAP = 2;\n/** A secondary segment thinner than this is dropped rather than shredded into a stub. */\nconst MIN_SEGMENT_WIDTH = 8;\nconst ELLIPSIS = '…';\nconst BORDER_COLOR = 'borderMuted';\nconst TEXT_COLOR = 'text';\n/** Filled key caps read as pressable, matching the doom mockups' footers. */\nconst HINT_CAP_BACKGROUND = 'selectedBg';\nconst HINT_SEPARATOR = ' ';\n/** A cap's two pad columns plus the space before its label. */\nconst HINT_CHROME_COLUMNS = 6;\nconst TITLE_COLOR = 'accent';\nconst SECONDARY_COLOR = 'dim';\nconst HORIZONTAL_BORDER = '─';\nconst VERTICAL_BORDER = '│';\nconst TOP_CORNERS = ['╭', '╮'] as const;\nconst DIVIDER_JOINTS = ['├', '┤'] as const;\nconst BOTTOM_CORNERS = ['╰', '╯'] as const;\n\nexport interface DoomOverlayTui {\n terminal?: {\n rows: number;\n columns?: number;\n };\n requestRender(force?: boolean): void;\n}\n\nexport interface DoomOverlayChrome {\n title: string;\n breadcrumb?: string;\n headerRight?: string;\n footer: string;\n footerRight?: string;\n /**\n * Key/label pairs rendered as filled caps, e.g. `[ enter ] edit subject`.\n * Takes precedence over `footer`, which stays for surfaces that want a plain\n * sentence rather than a key legend.\n */\n footerHints?: readonly (readonly [string, string])[];\n /**\n * Signature colour for the surface. When set, the frame takes it and the\n * title renders as a filled badge rather than plain accent text, matching the\n * doom mockups. Left unset, the frame stays muted and the title plain.\n */\n accent?: Parameters<Theme['fg']>[0];\n}\n\n/**\n * One signature colour for every doom overlay: the frame and the title badge.\n * Per-surface colours were tried and read as arbitrary, and they also borrowed\n * tokens that mean something else (`warning`, `syntaxNumber`), so a theme edit\n * elsewhere would have repainted a frame. Change it here to change them all.\n */\nexport const DOOM_OVERLAY_ACCENT = 'mdHeading' satisfies DoomOverlayChrome['accent'];\n\nexport const DOOM_FULLSCREEN_UI_OPTIONS = {\n overlay: true,\n overlayOptions: {\n anchor: 'top-left',\n width: '100%',\n maxHeight: '100%',\n margin: 0,\n },\n} as const;\n\n/**\n * Fits a secondary segment into `budget`, or drops it entirely when too few\n * columns remain for it to stay readable. Truncating everything to fit is what\n * makes a tight header read as one run-on string.\n */\nfunction fitSegment(text: string | undefined, budget: number): string {\n if (!text || budget <= 0) return '';\n return budget < Math.min(visibleWidth(text), MIN_SEGMENT_WIDTH) ? '' : truncateToWidth(text, budget, ELLIPSIS);\n}\n\nexport abstract class DoomOverlay implements Component {\n protected constructor(\n protected readonly tui: DoomOverlayTui,\n protected readonly theme: Theme,\n ) {}\n\n invalidate(): void {\n this.tui.requestRender();\n }\n\n render(width: number): string[] {\n const safeWidth = Math.max(0, Math.floor(width));\n const height = Math.max(0, Math.floor(this.tui.terminal?.rows ?? DEFAULT_TERMINAL_ROWS));\n if (safeWidth === 0 || height === 0) return [];\n\n const chrome = this.getChrome();\n if (height < FULL_CHROME_ROWS || safeWidth < MIN_FRAMED_WIDTH) {\n return this.renderCompact(chrome, safeWidth, height);\n }\n\n const innerWidth = safeWidth - GUTTER_COLUMNS;\n const contentWidth = innerWidth - GUTTER_COLUMNS;\n const bodyHeight = height - FULL_CHROME_ROWS;\n const body = this.renderBody(contentWidth, bodyHeight).slice(0, bodyHeight);\n while (body.length < bodyHeight) body.push('');\n\n const frame = chrome.accent ?? BORDER_COLOR;\n return [\n this.borderRow(TOP_CORNERS, innerWidth, frame),\n this.contentRow(this.headerRow(chrome, contentWidth), contentWidth, safeWidth, frame),\n this.borderRow(DIVIDER_JOINTS, innerWidth, frame, BORDER_COLOR),\n ...body.map((line) => this.contentRow(line, contentWidth, safeWidth, frame)),\n this.borderRow(DIVIDER_JOINTS, innerWidth, frame, BORDER_COLOR),\n this.contentRow(this.footerRow(chrome, contentWidth), contentWidth, safeWidth, frame),\n this.borderRow(BOTTOM_CORNERS, innerWidth, frame),\n ].map((line) => padLine(line, safeWidth));\n }\n\n protected abstract getChrome(): DoomOverlayChrome;\n\n protected abstract renderBody(width: number, height: number): string[];\n\n /**\n * Edge glyphs and the rule between them are coloured separately: the outer\n * rectangle carries the signature colour while an internal separator stays\n * muted, meeting the frame at accent-coloured junctions.\n */\n private borderRow(\n [left, right]: readonly [string, string],\n innerWidth: number,\n edge: DoomOverlayChrome['accent'] = BORDER_COLOR,\n fill: DoomOverlayChrome['accent'] = edge,\n ): string {\n const edgeColour = edge ?? BORDER_COLOR;\n return [\n this.theme.fg(edgeColour, left),\n this.theme.fg(fill ?? edgeColour, HORIZONTAL_BORDER.repeat(innerWidth)),\n this.theme.fg(edgeColour, right),\n ].join('');\n }\n\n private contentRow(\n content: string,\n contentWidth: number,\n width: number,\n colour: DoomOverlayChrome['accent'] = BORDER_COLOR,\n ): string {\n const edge = this.theme.fg(colour ?? BORDER_COLOR, VERTICAL_BORDER);\n return frameLine(`${GUTTER}${padLine(content, contentWidth)}${GUTTER}`, width, edge, edge);\n }\n\n private headerRow(chrome: DoomOverlayChrome, width: number): string {\n // A signature colour turns the title into a filled badge, as the mockups\n // show; without one it stays plain accent text.\n const accent = chrome.accent;\n const titleBudget = accent ? width - GUTTER_COLUMNS : width;\n const title = truncateToWidth(chrome.title, Math.max(0, titleBudget), ELLIPSIS);\n let left = accent\n ? this.theme.inverse(this.theme.bold(this.theme.fg(accent, ` ${title} `)))\n : this.theme.bold(this.theme.fg(TITLE_COLOR, title));\n let leftWidth = visibleWidth(title) + (accent ? GUTTER_COLUMNS : 0);\n\n // The breadcrumb claims room before the right status: it says where you are,\n // which outranks ambient context when the terminal is narrow.\n const breadcrumb = fitSegment(chrome.breadcrumb, width - leftWidth - ROW_GAP);\n if (breadcrumb) {\n left += `${' '.repeat(ROW_GAP)}${this.theme.fg(SECONDARY_COLOR, breadcrumb)}`;\n leftWidth += ROW_GAP + visibleWidth(breadcrumb);\n }\n return this.withRightStatus(left, leftWidth, chrome.headerRight, width);\n }\n\n private footerRow(chrome: DoomOverlayChrome, width: number): string {\n const footer = chrome.footerHints?.length\n ? this.hintLegend(chrome.footerHints, width)\n : this.theme.fg(SECONDARY_COLOR, truncateToWidth(chrome.footer, width, ELLIPSIS));\n return this.withRightStatus(footer, visibleWidth(footer), chrome.footerRight, width);\n }\n\n /**\n * Key legend with each key in a filled cap. Every segment sets its own colour\n * so the caps survive: a cap's reset would otherwise strip the surrounding\n * dim from the labels that follow it.\n */\n private hintLegend(hints: readonly (readonly [string, string])[], width: number): string {\n let legend = '';\n let used = 0;\n for (const [key, label] of hints) {\n const segmentWidth = visibleWidth(key) + visibleWidth(label) + HINT_CHROME_COLUMNS;\n if (used + segmentWidth > width) break;\n const cap = this.theme.bg(HINT_CAP_BACKGROUND, this.theme.fg(TEXT_COLOR, ` ${key} `));\n legend += `${legend ? HINT_SEPARATOR : ''}${cap}${this.theme.fg(SECONDARY_COLOR, ` ${label}`)}`;\n used += segmentWidth;\n }\n return legend;\n }\n\n /**\n * Right-aligns a status cluster, dropping it whole when the row cannot spare\n * the columns. A clipped status reads as a stub (`doom-log…`) that carries\n * less than the blank space it costs.\n */\n private withRightStatus(left: string, leftWidth: number, right: string | undefined, width: number): string {\n const statusWidth = right ? visibleWidth(right) : 0;\n if (!right || statusWidth === 0 || leftWidth + ROW_GAP + statusWidth > width) return left;\n return `${left}${' '.repeat(width - leftWidth - statusWidth)}${this.theme.fg(SECONDARY_COLOR, right)}`;\n }\n\n private renderCompact(chrome: DoomOverlayChrome, width: number, height: number): string[] {\n const lines = Array.from({ length: height }, () => '');\n lines[0] = this.theme.bold(this.theme.fg(TITLE_COLOR, chrome.title));\n if (height > 1) lines[height - 1] = this.theme.fg(SECONDARY_COLOR, chrome.footer);\n return lines.map((line) => padLine(fitLine(line, width), width));\n }\n}\n"],"mappings":"2EAIA,MAWM,EAAe,cAOf,EAAc,SAId,EAAc,CAAC,IAAK,GAAG,EACvB,EAAiB,CAAC,IAAK,GAAG,EAC1B,EAAiB,CAAC,IAAK,GAAG,EAqDhC,SAAS,EAAW,EAA0B,EAAwB,CAEpE,MADI,CAAC,GAAQ,GAAU,GAChB,EAAS,KAAK,KAAA,EAAIA,EAAAA,aAAAA,CAAa,CAAI,EAAG,CAAiB,EAD7B,IACiC,EAAKC,EAAAA,gBAAAA,CAAgB,EAAM,EAAQ,GAAQ,CAC/G,CAEA,IAAsB,EAAtB,KAAuD,CAEhC,IACA,MAFrB,YACE,EACA,EACA,CAFmB,KAAA,IAAA,EACA,KAAA,MAAA,CAClB,CAEH,YAAmB,CACjB,KAAK,IAAI,cAAc,CACzB,CAEA,OAAO,EAAyB,CAC9B,IAAM,EAAY,KAAK,IAAI,EAAG,KAAK,MAAM,CAAK,CAAC,EACzC,EAAS,KAAK,IAAI,EAAG,KAAK,MAAM,KAAK,IAAI,UAAU,MAAQ,EAAqB,CAAC,EACvF,GAAI,IAAc,GAAK,IAAW,EAAG,MAAO,CAAC,EAE7C,IAAM,EAAS,KAAK,UAAU,EAC9B,GAAI,EAAS,GAAoB,EAAY,EAC3C,OAAO,KAAK,cAAc,EAAQ,EAAW,CAAM,EAGrD,IAAM,EAAa,EAAY,EACzB,EAAe,EAAa,EAC5B,EAAa,EAAS,EACtB,EAAO,KAAK,WAAW,EAAc,CAAU,CAAC,CAAC,MAAM,EAAG,CAAU,EAC1E,KAAO,EAAK,OAAS,GAAY,EAAK,KAAK,EAAE,EAE7C,IAAM,EAAQ,EAAO,QAAU,EAC/B,MAAO,CACL,KAAK,UAAU,EAAa,EAAY,CAAK,EAC7C,KAAK,WAAW,KAAK,UAAU,EAAQ,CAAY,EAAG,EAAc,EAAW,CAAK,EACpF,KAAK,UAAU,EAAgB,EAAY,EAAO,CAAY,EAC9D,GAAG,EAAK,IAAK,GAAS,KAAK,WAAW,EAAM,EAAc,EAAW,CAAK,CAAC,EAC3E,KAAK,UAAU,EAAgB,EAAY,EAAO,CAAY,EAC9D,KAAK,WAAW,KAAK,UAAU,EAAQ,CAAY,EAAG,EAAc,EAAW,CAAK,EACpF,KAAK,UAAU,EAAgB,EAAY,CAAK,CAClD,CAAC,CAAC,IAAK,GAASC,EAAAA,QAAQ,EAAM,CAAS,CAAC,CAC1C,CAWA,UACE,CAAC,EAAM,GACP,EACA,EAAoC,EACpC,EAAoC,EAC5B,CACR,IAAM,EAAa,GAAQ,EAC3B,MAAO,CACL,KAAK,MAAM,GAAG,EAAY,CAAI,EAC9B,KAAK,MAAM,GAAG,GAAQ,EAAY,IAAkB,OAAO,CAAU,CAAC,EACtE,KAAK,MAAM,GAAG,EAAY,CAAK,CACjC,CAAC,CAAC,KAAK,EAAE,CACX,CAEA,WACE,EACA,EACA,EACA,EAAsC,EAC9B,CACR,IAAM,EAAO,KAAK,MAAM,GAAG,GAAU,EAAc,GAAe,EAClE,OAAOC,EAAAA,UAAU,IAAYD,EAAAA,QAAQ,EAAS,CAAY,KAAc,EAAO,EAAM,CAAI,CAC3F,CAEA,UAAkB,EAA2B,EAAuB,CAGlE,IAAM,EAAS,EAAO,OAChB,EAAc,EAAS,EAAQ,EAAiB,EAChD,GAAA,EAAQD,EAAAA,gBAAAA,CAAgB,EAAO,MAAO,KAAK,IAAI,EAAG,CAAW,EAAG,GAAQ,EAC1E,EAAO,EACP,KAAK,MAAM,QAAQ,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,EAAQ,IAAI,EAAM,EAAE,CAAC,CAAC,EACvE,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,EAAa,CAAK,CAAC,EACjD,GAAA,EAAYD,EAAAA,aAAAA,CAAa,CAAK,GAAK,EAAS,EAAiB,GAI3D,EAAa,EAAW,EAAO,WAAY,EAAQ,EAAY,CAAO,EAK5E,OAJI,IACF,GAAQ,GAAG,IAAI,OAAO,CAAO,IAAI,KAAK,MAAM,GAAG,MAAiB,CAAU,IAC1E,GAAa,GAAA,EAAUA,EAAAA,aAAAA,CAAa,CAAU,GAEzC,KAAK,gBAAgB,EAAM,EAAW,EAAO,YAAa,CAAK,CACxE,CAEA,UAAkB,EAA2B,EAAuB,CAClE,IAAM,EAAS,EAAO,aAAa,OAC/B,KAAK,WAAW,EAAO,YAAa,CAAK,EACzC,KAAK,MAAM,GAAG,OAAA,EAAiBC,EAAAA,gBAAAA,CAAgB,EAAO,OAAQ,EAAO,GAAQ,CAAC,EAClF,OAAO,KAAK,gBAAgB,GAAA,EAAQD,EAAAA,aAAAA,CAAa,CAAM,EAAG,EAAO,YAAa,CAAK,CACrF,CAOA,WAAmB,EAA+C,EAAuB,CACvF,IAAI,EAAS,GACT,EAAO,EACX,IAAK,GAAM,CAAC,EAAK,KAAU,EAAO,CAChC,IAAM,GAAA,EAAeA,EAAAA,aAAAA,CAAa,CAAG,GAAA,EAAIA,EAAAA,aAAAA,CAAa,CAAK,EAAI,EAC/D,GAAI,EAAO,EAAe,EAAO,MACjC,IAAM,EAAM,KAAK,MAAM,GAAG,aAAqB,KAAK,MAAM,GAAG,OAAY,IAAI,EAAI,EAAE,CAAC,EACpF,GAAU,GAAG,EAAS,MAAiB,KAAK,IAAM,KAAK,MAAM,GAAG,MAAiB,IAAI,GAAO,IAC5F,GAAQ,CACV,CACA,OAAO,CACT,CAOA,gBAAwB,EAAc,EAAmB,EAA2B,EAAuB,CACzG,IAAM,EAAc,GAAA,EAAQA,EAAAA,aAAAA,CAAa,CAAK,EAAI,EAElD,MADI,CAAC,GAAS,IAAgB,GAAK,EAAY,EAAU,EAAc,EAAc,EAC9E,GAAG,IAAO,IAAI,OAAO,EAAQ,EAAY,CAAW,IAAI,KAAK,MAAM,GAAG,MAAiB,CAAK,GACrG,CAEA,cAAsB,EAA2B,EAAe,EAA0B,CACxF,IAAM,EAAQ,MAAM,KAAK,CAAE,OAAQ,CAAO,MAAS,EAAE,EAGrD,MAFA,GAAM,GAAK,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,EAAa,EAAO,KAAK,CAAC,EAC/D,EAAS,IAAG,EAAM,EAAS,GAAK,KAAK,MAAM,GAAG,MAAiB,EAAO,MAAM,GACzE,EAAM,IAAK,GAASE,EAAAA,QAAQE,EAAAA,QAAQ,EAAM,CAAK,EAAG,CAAK,CAAC,CACjE,CACF"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Component } from "@earendil-works/pi-tui";
|
|
3
|
+
//#region src/tui/doomOverlay.d.ts
|
|
4
|
+
interface DoomOverlayTui {
|
|
5
|
+
terminal?: {
|
|
6
|
+
rows: number;
|
|
7
|
+
columns?: number;
|
|
8
|
+
};
|
|
9
|
+
requestRender(force?: boolean): void;
|
|
10
|
+
}
|
|
11
|
+
interface DoomOverlayChrome {
|
|
12
|
+
title: string;
|
|
13
|
+
breadcrumb?: string;
|
|
14
|
+
headerRight?: string;
|
|
15
|
+
footer: string;
|
|
16
|
+
footerRight?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Key/label pairs rendered as filled caps, e.g. `[ enter ] edit subject`.
|
|
19
|
+
* Takes precedence over `footer`, which stays for surfaces that want a plain
|
|
20
|
+
* sentence rather than a key legend.
|
|
21
|
+
*/
|
|
22
|
+
footerHints?: readonly (readonly [string, string])[];
|
|
23
|
+
/**
|
|
24
|
+
* Signature colour for the surface. When set, the frame takes it and the
|
|
25
|
+
* title renders as a filled badge rather than plain accent text, matching the
|
|
26
|
+
* doom mockups. Left unset, the frame stays muted and the title plain.
|
|
27
|
+
*/
|
|
28
|
+
accent?: Parameters<Theme['fg']>[0];
|
|
29
|
+
}
|
|
30
|
+
declare abstract class DoomOverlay implements Component {
|
|
31
|
+
protected readonly tui: DoomOverlayTui;
|
|
32
|
+
protected readonly theme: Theme;
|
|
33
|
+
protected constructor(tui: DoomOverlayTui, theme: Theme);
|
|
34
|
+
invalidate(): void;
|
|
35
|
+
render(width: number): string[];
|
|
36
|
+
protected abstract getChrome(): DoomOverlayChrome;
|
|
37
|
+
protected abstract renderBody(width: number, height: number): string[];
|
|
38
|
+
/**
|
|
39
|
+
* Edge glyphs and the rule between them are coloured separately: the outer
|
|
40
|
+
* rectangle carries the signature colour while an internal separator stays
|
|
41
|
+
* muted, meeting the frame at accent-coloured junctions.
|
|
42
|
+
*/
|
|
43
|
+
private borderRow;
|
|
44
|
+
private contentRow;
|
|
45
|
+
private headerRow;
|
|
46
|
+
private footerRow;
|
|
47
|
+
/**
|
|
48
|
+
* Key legend with each key in a filled cap. Every segment sets its own colour
|
|
49
|
+
* so the caps survive: a cap's reset would otherwise strip the surrounding
|
|
50
|
+
* dim from the labels that follow it.
|
|
51
|
+
*/
|
|
52
|
+
private hintLegend;
|
|
53
|
+
/**
|
|
54
|
+
* Right-aligns a status cluster, dropping it whole when the row cannot spare
|
|
55
|
+
* the columns. A clipped status reads as a stub (`doom-log…`) that carries
|
|
56
|
+
* less than the blank space it costs.
|
|
57
|
+
*/
|
|
58
|
+
private withRightStatus;
|
|
59
|
+
private renderCompact;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { DoomOverlay, DoomOverlayChrome, DoomOverlayTui };
|
|
63
|
+
//# sourceMappingURL=doomOverlay.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doomOverlay.d.cts","names":[],"sources":["../../src/tui/doomOverlay.ts"],"mappings":";;;UA8BiB;EACf;IACE;IACA;;EAEF,cAAc;;UAGC;EACf;EACA;EACA;EACA;EACA;;;;;;EAMA;;;;;;EAMA,SAAS,WAAW;;uBA+BA,uBAAuB;qBAEtB,KAAK;qBACL,OAAO;YAFnB,YACY,KAAK,gBACL,OAAO;EAG5B;EAIA,OAAO;qBA4BY,aAAa;qBAEb,WAAW,eAAe;;;;;;UAOrC;UAcA;UAUA;UAqBA;;;;;;UAYA;;;;;;UAkBA;UAMA"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
//#region src/tui/doomOverlay.d.ts
|
|
4
|
+
interface DoomOverlayTui {
|
|
5
|
+
terminal?: {
|
|
6
|
+
rows: number;
|
|
7
|
+
columns?: number;
|
|
8
|
+
};
|
|
9
|
+
requestRender(force?: boolean): void;
|
|
10
|
+
}
|
|
11
|
+
interface DoomOverlayChrome {
|
|
12
|
+
title: string;
|
|
13
|
+
breadcrumb?: string;
|
|
14
|
+
headerRight?: string;
|
|
15
|
+
footer: string;
|
|
16
|
+
footerRight?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Key/label pairs rendered as filled caps, e.g. `[ enter ] edit subject`.
|
|
19
|
+
* Takes precedence over `footer`, which stays for surfaces that want a plain
|
|
20
|
+
* sentence rather than a key legend.
|
|
21
|
+
*/
|
|
22
|
+
footerHints?: readonly (readonly [string, string])[];
|
|
23
|
+
/**
|
|
24
|
+
* Signature colour for the surface. When set, the frame takes it and the
|
|
25
|
+
* title renders as a filled badge rather than plain accent text, matching the
|
|
26
|
+
* doom mockups. Left unset, the frame stays muted and the title plain.
|
|
27
|
+
*/
|
|
28
|
+
accent?: Parameters<Theme['fg']>[0];
|
|
29
|
+
}
|
|
30
|
+
declare abstract class DoomOverlay implements Component {
|
|
31
|
+
protected readonly tui: DoomOverlayTui;
|
|
32
|
+
protected readonly theme: Theme;
|
|
33
|
+
protected constructor(tui: DoomOverlayTui, theme: Theme);
|
|
34
|
+
invalidate(): void;
|
|
35
|
+
render(width: number): string[];
|
|
36
|
+
protected abstract getChrome(): DoomOverlayChrome;
|
|
37
|
+
protected abstract renderBody(width: number, height: number): string[];
|
|
38
|
+
/**
|
|
39
|
+
* Edge glyphs and the rule between them are coloured separately: the outer
|
|
40
|
+
* rectangle carries the signature colour while an internal separator stays
|
|
41
|
+
* muted, meeting the frame at accent-coloured junctions.
|
|
42
|
+
*/
|
|
43
|
+
private borderRow;
|
|
44
|
+
private contentRow;
|
|
45
|
+
private headerRow;
|
|
46
|
+
private footerRow;
|
|
47
|
+
/**
|
|
48
|
+
* Key legend with each key in a filled cap. Every segment sets its own colour
|
|
49
|
+
* so the caps survive: a cap's reset would otherwise strip the surrounding
|
|
50
|
+
* dim from the labels that follow it.
|
|
51
|
+
*/
|
|
52
|
+
private hintLegend;
|
|
53
|
+
/**
|
|
54
|
+
* Right-aligns a status cluster, dropping it whole when the row cannot spare
|
|
55
|
+
* the columns. A clipped status reads as a stub (`doom-log…`) that carries
|
|
56
|
+
* less than the blank space it costs.
|
|
57
|
+
*/
|
|
58
|
+
private withRightStatus;
|
|
59
|
+
private renderCompact;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { DoomOverlay, DoomOverlayChrome, DoomOverlayTui };
|
|
63
|
+
//# sourceMappingURL=doomOverlay.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doomOverlay.d.mts","names":[],"sources":["../../src/tui/doomOverlay.ts"],"mappings":";;;UA8BiB;EACf;IACE;IACA;;EAEF,cAAc;;UAGC;EACf;EACA;EACA;EACA;EACA;;;;;;EAMA;;;;;;EAMA,SAAS,WAAW;;uBA+BA,uBAAuB;qBAEtB,KAAK;qBACL,OAAO;YAFnB,YACY,KAAK,gBACL,OAAO;EAG5B;EAIA,OAAO;qBA4BY,aAAa;qBAEb,WAAW,eAAe;;;;;;UAOrC;UAcA;UAUA;UAqBA;;;;;;UAYA;;;;;;UAkBA;UAMA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{fitLine as e,frameLine as t,padLine as n}from"./rendering.mjs";import{truncateToWidth as r,visibleWidth as i}from"@earendil-works/pi-tui";const a=`borderMuted`,o=`accent`,s=[`╭`,`╮`],c=[`├`,`┤`],l=[`╰`,`╯`],u=`mdHeading`;function d(e,t){return!e||t<=0||t<Math.min(i(e),8)?``:r(e,t,`…`)}var f=class{tui;theme;constructor(e,t){this.tui=e,this.theme=t}invalidate(){this.tui.requestRender()}render(e){let t=Math.max(0,Math.floor(e)),r=Math.max(0,Math.floor(this.tui.terminal?.rows??24));if(t===0||r===0)return[];let i=this.getChrome();if(r<6||t<8)return this.renderCompact(i,t,r);let o=t-2,u=o-2,d=r-6,f=this.renderBody(u,d).slice(0,d);for(;f.length<d;)f.push(``);let p=i.accent??a;return[this.borderRow(s,o,p),this.contentRow(this.headerRow(i,u),u,t,p),this.borderRow(c,o,p,a),...f.map(e=>this.contentRow(e,u,t,p)),this.borderRow(c,o,p,a),this.contentRow(this.footerRow(i,u),u,t,p),this.borderRow(l,o,p)].map(e=>n(e,t))}borderRow([e,t],n,r=a,i=r){let o=r??a;return[this.theme.fg(o,e),this.theme.fg(i??o,`─`.repeat(n)),this.theme.fg(o,t)].join(``)}contentRow(e,r,i,o=a){let s=this.theme.fg(o??a,`│`);return t(` ${n(e,r)} `,i,s,s)}headerRow(e,t){let n=e.accent,a=n?t-2:t,s=r(e.title,Math.max(0,a),`…`),c=n?this.theme.inverse(this.theme.bold(this.theme.fg(n,` ${s} `))):this.theme.bold(this.theme.fg(o,s)),l=i(s)+(n?2:0),u=d(e.breadcrumb,t-l-2);return u&&(c+=`${` `.repeat(2)}${this.theme.fg(`dim`,u)}`,l+=2+i(u)),this.withRightStatus(c,l,e.headerRight,t)}footerRow(e,t){let n=e.footerHints?.length?this.hintLegend(e.footerHints,t):this.theme.fg(`dim`,r(e.footer,t,`…`));return this.withRightStatus(n,i(n),e.footerRight,t)}hintLegend(e,t){let n=``,r=0;for(let[a,o]of e){let e=i(a)+i(o)+6;if(r+e>t)break;let s=this.theme.bg(`selectedBg`,this.theme.fg(`text`,` ${a} `));n+=`${n?` `:``}${s}${this.theme.fg(`dim`,` ${o}`)}`,r+=e}return n}withRightStatus(e,t,n,r){let a=n?i(n):0;return!n||a===0||t+2+a>r?e:`${e}${` `.repeat(r-t-a)}${this.theme.fg(`dim`,n)}`}renderCompact(t,r,i){let a=Array.from({length:i},()=>``);return a[0]=this.theme.bold(this.theme.fg(o,t.title)),i>1&&(a[i-1]=this.theme.fg(`dim`,t.footer)),a.map(t=>n(e(t,r),r))}};export{u as DOOM_OVERLAY_ACCENT,f as DoomOverlay};
|
|
2
|
+
//# sourceMappingURL=doomOverlay.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doomOverlay.mjs","names":[],"sources":["../../src/tui/doomOverlay.ts"],"sourcesContent":["import type { Theme } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport { fitLine, frameLine, padLine } from './rendering.ts';\n\nconst DEFAULT_TERMINAL_ROWS = 24;\nconst FULL_CHROME_ROWS = 6;\n/** Two frame columns and two gutter columns, leaving content worth framing. */\nconst MIN_FRAMED_WIDTH = 8;\nconst GUTTER = ' ';\nconst GUTTER_COLUMNS = 2;\n/** Blank columns kept between a row's left cluster and its right-aligned status. */\nconst ROW_GAP = 2;\n/** A secondary segment thinner than this is dropped rather than shredded into a stub. */\nconst MIN_SEGMENT_WIDTH = 8;\nconst ELLIPSIS = '…';\nconst BORDER_COLOR = 'borderMuted';\nconst TEXT_COLOR = 'text';\n/** Filled key caps read as pressable, matching the doom mockups' footers. */\nconst HINT_CAP_BACKGROUND = 'selectedBg';\nconst HINT_SEPARATOR = ' ';\n/** A cap's two pad columns plus the space before its label. */\nconst HINT_CHROME_COLUMNS = 6;\nconst TITLE_COLOR = 'accent';\nconst SECONDARY_COLOR = 'dim';\nconst HORIZONTAL_BORDER = '─';\nconst VERTICAL_BORDER = '│';\nconst TOP_CORNERS = ['╭', '╮'] as const;\nconst DIVIDER_JOINTS = ['├', '┤'] as const;\nconst BOTTOM_CORNERS = ['╰', '╯'] as const;\n\nexport interface DoomOverlayTui {\n terminal?: {\n rows: number;\n columns?: number;\n };\n requestRender(force?: boolean): void;\n}\n\nexport interface DoomOverlayChrome {\n title: string;\n breadcrumb?: string;\n headerRight?: string;\n footer: string;\n footerRight?: string;\n /**\n * Key/label pairs rendered as filled caps, e.g. `[ enter ] edit subject`.\n * Takes precedence over `footer`, which stays for surfaces that want a plain\n * sentence rather than a key legend.\n */\n footerHints?: readonly (readonly [string, string])[];\n /**\n * Signature colour for the surface. When set, the frame takes it and the\n * title renders as a filled badge rather than plain accent text, matching the\n * doom mockups. Left unset, the frame stays muted and the title plain.\n */\n accent?: Parameters<Theme['fg']>[0];\n}\n\n/**\n * One signature colour for every doom overlay: the frame and the title badge.\n * Per-surface colours were tried and read as arbitrary, and they also borrowed\n * tokens that mean something else (`warning`, `syntaxNumber`), so a theme edit\n * elsewhere would have repainted a frame. Change it here to change them all.\n */\nexport const DOOM_OVERLAY_ACCENT = 'mdHeading' satisfies DoomOverlayChrome['accent'];\n\nexport const DOOM_FULLSCREEN_UI_OPTIONS = {\n overlay: true,\n overlayOptions: {\n anchor: 'top-left',\n width: '100%',\n maxHeight: '100%',\n margin: 0,\n },\n} as const;\n\n/**\n * Fits a secondary segment into `budget`, or drops it entirely when too few\n * columns remain for it to stay readable. Truncating everything to fit is what\n * makes a tight header read as one run-on string.\n */\nfunction fitSegment(text: string | undefined, budget: number): string {\n if (!text || budget <= 0) return '';\n return budget < Math.min(visibleWidth(text), MIN_SEGMENT_WIDTH) ? '' : truncateToWidth(text, budget, ELLIPSIS);\n}\n\nexport abstract class DoomOverlay implements Component {\n protected constructor(\n protected readonly tui: DoomOverlayTui,\n protected readonly theme: Theme,\n ) {}\n\n invalidate(): void {\n this.tui.requestRender();\n }\n\n render(width: number): string[] {\n const safeWidth = Math.max(0, Math.floor(width));\n const height = Math.max(0, Math.floor(this.tui.terminal?.rows ?? DEFAULT_TERMINAL_ROWS));\n if (safeWidth === 0 || height === 0) return [];\n\n const chrome = this.getChrome();\n if (height < FULL_CHROME_ROWS || safeWidth < MIN_FRAMED_WIDTH) {\n return this.renderCompact(chrome, safeWidth, height);\n }\n\n const innerWidth = safeWidth - GUTTER_COLUMNS;\n const contentWidth = innerWidth - GUTTER_COLUMNS;\n const bodyHeight = height - FULL_CHROME_ROWS;\n const body = this.renderBody(contentWidth, bodyHeight).slice(0, bodyHeight);\n while (body.length < bodyHeight) body.push('');\n\n const frame = chrome.accent ?? BORDER_COLOR;\n return [\n this.borderRow(TOP_CORNERS, innerWidth, frame),\n this.contentRow(this.headerRow(chrome, contentWidth), contentWidth, safeWidth, frame),\n this.borderRow(DIVIDER_JOINTS, innerWidth, frame, BORDER_COLOR),\n ...body.map((line) => this.contentRow(line, contentWidth, safeWidth, frame)),\n this.borderRow(DIVIDER_JOINTS, innerWidth, frame, BORDER_COLOR),\n this.contentRow(this.footerRow(chrome, contentWidth), contentWidth, safeWidth, frame),\n this.borderRow(BOTTOM_CORNERS, innerWidth, frame),\n ].map((line) => padLine(line, safeWidth));\n }\n\n protected abstract getChrome(): DoomOverlayChrome;\n\n protected abstract renderBody(width: number, height: number): string[];\n\n /**\n * Edge glyphs and the rule between them are coloured separately: the outer\n * rectangle carries the signature colour while an internal separator stays\n * muted, meeting the frame at accent-coloured junctions.\n */\n private borderRow(\n [left, right]: readonly [string, string],\n innerWidth: number,\n edge: DoomOverlayChrome['accent'] = BORDER_COLOR,\n fill: DoomOverlayChrome['accent'] = edge,\n ): string {\n const edgeColour = edge ?? BORDER_COLOR;\n return [\n this.theme.fg(edgeColour, left),\n this.theme.fg(fill ?? edgeColour, HORIZONTAL_BORDER.repeat(innerWidth)),\n this.theme.fg(edgeColour, right),\n ].join('');\n }\n\n private contentRow(\n content: string,\n contentWidth: number,\n width: number,\n colour: DoomOverlayChrome['accent'] = BORDER_COLOR,\n ): string {\n const edge = this.theme.fg(colour ?? BORDER_COLOR, VERTICAL_BORDER);\n return frameLine(`${GUTTER}${padLine(content, contentWidth)}${GUTTER}`, width, edge, edge);\n }\n\n private headerRow(chrome: DoomOverlayChrome, width: number): string {\n // A signature colour turns the title into a filled badge, as the mockups\n // show; without one it stays plain accent text.\n const accent = chrome.accent;\n const titleBudget = accent ? width - GUTTER_COLUMNS : width;\n const title = truncateToWidth(chrome.title, Math.max(0, titleBudget), ELLIPSIS);\n let left = accent\n ? this.theme.inverse(this.theme.bold(this.theme.fg(accent, ` ${title} `)))\n : this.theme.bold(this.theme.fg(TITLE_COLOR, title));\n let leftWidth = visibleWidth(title) + (accent ? GUTTER_COLUMNS : 0);\n\n // The breadcrumb claims room before the right status: it says where you are,\n // which outranks ambient context when the terminal is narrow.\n const breadcrumb = fitSegment(chrome.breadcrumb, width - leftWidth - ROW_GAP);\n if (breadcrumb) {\n left += `${' '.repeat(ROW_GAP)}${this.theme.fg(SECONDARY_COLOR, breadcrumb)}`;\n leftWidth += ROW_GAP + visibleWidth(breadcrumb);\n }\n return this.withRightStatus(left, leftWidth, chrome.headerRight, width);\n }\n\n private footerRow(chrome: DoomOverlayChrome, width: number): string {\n const footer = chrome.footerHints?.length\n ? this.hintLegend(chrome.footerHints, width)\n : this.theme.fg(SECONDARY_COLOR, truncateToWidth(chrome.footer, width, ELLIPSIS));\n return this.withRightStatus(footer, visibleWidth(footer), chrome.footerRight, width);\n }\n\n /**\n * Key legend with each key in a filled cap. Every segment sets its own colour\n * so the caps survive: a cap's reset would otherwise strip the surrounding\n * dim from the labels that follow it.\n */\n private hintLegend(hints: readonly (readonly [string, string])[], width: number): string {\n let legend = '';\n let used = 0;\n for (const [key, label] of hints) {\n const segmentWidth = visibleWidth(key) + visibleWidth(label) + HINT_CHROME_COLUMNS;\n if (used + segmentWidth > width) break;\n const cap = this.theme.bg(HINT_CAP_BACKGROUND, this.theme.fg(TEXT_COLOR, ` ${key} `));\n legend += `${legend ? HINT_SEPARATOR : ''}${cap}${this.theme.fg(SECONDARY_COLOR, ` ${label}`)}`;\n used += segmentWidth;\n }\n return legend;\n }\n\n /**\n * Right-aligns a status cluster, dropping it whole when the row cannot spare\n * the columns. A clipped status reads as a stub (`doom-log…`) that carries\n * less than the blank space it costs.\n */\n private withRightStatus(left: string, leftWidth: number, right: string | undefined, width: number): string {\n const statusWidth = right ? visibleWidth(right) : 0;\n if (!right || statusWidth === 0 || leftWidth + ROW_GAP + statusWidth > width) return left;\n return `${left}${' '.repeat(width - leftWidth - statusWidth)}${this.theme.fg(SECONDARY_COLOR, right)}`;\n }\n\n private renderCompact(chrome: DoomOverlayChrome, width: number, height: number): string[] {\n const lines = Array.from({ length: height }, () => '');\n lines[0] = this.theme.bold(this.theme.fg(TITLE_COLOR, chrome.title));\n if (height > 1) lines[height - 1] = this.theme.fg(SECONDARY_COLOR, chrome.footer);\n return lines.map((line) => padLine(fitLine(line, width), width));\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./doomOverlay.cjs");let t=require("@earendil-works/pi-tui");const n=[`session`,`agent`,`workflow-name`,`step`],r=[`day`,`week`,`month`,`all`],i=n[0],a=r[0],o=`TOOL LATENCY · p95 duration_ms by tool.name`,s=`EVENT VOLUME · by log record name`,c=`RECENT ERRORS`,l=`SINK STATUS`,u=`COLLECTION`,d=`AGENT_TELEMETRY_DISABLED`,f=`this session`,p=[0,1,2],m=1e3,h=1e6,g=1e3;function _(e,n){let r=(0,t.visibleWidth)(e);return r>=n?(0,t.truncateToWidth)(e,n,`…`):e+` `.repeat(n-r)}function v(e,n){let r=(0,t.visibleWidth)(e);return r>=n?(0,t.truncateToWidth)(e,n):` `.repeat(n-r)+e}function y(e){return e.toLocaleString(`en-US`)}function b(e){return e>=h?`${(e/h).toFixed(1)}M`:e>=g?`${Math.round(e/g)}k`:String(e)}function x(e){return e===void 0?`—`:e>=m?`${(e/m).toFixed(2)}s`:`${Math.round(e)}ms`}function S(e){return new Date(e).toTimeString().slice(0,5)}function C(e,t,n){let r=Math.min(n,Math.max(+(e>0),Math.round(e/t*n)));return`█`.repeat(r)+`░`.repeat(n-r)}function w(e,n,r){return(0,t.truncateToWidth)(_(e,14)+n,r)}function T(e,t,n,r){return[_(e,r),_(t,r),_(n,r)]}function E(e,t){let n=Math.max(1,Math.floor(t/6)),r=e.toolLatency[0],i=(e.errors/Math.max(e.events,1)*100).toFixed(2),a=[T(`EVENTS`,y(e.events),f,n),T(`ERRORS`,y(e.errors),`${i}%`,n),T(`TOOL CALLS`,y(e.toolCalls),`${e.failedToolCalls} failed`,n),T(`P95 TOOL`,x(r?.p95Ms),r?.name??`—`,n),T(`TOKENS`,b(e.tokens.total),`↑${b(e.tokens.input)} ↓${b(e.tokens.output)}`,n),T(`COST`,`$${e.cost.toFixed(2)}`,f,n)];return p.map(e=>_(a.map(t=>t[e]??``).join(``),t))}function D(e,t){let n=t>0?e/t:0;return n>=.66?`error`:n>=.33?`warning`:`success`}function O(e,n,r){let i=e.toolLatency.slice(0,6),a=Math.max(1,n-10-8-1),o=Math.max(...i.map(e=>e.p95Ms??0),1);return i.map(e=>{let i=r.fg(D(e.p95Ms??0,o),C(e.p95Ms??0,o,a));return(0,t.truncateToWidth)(`${_(e.name,10)}${i} ${v(x(e.p95Ms),8)}`,n)})}function k(e,n){let r=e.eventVolume.slice(0,5),i=Math.max(1,n-19-8-1),a=Math.max(...r.map(e=>e.count),1);return r.map(e=>(0,t.truncateToWidth)(`${_(e.name,19)}${C(e.count,a,i)} ${v(y(e.count),8)}`,n))}function A(e,n){let r=Math.max(1,n-6-12-5);return e.recentErrors.slice(0,5).map(e=>(0,t.truncateToWidth)(_(S(e.at),6)+_(e.event,12)+_(e.message,r)+v(e.code,5),n))}function j(e,t,n){let r=n?[w(`diagnostic`,n,t)]:[];return e?[w(`service`,e.service,t),w(`backend`,e.backend,t),w(`endpoint`,`${e.endpointSource} · ${e.endpoint}`,t),w(`traces`,`${e.traces?`on`:`off`} · AGENT_OTEL_TRACES`,t),w(`redaction`,`${e.redaction?`on`:`off`} · metadata only`,t),w(`file fallback`,e.fileFallback?`allowed`:`disabled`,t),...r]:[w(`status`,`not connected`,t),w(`reason`,`no sink handle this session`,t),w(`metrics`,`aggregated in-process anyway`,t),...r]}function M(e,n,r){let i=e?.groups.slice(0,6)??[];if(i.length===0)return[];let a=Math.max(1,n-20-12-8-1),o=Math.max(...i.map(e=>e.totalTokens),1);return i.map(e=>{let i=r.fg(`accent`,C(e.totalTokens,o,a));return(0,t.truncateToWidth)(`${_(e.key,20)}${_(e.agentName??`—`,12)}${i} ${v(b(e.totalTokens),8)}`,n)})}function N(e,n,r){let i=e?.timeline.slice(-8)??[];if(i.length===0)return[];let a=Math.max(1,n-17-8-1),o=Math.max(...i.map(e=>e.totalTokens),1);return i.map(e=>{let i=r.fg(`success`,C(e.totalTokens,o,a));return(0,t.truncateToWidth)(`${_(e.label,17)}${i} ${v(b(e.totalTokens),8)}`,n)})}function P(e,t){if(t<1)return[e];let n=[],r=``;for(let i of e.split(` `)){let e=r.length===0?i:`${r} ${i}`;e.length>t&&r.length>0?(n.push(r),r=i):r=e}return r.length>0&&n.push(r),n.map(e=>_(e,t))}function F(e){return[w(`source`,`in-process Pi events`,e),w(`scope`,`current session only`,e),...P(`Headline counters are aggregated in-process from this session. The TOKENS and TOKEN BURN panels come from the sink database, so they span every agent and workflow in the selected period, not just this session.`,e)]}var I=class extends e.DoomOverlay{getView;done;timer;query;disposed=!1;groupIndex=0;periodIndex=0;requestId=0;loading=!1;report;reportError;constructor(e,t,n,r,i){super(e,t),this.getView=n,this.done=r,this.query=i,this.timer=setInterval(()=>{this.disposed||this.tui.requestRender()},1e3),this.timer.unref?.(),this.fetchReport()}handleInput(e){if((0,t.matchesKey)(e,`escape`)||(0,t.matchesKey)(e,`ctrl+c`)||(0,t.matchesKey)(e,`q`)){this.done(void 0);return}let i=e.toLowerCase();if(i===`g`){this.groupIndex=(this.groupIndex+1)%n.length,this.refresh();return}if(i===`p`){this.periodIndex=(this.periodIndex+1)%r.length,this.refresh();return}i===`r`&&this.refresh()}refresh(){this.fetchReport(),this.tui.requestRender()}fetchReport(){if(!this.query)return;let e=++this.requestId;this.loading=!0,this.reportError=void 0,this.query({groupBy:this.groupBy(),period:this.period(),limit:6}).then(t=>{this.disposed||e!==this.requestId||(this.report=t,this.loading=!1,this.tui.requestRender())}).catch(t=>{this.disposed||e!==this.requestId||(this.report=void 0,this.reportError=t instanceof Error?t.message:String(t),this.loading=!1,this.tui.requestRender())})}tokenPanel(e){let t=this.theme.fg(`accent`,`TOKENS · by ${this.groupBy()} · ${this.period()}`),n=M(this.report,e,this.theme);return[t,...n.length>0?n:[this.theme.fg(`dim`,this.historyStatus())]]}burnPanel(e){let t=this.report?.bucket??``,n=this.theme.fg(`accent`,`TOKEN BURN${t?` · per ${t}`:``}`),r=N(this.report,e,this.theme);return[n,...r.length>0?r:[this.theme.fg(`dim`,this.historyStatus())]]}groupBy(){return n[this.groupIndex]??i}period(){return r[this.periodIndex]??a}historyStatus(){return this.loading?`loading…`:this.reportError?this.reportError:this.report?`${this.transportLabel()} · ${this.report.totals.groupCount} groups`:`no sink history available`}transportLabel(){return this.getView().transport??`sink`}dispose(){this.disposed=!0,clearInterval(this.timer)}getChrome(){return{title:`LOG METRICS`,accent:e.DOOM_OVERLAY_ACCENT,breadcrumb:`SPC › h / help › l / logs`,headerRight:`doom-log · service pi · this session`,footer:`r refresh · g group · p period · esc close`,footerHints:[[`r`,`refresh`],[`g`,`group`],[`p`,`period`],[`esc`,`close`]]}}renderBody(e,t){let n=this.getView();return n.disabled?this.disabledBody(e):this.metricsBody(n,e,t)}disabledBody(e){return[(0,t.truncateToWidth)(this.theme.fg(`warning`,` Metrics collection is disabled for this session.`),e),(0,t.truncateToWidth)(this.theme.fg(`dim`,` ${d}=1 is set, so no log records are produced.`),e),(0,t.truncateToWidth)(this.theme.fg(`dim`,` Unset ${d} and restart Pi to collect metrics.`),e)]}metricsBody(e,t,n){let r=E(e.snapshot,t);if(n<=r.length)return r.slice(0,n);if(t<72)return[...r,``,...this.singleColumnPanels(e,t)].slice(0,n);let i=Math.max(24,Math.floor(t*.4)),a=Math.max(1,t-i-1),d=Math.max(1,a-1),f=Math.max(1,i-1),p=[...this.tokenPanel(d),``,this.theme.fg(`accent`,o),...O(e.snapshot,d,this.theme),``,this.theme.fg(`accent`,s),...k(e.snapshot,d)],m=[...this.burnPanel(f),``,this.theme.fg(`accent`,c),...A(e.snapshot,f),``,this.theme.fg(`accent`,l),...j(e.sink,f,e.lastDiagnostic),``,this.theme.fg(`accent`,u),...F(f)],h=Math.max(0,n-r.length-1),g=[this.theme.fg(`borderMuted`,`${`─`.repeat(a)}┬${`─`.repeat(i)}`)];for(let e=0;e<h;e++)g.push(_(` ${p[e]??``}`,a)+this.theme.fg(`borderMuted`,`│`)+_(` ${m[e]??``}`,i));return[...r,...g].slice(0,n)}singleColumnPanels(e,t){return[this.theme.fg(`accent`,o),...O(e.snapshot,t,this.theme),``,this.theme.fg(`accent`,s),...k(e.snapshot,t),``,this.theme.fg(`accent`,c),...A(e.snapshot,t),``,this.theme.fg(`accent`,l),...j(e.sink,t,e.lastDiagnostic),``,this.theme.fg(`accent`,u),...F(t)]}};exports.LogMetricsOverlayComponent=I;
|
|
2
|
+
//# sourceMappingURL=metricsOverlay.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricsOverlay.cjs","names":["visibleWidth","truncateToWidth","DoomOverlay","matchesKey","DOOM_OVERLAY_ACCENT"],"sources":["../../src/tui/metricsOverlay.ts"],"sourcesContent":["/**\n * The `SPC h l` Log Metrics overlay.\n *\n * Rendering stays a pure `render(width)` so the layout can be asserted as text\n * without a live terminal. Every number comes from the in-process aggregator,\n * so the panels keep working when no sink is connected; only SINK STATUS\n * degrades.\n */\nimport type { Theme } from '@earendil-works/pi-coding-agent';\nimport { DOOM_OVERLAY_ACCENT, DoomOverlay, type DoomOverlayChrome, type DoomOverlayTui } from './doomOverlay.ts';\nimport { matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport type { LogMetricsGroupBy, LogMetricsPeriod, LogMetricsReport } from '@agimon-ai/log-sink-mcp';\nimport type { LogMetricsSnapshot } from '../metrics.ts';\nimport type { MetricsQuery, MetricsTransport } from '../metricsSource.ts';\n\nexport interface SinkStatus {\n service: string;\n backend: string;\n endpoint: string;\n endpointSource: string;\n traces: boolean;\n redaction: boolean;\n fileFallback: boolean;\n}\n\nexport interface LogMetricsView {\n disabled: boolean;\n snapshot: LogMetricsSnapshot;\n sink: SinkStatus | undefined;\n /** Which transport answered the last history query, for the panel status line. */\n transport?: MetricsTransport;\n /**\n * Most recent telemetry diagnostic. Surfaced here because writing it to stderr\n * would corrupt the TUI frame.\n */\n lastDiagnostic?: string;\n}\n\nconst TITLE = 'LOG METRICS';\nconst BREADCRUMB = 'SPC › h / help › l / logs';\nconst SUBTITLE = 'doom-log · service pi · this session';\nconst FOOTER = 'r refresh · g group · p period · esc close';\nconst TOKENS_TITLE = 'TOKENS';\nconst BURN_TITLE = 'TOKEN BURN';\n/** Dimensions that actually carry attribution today; workflow/step stay for runner-driven work. */\nconst GROUP_CYCLE = ['session', 'agent', 'workflow-name', 'step'] as const satisfies readonly LogMetricsGroupBy[];\nconst PERIOD_CYCLE = ['day', 'week', 'month', 'all'] as const satisfies readonly LogMetricsPeriod[];\n/** Head of each cycle, so the fallback never restates a literal. */\nconst DEFAULT_GROUP = GROUP_CYCLE[0];\nconst DEFAULT_PERIOD = PERIOD_CYCLE[0];\nconst MAX_TOKEN_ROWS = 6;\nconst MAX_BURN_ROWS = 8;\nconst GROUP_KEY_WIDTH = 20;\nconst AGENT_WIDTH = 12;\nconst BURN_LABEL_WIDTH = 17;\nconst LOADING_TEXT = 'loading…';\nconst NO_HISTORY_TEXT = 'no sink history available';\nconst LATENCY_TITLE = 'TOOL LATENCY · p95 duration_ms by tool.name';\nconst VOLUME_TITLE = 'EVENT VOLUME · by log record name';\nconst ERRORS_TITLE = 'RECENT ERRORS';\nconst SINK_TITLE = 'SINK STATUS';\nconst COLLECTION_TITLE = 'COLLECTION';\nconst DISABLED_ENV = 'AGENT_TELEMETRY_DISABLED';\nconst NOT_CONNECTED = 'not connected';\nconst EMPTY_VALUE = '—';\nconst ELLIPSIS = '…';\nconst THIS_SESSION = 'this session';\nconst TOGGLE_ON = 'on';\nconst TOGGLE_OFF = 'off';\n\nconst MIN_TWO_COLUMN_WIDTH = 72;\nconst RIGHT_COLUMN_RATIO = 0.4;\nconst MIN_RIGHT_COLUMN = 24;\nconst REFRESH_MS = 1000;\n\nconst STAT_CELLS = 6;\nconst STAT_ROW_INDEXES = [0, 1, 2];\nconst TOOL_NAME_WIDTH = 10;\n/** Wide enough for the longest record name the extension emits. */\nconst RECORD_NAME_WIDTH = 19;\nconst VALUE_WIDTH = 8;\nconst SINK_LABEL_WIDTH = 14;\nconst ERROR_TIME_WIDTH = 6;\n/** One wider than the longest event name, so the message never abuts it. */\nconst ERROR_EVENT_WIDTH = 12;\nconst ERROR_CODE_WIDTH = 5;\nconst MAX_TOOL_ROWS = 6;\nconst MAX_VOLUME_ROWS = 5;\nconst MAX_ERROR_ROWS = 5;\n/** Bar color thresholds, as a fraction of the panel's slowest tool. */\nconst LATENCY_WARN_RATIO = 0.33;\nconst LATENCY_ERROR_RATIO = 0.66;\n\nconst SECOND_MS = 1000;\nconst MILLION = 1_000_000;\nconst THOUSAND = 1000;\nconst PERCENT = 100;\nconst PERCENT_DECIMALS = 2;\nconst COMPACT_DECIMALS = 1;\nconst DURATION_DECIMALS = 2;\nconst COST_DECIMALS = 2;\nconst TIME_SLICE_END = 5;\n\nfunction pad(text: string, width: number): string {\n const visible = visibleWidth(text);\n // An explicit ellipsis: the default '...' also emits a colour reset, which\n // leaks escape codes into the middle of a padded column.\n return visible >= width ? truncateToWidth(text, width, ELLIPSIS) : text + ' '.repeat(width - visible);\n}\n\nfunction padStart(text: string, width: number): string {\n const visible = visibleWidth(text);\n return visible >= width ? truncateToWidth(text, width) : ' '.repeat(width - visible) + text;\n}\n\nfunction formatCount(value: number): string {\n return value.toLocaleString('en-US');\n}\n\nfunction formatCompact(value: number): string {\n if (value >= MILLION) return `${(value / MILLION).toFixed(COMPACT_DECIMALS)}M`;\n if (value >= THOUSAND) return `${Math.round(value / THOUSAND)}k`;\n return String(value);\n}\n\nfunction formatDuration(ms: number | undefined): string {\n if (ms === undefined) return EMPTY_VALUE;\n if (ms >= SECOND_MS) return `${(ms / SECOND_MS).toFixed(DURATION_DECIMALS)}s`;\n return `${Math.round(ms)}ms`;\n}\n\nfunction formatTime(at: number): string {\n return new Date(at).toTimeString().slice(0, TIME_SLICE_END);\n}\n\nfunction bar(value: number, max: number, width: number): string {\n const filled = Math.min(width, Math.max(value > 0 ? 1 : 0, Math.round((value / max) * width)));\n return '█'.repeat(filled) + '░'.repeat(width - filled);\n}\n\nfunction labelled(label: string, value: string, width: number): string {\n return truncateToWidth(pad(label, SINK_LABEL_WIDTH) + value, width);\n}\n\nfunction statCell(label: string, value: string, sublabel: string, width: number): string[] {\n return [pad(label, width), pad(value, width), pad(sublabel, width)];\n}\n\n/** The six headline cells, laid out as three full-width rows. */\nfunction statRows(snapshot: LogMetricsSnapshot, width: number): string[] {\n const cellWidth = Math.max(1, Math.floor(width / STAT_CELLS));\n const slowest = snapshot.toolLatency[0];\n const errorRate = ((snapshot.errors / Math.max(snapshot.events, 1)) * PERCENT).toFixed(PERCENT_DECIMALS);\n const cells = [\n statCell('EVENTS', formatCount(snapshot.events), THIS_SESSION, cellWidth),\n statCell('ERRORS', formatCount(snapshot.errors), `${errorRate}%`, cellWidth),\n statCell('TOOL CALLS', formatCount(snapshot.toolCalls), `${snapshot.failedToolCalls} failed`, cellWidth),\n statCell('P95 TOOL', formatDuration(slowest?.p95Ms), slowest?.name ?? EMPTY_VALUE, cellWidth),\n statCell(\n 'TOKENS',\n formatCompact(snapshot.tokens.total),\n `↑${formatCompact(snapshot.tokens.input)} ↓${formatCompact(snapshot.tokens.output)}`,\n cellWidth,\n ),\n statCell('COST', `$${snapshot.cost.toFixed(COST_DECIMALS)}`, THIS_SESSION, cellWidth),\n ];\n return STAT_ROW_INDEXES.map((row) => pad(cells.map((cell) => cell[row] ?? '').join(''), width));\n}\n\n/** Fast tools read as `success`, slow ones as `error`, matching the mockup's green-to-red bar gradient. */\nfunction latencyColor(value: number, max: number): 'success' | 'warning' | 'error' {\n const ratio = max > 0 ? value / max : 0;\n if (ratio >= LATENCY_ERROR_RATIO) return 'error';\n if (ratio >= LATENCY_WARN_RATIO) return 'warning';\n return 'success';\n}\n\nfunction latencyRows(snapshot: LogMetricsSnapshot, width: number, theme: Theme): string[] {\n const entries = snapshot.toolLatency.slice(0, MAX_TOOL_ROWS);\n const barWidth = Math.max(1, width - TOOL_NAME_WIDTH - VALUE_WIDTH - 1);\n const max = Math.max(...entries.map((entry) => entry.p95Ms ?? 0), 1);\n return entries.map((entry) => {\n const barText = theme.fg(latencyColor(entry.p95Ms ?? 0, max), bar(entry.p95Ms ?? 0, max, barWidth));\n return truncateToWidth(\n `${pad(entry.name, TOOL_NAME_WIDTH)}${barText} ${padStart(formatDuration(entry.p95Ms), VALUE_WIDTH)}`,\n width,\n );\n });\n}\n\nfunction volumeRows(snapshot: LogMetricsSnapshot, width: number): string[] {\n const entries = snapshot.eventVolume.slice(0, MAX_VOLUME_ROWS);\n const barWidth = Math.max(1, width - RECORD_NAME_WIDTH - VALUE_WIDTH - 1);\n const max = Math.max(...entries.map((entry) => entry.count), 1);\n return entries.map((entry) =>\n truncateToWidth(\n `${pad(entry.name, RECORD_NAME_WIDTH)}${bar(entry.count, max, barWidth)} ${padStart(formatCount(entry.count), VALUE_WIDTH)}`,\n width,\n ),\n );\n}\n\nfunction errorRows(snapshot: LogMetricsSnapshot, width: number): string[] {\n const messageWidth = Math.max(1, width - ERROR_TIME_WIDTH - ERROR_EVENT_WIDTH - ERROR_CODE_WIDTH);\n return snapshot.recentErrors\n .slice(0, MAX_ERROR_ROWS)\n .map((entry) =>\n truncateToWidth(\n pad(formatTime(entry.at), ERROR_TIME_WIDTH) +\n pad(entry.event, ERROR_EVENT_WIDTH) +\n pad(entry.message, messageWidth) +\n padStart(entry.code, ERROR_CODE_WIDTH),\n width,\n ),\n );\n}\n\n/**\n * Only fields NodeTelemetryHandle can actually supply. It exposes no queue\n * counters, so none are invented here.\n */\nfunction sinkRows(sink: SinkStatus | undefined, width: number, lastDiagnostic?: string): string[] {\n /** Diagnostics can land before a handle exists, so both branches report them. */\n const diagnosticRows = lastDiagnostic ? [labelled('diagnostic', lastDiagnostic, width)] : [];\n if (!sink) {\n return [\n labelled('status', NOT_CONNECTED, width),\n labelled('reason', 'no sink handle this session', width),\n labelled('metrics', 'aggregated in-process anyway', width),\n ...diagnosticRows,\n ];\n }\n return [\n labelled('service', sink.service, width),\n labelled('backend', sink.backend, width),\n labelled('endpoint', `${sink.endpointSource} · ${sink.endpoint}`, width),\n labelled('traces', `${sink.traces ? TOGGLE_ON : TOGGLE_OFF} · AGENT_OTEL_TRACES`, width),\n labelled('redaction', `${sink.redaction ? TOGGLE_ON : TOGGLE_OFF} · metadata only`, width),\n labelled('file fallback', sink.fileFallback ? 'allowed' : 'disabled', width),\n ...diagnosticRows,\n ];\n}\n\nconst COLLECTION_NOTE =\n 'Headline counters are aggregated in-process from this session. The TOKENS and ' +\n 'TOKEN BURN panels come from the sink database, so they span every agent and ' +\n 'workflow in the selected period, not just this session.';\n\n/** Ranked token consumers for the selected dimension. */\nfunction tokenRows(report: LogMetricsReport | undefined, width: number, theme: Theme): string[] {\n const groups = report?.groups.slice(0, MAX_TOKEN_ROWS) ?? [];\n if (groups.length === 0) return [];\n\n const barWidth = Math.max(1, width - GROUP_KEY_WIDTH - AGENT_WIDTH - VALUE_WIDTH - 1);\n const max = Math.max(...groups.map((group) => group.totalTokens), 1);\n return groups.map((group) => {\n const barText = theme.fg('accent', bar(group.totalTokens, max, barWidth));\n return truncateToWidth(\n `${pad(group.key, GROUP_KEY_WIDTH)}${pad(group.agentName ?? EMPTY_VALUE, AGENT_WIDTH)}${barText} ${padStart(\n formatCompact(group.totalTokens),\n VALUE_WIDTH,\n )}`,\n width,\n );\n });\n}\n\n/** Token burn per timeline bucket, newest last so the trend reads left to right. */\nfunction burnRows(report: LogMetricsReport | undefined, width: number, theme: Theme): string[] {\n const buckets = report?.timeline.slice(-MAX_BURN_ROWS) ?? [];\n if (buckets.length === 0) return [];\n\n const barWidth = Math.max(1, width - BURN_LABEL_WIDTH - VALUE_WIDTH - 1);\n const max = Math.max(...buckets.map((bucket) => bucket.totalTokens), 1);\n return buckets.map((bucket) => {\n const barText = theme.fg('success', bar(bucket.totalTokens, max, barWidth));\n // `label` is the local-time rendering; bucketStart serialises to UTC.\n return truncateToWidth(\n `${pad(bucket.label, BURN_LABEL_WIDTH)}${barText} ${padStart(formatCompact(bucket.totalTokens), VALUE_WIDTH)}`,\n width,\n );\n });\n}\n\n/** Greedy word wrap: no library dependency for a single static paragraph. */\nfunction wrapText(text: string, width: number): string[] {\n if (width < 1) return [text];\n const lines: string[] = [];\n let current = '';\n for (const word of text.split(' ')) {\n const candidate = current.length === 0 ? word : `${current} ${word}`;\n if (candidate.length > width && current.length > 0) {\n lines.push(current);\n current = word;\n } else {\n current = candidate;\n }\n }\n if (current.length > 0) lines.push(current);\n return lines.map((line) => pad(line, width));\n}\n\nfunction collectionRows(width: number): string[] {\n return [\n labelled('source', 'in-process Pi events', width),\n labelled('scope', 'current session only', width),\n ...wrapText(COLLECTION_NOTE, width),\n ];\n}\n\nexport class LogMetricsOverlayComponent extends DoomOverlay {\n private readonly getView: () => LogMetricsView;\n private readonly done: (result: undefined) => void;\n private readonly timer: ReturnType<typeof setInterval>;\n private readonly query: MetricsQuery | undefined;\n private disposed = false;\n private groupIndex = 0;\n private periodIndex = 0;\n private requestId = 0;\n private loading = false;\n private report: LogMetricsReport | undefined;\n private reportError: string | undefined;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n getView: () => LogMetricsView,\n done: (result: undefined) => void,\n query?: MetricsQuery,\n ) {\n super(tui, theme);\n this.getView = getView;\n this.done = done;\n this.query = query;\n this.timer = setInterval(() => {\n if (!this.disposed) this.tui.requestRender();\n }, REFRESH_MS);\n this.timer.unref?.();\n this.fetchReport();\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c') || matchesKey(data, 'q')) {\n this.done(undefined);\n return;\n }\n\n const key = data.toLowerCase();\n if (key === 'g') {\n this.groupIndex = (this.groupIndex + 1) % GROUP_CYCLE.length;\n this.refresh();\n return;\n }\n if (key === 'p') {\n this.periodIndex = (this.periodIndex + 1) % PERIOD_CYCLE.length;\n this.refresh();\n return;\n }\n if (key === 'r') this.refresh();\n }\n\n /** Re-reads the live session counters, and restarts the history query behind them. */\n private refresh(): void {\n this.fetchReport();\n this.tui.requestRender();\n }\n\n /**\n * Sink history is fetched out of band: the query crosses a process boundary\n * and takes seconds on a busy database, so render never waits on it. Stale\n * responses are dropped by sequence number when the dimension changes mid\n * flight.\n */\n private fetchReport(): void {\n if (!this.query) return;\n\n const requestId = ++this.requestId;\n this.loading = true;\n this.reportError = undefined;\n this.query({ groupBy: this.groupBy(), period: this.period(), limit: MAX_TOKEN_ROWS })\n .then((report) => {\n if (this.disposed || requestId !== this.requestId) return;\n this.report = report;\n this.loading = false;\n this.tui.requestRender();\n })\n .catch((error: unknown) => {\n if (this.disposed || requestId !== this.requestId) return;\n this.report = undefined;\n this.reportError = error instanceof Error ? error.message : String(error);\n this.loading = false;\n this.tui.requestRender();\n });\n }\n\n /** Ranked consumers for the selected dimension, sourced from the sink database. */\n private tokenPanel(width: number): string[] {\n const heading = this.theme.fg('accent', `${TOKENS_TITLE} · by ${this.groupBy()} · ${this.period()}`);\n const rows = tokenRows(this.report, width, this.theme);\n return [heading, ...(rows.length > 0 ? rows : [this.theme.fg('dim', this.historyStatus())])];\n }\n\n private burnPanel(width: number): string[] {\n const bucket = this.report?.bucket ?? '';\n const heading = this.theme.fg('accent', `${BURN_TITLE}${bucket ? ` · per ${bucket}` : ''}`);\n const rows = burnRows(this.report, width, this.theme);\n return [heading, ...(rows.length > 0 ? rows : [this.theme.fg('dim', this.historyStatus())])];\n }\n\n private groupBy(): LogMetricsGroupBy {\n return GROUP_CYCLE[this.groupIndex] ?? DEFAULT_GROUP;\n }\n\n private period(): LogMetricsPeriod {\n return PERIOD_CYCLE[this.periodIndex] ?? DEFAULT_PERIOD;\n }\n\n /** One status line for the history panels: what is shown, from where. */\n private historyStatus(): string {\n if (this.loading) return LOADING_TEXT;\n if (this.reportError) return this.reportError;\n if (!this.report) return NO_HISTORY_TEXT;\n return `${this.transportLabel()} · ${this.report.totals.groupCount} groups`;\n }\n\n private transportLabel(): MetricsTransport | 'sink' {\n return this.getView().transport ?? 'sink';\n }\n\n dispose(): void {\n this.disposed = true;\n clearInterval(this.timer);\n }\n\n protected getChrome(): DoomOverlayChrome {\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: SUBTITLE,\n footer: FOOTER,\n footerHints: [\n ['r', 'refresh'],\n ['g', 'group'],\n ['p', 'period'],\n ['esc', 'close'],\n ],\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const view = this.getView();\n return view.disabled ? this.disabledBody(width) : this.metricsBody(view, width, height);\n }\n\n /**\n * An explicit disabled state: zeroed stat cells would read as a real session\n * that happened to do nothing.\n */\n private disabledBody(width: number): string[] {\n return [\n truncateToWidth(this.theme.fg('warning', ' Metrics collection is disabled for this session.'), width),\n truncateToWidth(this.theme.fg('dim', ` ${DISABLED_ENV}=1 is set, so no log records are produced.`), width),\n truncateToWidth(this.theme.fg('dim', ` Unset ${DISABLED_ENV} and restart Pi to collect metrics.`), width),\n ];\n }\n\n private metricsBody(view: LogMetricsView, width: number, height: number): string[] {\n const stats = statRows(view.snapshot, width);\n if (height <= stats.length) return stats.slice(0, height);\n\n if (width < MIN_TWO_COLUMN_WIDTH) {\n return [...stats, '', ...this.singleColumnPanels(view, width)].slice(0, height);\n }\n\n const rightWidth = Math.max(MIN_RIGHT_COLUMN, Math.floor(width * RIGHT_COLUMN_RATIO));\n const leftWidth = Math.max(1, width - rightWidth - 1);\n\n // Each column reserves one leading space, so rows are built one narrower.\n const leftContent = Math.max(1, leftWidth - 1);\n const rightContent = Math.max(1, rightWidth - 1);\n const left = [\n ...this.tokenPanel(leftContent),\n '',\n this.theme.fg('accent', LATENCY_TITLE),\n ...latencyRows(view.snapshot, leftContent, this.theme),\n '',\n this.theme.fg('accent', VOLUME_TITLE),\n ...volumeRows(view.snapshot, leftContent),\n ];\n const right = [\n ...this.burnPanel(rightContent),\n '',\n this.theme.fg('accent', ERRORS_TITLE),\n ...errorRows(view.snapshot, rightContent),\n '',\n this.theme.fg('accent', SINK_TITLE),\n ...sinkRows(view.sink, rightContent, view.lastDiagnostic),\n '',\n this.theme.fg('accent', COLLECTION_TITLE),\n ...collectionRows(rightContent),\n ];\n\n const bodyHeight = Math.max(0, height - stats.length - 1);\n const body = [this.theme.fg('borderMuted', `${'─'.repeat(leftWidth)}┬${'─'.repeat(rightWidth)}`)];\n for (let index = 0; index < bodyHeight; index++) {\n body.push(\n pad(` ${left[index] ?? ''}`, leftWidth) +\n this.theme.fg('borderMuted', '│') +\n pad(` ${right[index] ?? ''}`, rightWidth),\n );\n }\n return [...stats, ...body].slice(0, height);\n }\n\n private singleColumnPanels(view: LogMetricsView, width: number): string[] {\n return [\n this.theme.fg('accent', LATENCY_TITLE),\n ...latencyRows(view.snapshot, width, this.theme),\n '',\n this.theme.fg('accent', VOLUME_TITLE),\n ...volumeRows(view.snapshot, width),\n '',\n this.theme.fg('accent', ERRORS_TITLE),\n ...errorRows(view.snapshot, width),\n '',\n this.theme.fg('accent', SINK_TITLE),\n ...sinkRows(view.sink, width, view.lastDiagnostic),\n '',\n this.theme.fg('accent', COLLECTION_TITLE),\n ...collectionRows(width),\n ];\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { LogMetricsSnapshot } from "../metrics.cjs";
|
|
2
|
+
import { MetricsQuery, MetricsTransport } from "../metricsSource.cjs";
|
|
3
|
+
import { DoomOverlay, DoomOverlayChrome, DoomOverlayTui } from "./doomOverlay.cjs";
|
|
4
|
+
import { Theme } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
//#region src/tui/metricsOverlay.d.ts
|
|
6
|
+
interface SinkStatus {
|
|
7
|
+
service: string;
|
|
8
|
+
backend: string;
|
|
9
|
+
endpoint: string;
|
|
10
|
+
endpointSource: string;
|
|
11
|
+
traces: boolean;
|
|
12
|
+
redaction: boolean;
|
|
13
|
+
fileFallback: boolean;
|
|
14
|
+
}
|
|
15
|
+
interface LogMetricsView {
|
|
16
|
+
disabled: boolean;
|
|
17
|
+
snapshot: LogMetricsSnapshot;
|
|
18
|
+
sink: SinkStatus | undefined;
|
|
19
|
+
/** Which transport answered the last history query, for the panel status line. */
|
|
20
|
+
transport?: MetricsTransport;
|
|
21
|
+
/**
|
|
22
|
+
* Most recent telemetry diagnostic. Surfaced here because writing it to stderr
|
|
23
|
+
* would corrupt the TUI frame.
|
|
24
|
+
*/
|
|
25
|
+
lastDiagnostic?: string;
|
|
26
|
+
}
|
|
27
|
+
declare class LogMetricsOverlayComponent extends DoomOverlay {
|
|
28
|
+
private readonly getView;
|
|
29
|
+
private readonly done;
|
|
30
|
+
private readonly timer;
|
|
31
|
+
private readonly query;
|
|
32
|
+
private disposed;
|
|
33
|
+
private groupIndex;
|
|
34
|
+
private periodIndex;
|
|
35
|
+
private requestId;
|
|
36
|
+
private loading;
|
|
37
|
+
private report;
|
|
38
|
+
private reportError;
|
|
39
|
+
constructor(tui: DoomOverlayTui, theme: Theme, getView: () => LogMetricsView, done: (result: undefined) => void, query?: MetricsQuery);
|
|
40
|
+
handleInput(data: string): void;
|
|
41
|
+
/** Re-reads the live session counters, and restarts the history query behind them. */
|
|
42
|
+
private refresh;
|
|
43
|
+
/**
|
|
44
|
+
* Sink history is fetched out of band: the query crosses a process boundary
|
|
45
|
+
* and takes seconds on a busy database, so render never waits on it. Stale
|
|
46
|
+
* responses are dropped by sequence number when the dimension changes mid
|
|
47
|
+
* flight.
|
|
48
|
+
*/
|
|
49
|
+
private fetchReport;
|
|
50
|
+
/** Ranked consumers for the selected dimension, sourced from the sink database. */
|
|
51
|
+
private tokenPanel;
|
|
52
|
+
private burnPanel;
|
|
53
|
+
private groupBy;
|
|
54
|
+
private period;
|
|
55
|
+
/** One status line for the history panels: what is shown, from where. */
|
|
56
|
+
private historyStatus;
|
|
57
|
+
private transportLabel;
|
|
58
|
+
dispose(): void;
|
|
59
|
+
protected getChrome(): DoomOverlayChrome;
|
|
60
|
+
protected renderBody(width: number, height: number): string[];
|
|
61
|
+
/**
|
|
62
|
+
* An explicit disabled state: zeroed stat cells would read as a real session
|
|
63
|
+
* that happened to do nothing.
|
|
64
|
+
*/
|
|
65
|
+
private disabledBody;
|
|
66
|
+
private metricsBody;
|
|
67
|
+
private singleColumnPanels;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { LogMetricsOverlayComponent, LogMetricsView, SinkStatus };
|
|
71
|
+
//# sourceMappingURL=metricsOverlay.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricsOverlay.d.cts","names":[],"sources":["../../src/tui/metricsOverlay.ts"],"mappings":";;;;;UAeiB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA,UAAU;EACV,MAAM;;EAEN,YAAY;;;;;EAKZ;;cAmRW,mCAAmC;mBAC7B;mBACA;mBACA;mBACA;UACT;UACA;UACA;UACA;UACA;UACA;UACA;EAGN,YAAA,KAAK,gBACL,OAAO,OACP,eAAe,gBACf,OAAO,4BACP,QAAQ;EAaV,YAAY;;UAqBJ;;;;;;;UAWA;;UAuBA;UAMA;UAOA;UAIA;;UAKA;UAOA;EAIR;YAKU,aAAa;YAgBb,WAAW,eAAe;;;;;UAS5B;UAQA;UAgDA"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { LogMetricsSnapshot } from "../metrics.mjs";
|
|
2
|
+
import { MetricsQuery, MetricsTransport } from "../metricsSource.mjs";
|
|
3
|
+
import { DoomOverlay, DoomOverlayChrome, DoomOverlayTui } from "./doomOverlay.mjs";
|
|
4
|
+
import { Theme } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
//#region src/tui/metricsOverlay.d.ts
|
|
6
|
+
interface SinkStatus {
|
|
7
|
+
service: string;
|
|
8
|
+
backend: string;
|
|
9
|
+
endpoint: string;
|
|
10
|
+
endpointSource: string;
|
|
11
|
+
traces: boolean;
|
|
12
|
+
redaction: boolean;
|
|
13
|
+
fileFallback: boolean;
|
|
14
|
+
}
|
|
15
|
+
interface LogMetricsView {
|
|
16
|
+
disabled: boolean;
|
|
17
|
+
snapshot: LogMetricsSnapshot;
|
|
18
|
+
sink: SinkStatus | undefined;
|
|
19
|
+
/** Which transport answered the last history query, for the panel status line. */
|
|
20
|
+
transport?: MetricsTransport;
|
|
21
|
+
/**
|
|
22
|
+
* Most recent telemetry diagnostic. Surfaced here because writing it to stderr
|
|
23
|
+
* would corrupt the TUI frame.
|
|
24
|
+
*/
|
|
25
|
+
lastDiagnostic?: string;
|
|
26
|
+
}
|
|
27
|
+
declare class LogMetricsOverlayComponent extends DoomOverlay {
|
|
28
|
+
private readonly getView;
|
|
29
|
+
private readonly done;
|
|
30
|
+
private readonly timer;
|
|
31
|
+
private readonly query;
|
|
32
|
+
private disposed;
|
|
33
|
+
private groupIndex;
|
|
34
|
+
private periodIndex;
|
|
35
|
+
private requestId;
|
|
36
|
+
private loading;
|
|
37
|
+
private report;
|
|
38
|
+
private reportError;
|
|
39
|
+
constructor(tui: DoomOverlayTui, theme: Theme, getView: () => LogMetricsView, done: (result: undefined) => void, query?: MetricsQuery);
|
|
40
|
+
handleInput(data: string): void;
|
|
41
|
+
/** Re-reads the live session counters, and restarts the history query behind them. */
|
|
42
|
+
private refresh;
|
|
43
|
+
/**
|
|
44
|
+
* Sink history is fetched out of band: the query crosses a process boundary
|
|
45
|
+
* and takes seconds on a busy database, so render never waits on it. Stale
|
|
46
|
+
* responses are dropped by sequence number when the dimension changes mid
|
|
47
|
+
* flight.
|
|
48
|
+
*/
|
|
49
|
+
private fetchReport;
|
|
50
|
+
/** Ranked consumers for the selected dimension, sourced from the sink database. */
|
|
51
|
+
private tokenPanel;
|
|
52
|
+
private burnPanel;
|
|
53
|
+
private groupBy;
|
|
54
|
+
private period;
|
|
55
|
+
/** One status line for the history panels: what is shown, from where. */
|
|
56
|
+
private historyStatus;
|
|
57
|
+
private transportLabel;
|
|
58
|
+
dispose(): void;
|
|
59
|
+
protected getChrome(): DoomOverlayChrome;
|
|
60
|
+
protected renderBody(width: number, height: number): string[];
|
|
61
|
+
/**
|
|
62
|
+
* An explicit disabled state: zeroed stat cells would read as a real session
|
|
63
|
+
* that happened to do nothing.
|
|
64
|
+
*/
|
|
65
|
+
private disabledBody;
|
|
66
|
+
private metricsBody;
|
|
67
|
+
private singleColumnPanels;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { LogMetricsOverlayComponent, LogMetricsView, SinkStatus };
|
|
71
|
+
//# sourceMappingURL=metricsOverlay.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricsOverlay.d.mts","names":[],"sources":["../../src/tui/metricsOverlay.ts"],"mappings":";;;;;UAeiB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA,UAAU;EACV,MAAM;;EAEN,YAAY;;;;;EAKZ;;cAmRW,mCAAmC;mBAC7B;mBACA;mBACA;mBACA;UACT;UACA;UACA;UACA;UACA;UACA;UACA;EAGN,YAAA,KAAK,gBACL,OAAO,OACP,eAAe,gBACf,OAAO,4BACP,QAAQ;EAaV,YAAY;;UAqBJ;;;;;;;UAWA;;UAuBA;UAMA;UAOA;UAIA;;UAKA;UAOA;EAIR;YAKU,aAAa;YAgBb,WAAW,eAAe;;;;;UAS5B;UAQA;UAgDA"}
|