Code-review follow-ups (Approve-with-comments) for batch #197 (context badge #189 / e2e in CI #187 / inline MCP test #170): - server: extract the duplicated chatContextWindow ::text->positive-int coercion (resolve() + getMasked()) into an exported parsePositiveInt helper and unit-test its branches (200000/1.9/0/-5/""/abc/undefined), closing the untested read-path gap. - client: merge the two backward scans over messageRows into one pure, exported selectContextBadge helper (numerator and denominator still taken from the most recent row carrying EACH value) and unit-test the different-rows and fresh-zero-doesn't-shadow cases. - client: extract the MCP "Test" button tristate presentation into a pure mcpTestButtonView helper (collapses the two parallel if/else chains) and unit-test idle/ok-with-tools/ok-no-tools/failed label+tooltip branches. - ci: redirect the backgrounded prod server's stdout/stderr to a log file in e2e-mcp and cat it on failure, so a start-up crash is diagnosable instead of surfacing only as the generic health timeout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
50 lines
2.0 KiB
TypeScript
50 lines
2.0 KiB
TypeScript
import type { IAiChatMessageRow } from "@/features/ai-chat/types/ai-chat.types.ts";
|
|
|
|
/**
|
|
* Derive the header context badge figures from the persisted message rows.
|
|
*
|
|
* - `contextTokens` (numerator): how much the conversation now occupies in the
|
|
* model's context window. Read from the most recent row carrying a context
|
|
* figure — `contextTokens` (final-step input+output) on rows recorded after
|
|
* this shipped, else that turn's legacy `usage` total for older rows.
|
|
* - `maxContextTokens` (denominator): the model's configured max window, stamped
|
|
* alongside `contextTokens` on a completed turn.
|
|
*
|
|
* Each value is taken from the most recent row carrying THAT value
|
|
* independently — they may land on different rows (e.g. a fresh error row can
|
|
* carry `contextTokens` but not `maxContextTokens`), so the scan continues for
|
|
* whichever is still unset. `0` means "no row has it" (older rows, or no
|
|
* admin-configured limit); the badge then omits the value.
|
|
*/
|
|
export function selectContextBadge(
|
|
messageRows: readonly IAiChatMessageRow[] | undefined | null,
|
|
): { contextTokens: number; maxContextTokens: number } {
|
|
let contextTokens = 0;
|
|
let maxContextTokens = 0;
|
|
if (!messageRows) return { contextTokens, maxContextTokens };
|
|
for (let i = messageRows.length - 1; i >= 0; i--) {
|
|
const meta = messageRows[i].metadata;
|
|
if (!meta) continue;
|
|
if (contextTokens === 0) {
|
|
if (typeof meta.contextTokens === "number" && meta.contextTokens > 0) {
|
|
contextTokens = meta.contextTokens;
|
|
} else if (meta.usage) {
|
|
const usage = meta.usage;
|
|
const fallback =
|
|
usage.totalTokens ??
|
|
(usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
|
|
if (fallback > 0) contextTokens = fallback;
|
|
}
|
|
}
|
|
if (
|
|
maxContextTokens === 0 &&
|
|
typeof meta.maxContextTokens === "number" &&
|
|
meta.maxContextTokens > 0
|
|
) {
|
|
maxContextTokens = meta.maxContextTokens;
|
|
}
|
|
if (contextTokens !== 0 && maxContextTokens !== 0) break;
|
|
}
|
|
return { contextTokens, maxContextTokens };
|
|
}
|