@mrciphersmith/keryx 0.2.61 → 0.2.63

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.
Files changed (2) hide show
  1. package/dist/cli.js +716 -262
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -10173,7 +10173,90 @@ async function resolveModelsForPicker(fetchFn, provider, env = process.env, opts
10173
10173
  const apiKey = typeof raw === "string" && raw.length > 0 ? raw : undefined;
10174
10174
  return fetchOpenAiCompatModelsDetailed(fetchFn, { ...compat, ...provider.baseUrl !== undefined ? { baseUrl: provider.baseUrl } : {} }, apiKey, opts);
10175
10175
  }
10176
- var DEFAULT_MODELS_PATH = "/v1/models", OPENAI_COMPAT_PROVIDERS, MODELS_FETCH_TIMEOUT_MS = 1e4;
10176
+ function balanceCapableProvider(name) {
10177
+ const provider = providerByName(name);
10178
+ if (provider === undefined || provider.balancePath === undefined || provider.balanceKind === undefined) {
10179
+ return;
10180
+ }
10181
+ return provider;
10182
+ }
10183
+ function parseDeepSeekBalance(body) {
10184
+ if (typeof body !== "object" || body === null) {
10185
+ return;
10186
+ }
10187
+ const infos = body.balance_infos;
10188
+ if (!Array.isArray(infos)) {
10189
+ return;
10190
+ }
10191
+ for (const info of infos) {
10192
+ if (typeof info !== "object" || info === null) {
10193
+ continue;
10194
+ }
10195
+ const total = Number(info.total_balance);
10196
+ if (Number.isFinite(total)) {
10197
+ const currency = String(info.currency ?? "USD");
10198
+ return { currency, total, exact: true };
10199
+ }
10200
+ }
10201
+ return;
10202
+ }
10203
+ function parseOpenRouterBalance(body) {
10204
+ if (typeof body !== "object" || body === null) {
10205
+ return;
10206
+ }
10207
+ const credits = body.credits;
10208
+ if (typeof credits !== "object" || credits === null) {
10209
+ return;
10210
+ }
10211
+ const total = Number(credits.total);
10212
+ const used = Number(credits.used);
10213
+ if (!Number.isFinite(total)) {
10214
+ return;
10215
+ }
10216
+ const usedField = Number.isFinite(used) ? { used } : {};
10217
+ const remaining = Number.isFinite(used) ? total - used : undefined;
10218
+ return {
10219
+ currency: String(credits.currency ?? "USD"),
10220
+ total,
10221
+ ...usedField,
10222
+ ...remaining !== undefined ? { remaining } : {},
10223
+ exact: true
10224
+ };
10225
+ }
10226
+ async function fetchProviderBalance(fetchFn, provider, apiKey, opts) {
10227
+ if (provider.balancePath === undefined || provider.balanceKind === undefined) {
10228
+ return;
10229
+ }
10230
+ const url = `${provider.baseUrl.replace(/\/+$/, "")}${provider.balancePath}`;
10231
+ const timeoutMs = opts?.timeoutMs ?? BALANCE_FETCH_TIMEOUT_MS;
10232
+ const controller = new AbortController;
10233
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
10234
+ try {
10235
+ const init = { signal: controller.signal };
10236
+ if (apiKey !== undefined && apiKey.length > 0) {
10237
+ init.headers = { authorization: `Bearer ${apiKey}` };
10238
+ }
10239
+ const res = await fetchFn(url, init);
10240
+ if (!res.ok) {
10241
+ return;
10242
+ }
10243
+ const body = await res.json();
10244
+ return provider.balanceKind === "deepseek" ? parseDeepSeekBalance(body) : parseOpenRouterBalance(body);
10245
+ } catch {
10246
+ return;
10247
+ } finally {
10248
+ clearTimeout(timer);
10249
+ }
10250
+ }
10251
+ function providerApiKey(provider, env = process.env) {
10252
+ const envKey = provider.envKey;
10253
+ if (envKey === undefined) {
10254
+ return;
10255
+ }
10256
+ const raw = env[envKey];
10257
+ return typeof raw === "string" && raw.length > 0 ? raw : undefined;
10258
+ }
10259
+ var DEFAULT_MODELS_PATH = "/v1/models", OPENAI_COMPAT_PROVIDERS, MODELS_FETCH_TIMEOUT_MS = 1e4, BALANCE_FETCH_TIMEOUT_MS = 8000;
10177
10260
  var init_providers = __esm(() => {
10178
10261
  OPENAI_COMPAT_PROVIDERS = [
10179
10262
  {
@@ -10182,7 +10265,9 @@ var init_providers = __esm(() => {
10182
10265
  baseUrl: "https://openrouter.ai/api",
10183
10266
  envKey: "OPENROUTER_API_KEY",
10184
10267
  models: ["openai/gpt-4o-mini", "google/gemini-2.0-flash-001", "qwen/qwen-2.5-7b-instruct", "meta-llama/llama-3.1-8b-instruct"],
10185
- note: "hosted \xB7 400+ models"
10268
+ note: "hosted \xB7 400+ models",
10269
+ balancePath: "/api/v1/credits",
10270
+ balanceKind: "openrouter"
10186
10271
  },
10187
10272
  {
10188
10273
  name: "deepseek",
@@ -10190,7 +10275,9 @@ var init_providers = __esm(() => {
10190
10275
  baseUrl: "https://api.deepseek.com",
10191
10276
  envKey: "DEEPSEEK_API_KEY",
10192
10277
  models: ["deepseek-chat", "deepseek-reasoner"],
10193
- note: "cheap per-token"
10278
+ note: "cheap per-token",
10279
+ balancePath: "/user/balance",
10280
+ balanceKind: "deepseek"
10194
10281
  },
10195
10282
  {
10196
10283
  name: "zai",
@@ -13121,14 +13208,29 @@ async function runModelTurn(input2) {
13121
13208
  };
13122
13209
  let text = "";
13123
13210
  let error;
13211
+ let usage;
13212
+ let reasoning = false;
13213
+ const startedAt = performance.now();
13214
+ let firstByteAt;
13124
13215
  for await (const event of port.stream(request, { attemptId: request.requestId })) {
13216
+ if (firstByteAt === undefined) {
13217
+ firstByteAt = performance.now();
13218
+ }
13125
13219
  if (event.kind === "text_delta" && event.text) {
13126
13220
  text += event.text;
13127
13221
  } else if (event.kind === "provider_error" && event.error) {
13128
13222
  error = event.error;
13223
+ } else if (event.kind === "reasoning_delta") {
13224
+ reasoning = true;
13225
+ } else if (event.kind === "usage_update" && event.usage) {
13226
+ usage = event.usage;
13129
13227
  }
13130
13228
  }
13131
- return error ? { provider, model, credentialAvailable, text, error } : { provider, model, credentialAvailable, text };
13229
+ const latencyMs = firstByteAt !== undefined ? Math.max(0, Math.round(firstByteAt - startedAt)) : undefined;
13230
+ const usageField = usage !== undefined ? { usage } : {};
13231
+ const latencyField = latencyMs !== undefined ? { latencyMs } : {};
13232
+ const reasoningField = reasoning ? { reasoning } : {};
13233
+ return error ? { provider, model, credentialAvailable, text, error, ...usageField, ...latencyField, ...reasoningField } : { provider, model, credentialAvailable, text, ...usageField, ...latencyField, ...reasoningField };
13132
13234
  }
13133
13235
  var DEFAULT_PROVIDER = "anthropic", DEFAULT_MODELS;
13134
13236
  var init_single_turn = __esm(() => {
@@ -53454,7 +53556,7 @@ import { spawnSync as spawnSync2 } from "child_process";
53454
53556
  // package.json
53455
53557
  var package_default = {
53456
53558
  name: "@mrciphersmith/keryx",
53457
- version: "0.2.61",
53559
+ version: "0.2.63",
53458
53560
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
53459
53561
  private: false,
53460
53562
  publishConfig: {
@@ -57032,7 +57134,7 @@ var AGENT_SLASH_COMMANDS = [
57032
57134
  { name: "/theme", description: "Open the theme picker \u2014 /theme [name] applies immediately", modes: BOTH },
57033
57135
  {
57034
57136
  name: "/game",
57035
- description: "Play tic-tac-toe against the model \u2014 the game minimizes while the agent works",
57137
+ description: "Games against the model \u2014 /game [seconds] raises the model-turn deadline; add games via src/tui/games/",
57036
57138
  modes: AGENT_ONLY
57037
57139
  },
57038
57140
  {
@@ -57413,8 +57515,75 @@ function openThemePicker(otui, chrome, options) {
57413
57515
  return presentThemePicker((hostOtui, hostChrome, input2) => openModal(hostOtui, hostChrome, input2), otui, chrome, options);
57414
57516
  }
57415
57517
 
57416
- // src/tui/game-modal.ts
57417
- init_single_turn();
57518
+ // src/tui/games/registry.ts
57519
+ function createRegistry(games) {
57520
+ return {
57521
+ games,
57522
+ get(id) {
57523
+ return games.find((game) => game.id === id);
57524
+ }
57525
+ };
57526
+ }
57527
+
57528
+ // src/tui/games/stats.ts
57529
+ function emptyTurnTotals() {
57530
+ return {
57531
+ turns: 0,
57532
+ latencyMs: undefined,
57533
+ totalMs: undefined,
57534
+ inputTokens: undefined,
57535
+ outputTokens: undefined,
57536
+ localFallbacks: 0,
57537
+ errors: 0
57538
+ };
57539
+ }
57540
+ function addMaybe(a, b) {
57541
+ if (a === undefined && b === undefined) {
57542
+ return;
57543
+ }
57544
+ return (a ?? 0) + (b ?? 0);
57545
+ }
57546
+ function addTurn(totals, turn) {
57547
+ return {
57548
+ turns: totals.turns + 1,
57549
+ latencyMs: addMaybe(totals.latencyMs, turn.latencyMs),
57550
+ totalMs: addMaybe(totals.totalMs, turn.totalMs),
57551
+ inputTokens: addMaybe(totals.inputTokens, turn.inputTokens),
57552
+ outputTokens: addMaybe(totals.outputTokens, turn.outputTokens),
57553
+ localFallbacks: totals.localFallbacks + (turn.localFallback ? 1 : 0),
57554
+ errors: totals.errors + (turn.error ? 1 : 0)
57555
+ };
57556
+ }
57557
+ function formatMs(ms) {
57558
+ if (ms === undefined) {
57559
+ return "\u2013";
57560
+ }
57561
+ if (ms < 1000) {
57562
+ return `${ms}ms`;
57563
+ }
57564
+ return `${(ms / 1000).toFixed(1)}s`;
57565
+ }
57566
+ function formatTokens(n) {
57567
+ if (n === undefined) {
57568
+ return "\u2013";
57569
+ }
57570
+ if (n < 1000) {
57571
+ return `${n}`;
57572
+ }
57573
+ return `${(n / 1000).toFixed(1)}k`;
57574
+ }
57575
+
57576
+ // src/tui/games/constants.ts
57577
+ var GAME_MODEL_TIMEOUT_MS = 60000;
57578
+ var GAMES_FOOTER = [
57579
+ { key: "arrows", label: "move" },
57580
+ { key: "enter", label: "place" },
57581
+ { key: "r", label: "new game" },
57582
+ { key: "tab", label: "games" },
57583
+ { key: "esc", label: "minimize" }
57584
+ ];
57585
+
57586
+ // src/tui/games/tic-tac-toe/core.ts
57418
57587
  var WIN_LINES = [
57419
57588
  [0, 1, 2],
57420
57589
  [3, 4, 5],
@@ -57444,6 +57613,29 @@ function checkWinner(board) {
57444
57613
  function freeCells(board) {
57445
57614
  return board.flatMap((cell, index) => cell === null ? [index] : []);
57446
57615
  }
57616
+ function placeMark(board, index, mark) {
57617
+ if (index < 0 || index > 8 || board[index] !== null) {
57618
+ return;
57619
+ }
57620
+ const next = board.slice();
57621
+ next[index] = mark;
57622
+ const win = checkWinner(next);
57623
+ const draw = win === null && next.every((cell) => cell !== null);
57624
+ return {
57625
+ board: next,
57626
+ turn: win === null && !draw ? mark === "X" ? "O" : "X" : mark,
57627
+ winner: win?.winner ?? null,
57628
+ winLine: win?.line ?? null,
57629
+ draw,
57630
+ cursor: index
57631
+ };
57632
+ }
57633
+ function freshGame() {
57634
+ return { board: emptyBoard(), turn: "X", winner: null, winLine: null, draw: false, cursor: 4 };
57635
+ }
57636
+ function isGameOver(state) {
57637
+ return state.winner !== null || state.draw;
57638
+ }
57447
57639
  function bestLocalMove(board, mark) {
57448
57640
  const free = freeCells(board);
57449
57641
  if (free.length === 0) {
@@ -57466,28 +57658,6 @@ function bestLocalMove(board, mark) {
57466
57658
  }
57467
57659
  return free[0];
57468
57660
  }
57469
- function placeMark(board, index, mark) {
57470
- if (index < 0 || index > 8 || board[index] !== null) {
57471
- return;
57472
- }
57473
- const next = board.slice();
57474
- next[index] = mark;
57475
- const win = checkWinner(next);
57476
- const draw = win === null && next.every((cell) => cell !== null);
57477
- return {
57478
- board: next,
57479
- turn: win === null && !draw ? mark === "X" ? "O" : "X" : mark,
57480
- winner: win?.winner ?? null,
57481
- winLine: win?.line ?? null,
57482
- draw
57483
- };
57484
- }
57485
- function freshGame() {
57486
- return { board: emptyBoard(), turn: "X", winner: null, winLine: null, draw: false };
57487
- }
57488
- function isGameOver(state) {
57489
- return state.winner !== null || state.draw;
57490
- }
57491
57661
  function parseModelMove(reply, board) {
57492
57662
  if (reply.length === 0) {
57493
57663
  return;
@@ -57499,6 +57669,11 @@ function parseModelMove(reply, board) {
57499
57669
  const index = Number(match[1]);
57500
57670
  return board[index] === null ? index : undefined;
57501
57671
  }
57672
+ function asTtt(state) {
57673
+ return state;
57674
+ }
57675
+
57676
+ // src/tui/games/tic-tac-toe/prompts.ts
57502
57677
  function gameSystemPrompt() {
57503
57678
  return [
57504
57679
  "You are playing tic-tac-toe as O against a human playing X.",
@@ -57520,31 +57695,43 @@ ${rows.join(`
57520
57695
 
57521
57696
  Make your move. Reply with one digit 0-8.`;
57522
57697
  }
57523
- async function modelMove(board, opts = {}) {
57524
- const turn = await runModelTurn({
57525
- system: gameSystemPrompt(),
57526
- user: gameUserPrompt(board),
57527
- ...opts.provider !== undefined ? { provider: opts.provider } : {},
57528
- ...opts.model !== undefined ? { model: opts.model } : {},
57529
- maxOutputTokens: 256,
57530
- requestId: "keryx-game",
57531
- ...opts.providerFactory !== undefined ? { providerFactory: opts.providerFactory } : {},
57532
- ...opts.env !== undefined ? { env: opts.env } : {}
57533
- });
57534
- if (turn.error !== undefined) {
57535
- return { move: undefined, error: `model error: ${turn.error.message}` };
57698
+
57699
+ // src/tui/games/tic-tac-toe/input.ts
57700
+ function wrapCursor(row, col) {
57701
+ return (row + 3) % 3 * 3 + (col + 3) % 3;
57702
+ }
57703
+ function onKey(state, key) {
57704
+ const name = key.name || key.sequence;
57705
+ if (isGameOver(state)) {
57706
+ return;
57536
57707
  }
57537
- if (!turn.credentialAvailable && opts.providerFactory === undefined) {
57538
- return { move: undefined, error: "no model credential \u2014 configure a provider first (/provider)" };
57708
+ const row = Math.floor(state.cursor / 3);
57709
+ const col = state.cursor % 3;
57710
+ if (name === "up" || name === "k") {
57711
+ return { ...state, cursor: wrapCursor(row - 1, col) };
57539
57712
  }
57540
- return { move: parseModelMove(turn.text, board), error: undefined };
57713
+ if (name === "down" || name === "j") {
57714
+ return { ...state, cursor: wrapCursor(row + 1, col) };
57715
+ }
57716
+ if (name === "left" || name === "h") {
57717
+ return { ...state, cursor: wrapCursor(row, col - 1) };
57718
+ }
57719
+ if (name === "right" || name === "l") {
57720
+ return { ...state, cursor: wrapCursor(row, col + 1) };
57721
+ }
57722
+ if (name === "return" || name === "enter" || name === "space" || name === " ") {
57723
+ if (state.turn !== "X") {
57724
+ return;
57725
+ }
57726
+ return placeMark(state.board, state.cursor, "X");
57727
+ }
57728
+ return;
57541
57729
  }
57542
- var GAME_FOOTER = [
57543
- { key: "\u2190\u2191\u2193\u2192", label: "move" },
57544
- { key: "enter", label: "place" },
57545
- { key: "r", label: "new game" },
57546
- { key: "esc", label: "minimize" }
57547
- ];
57730
+ function markFor(player) {
57731
+ return player === "human" ? "X" : "O";
57732
+ }
57733
+
57734
+ // src/tui/games/tic-tac-toe/layout.ts
57548
57735
  var GAME_CELL_GAP = 1;
57549
57736
  var GAME_BOARD_CHROME_X = 4;
57550
57737
  var GAME_CELL_SIZES = {
@@ -57558,7 +57745,243 @@ function resolveCellSize(availableWidth) {
57558
57745
  const large = GAME_CELL_SIZES.large;
57559
57746
  return gameBoardWidth(large.width) <= availableWidth ? large : GAME_CELL_SIZES.small;
57560
57747
  }
57561
- var GAME_MODEL_TIMEOUT_MS = 12000;
57748
+
57749
+ // src/tui/games/tic-tac-toe/render.ts
57750
+ function markColor(mark, theme) {
57751
+ return mark === "X" ? theme.ok ?? "" : theme.error ?? "";
57752
+ }
57753
+ function statusText(state) {
57754
+ if (state.winner !== null) {
57755
+ return `${state.winner} wins! (r \u2014 new game)`;
57756
+ }
57757
+ if (state.draw) {
57758
+ return "Draw! (r \u2014 new game)";
57759
+ }
57760
+ return state.turn === "X" ? "Your turn \u2014 X" : "Agent's turn \u2014 O";
57761
+ }
57762
+ function render2(state, ctx) {
57763
+ const core = ctx.core;
57764
+ const r = ctx.renderer;
57765
+ const theme = ctx.theme;
57766
+ const cellSize = resolveCellSize(ctx.width);
57767
+ const wrap2 = new core.BoxRenderable(r, {
57768
+ id: "game-wrap",
57769
+ width: "100%",
57770
+ flexDirection: "column",
57771
+ alignItems: "center"
57772
+ });
57773
+ const legend = new core.BoxRenderable(r, {
57774
+ id: "game-legend",
57775
+ flexDirection: "row",
57776
+ marginBottom: 1
57777
+ });
57778
+ legend.add(new core.TextRenderable(r, { id: "game-legend-x", content: "X you", fg: theme.ok }));
57779
+ legend.add(new core.TextRenderable(r, { id: "game-legend-sep", content: " \xB7 ", fg: theme.muted }));
57780
+ legend.add(new core.TextRenderable(r, { id: "game-legend-o", content: "O model", fg: theme.error }));
57781
+ wrap2.add(legend);
57782
+ const board = new core.BoxRenderable(r, {
57783
+ id: "game-board",
57784
+ flexDirection: "column",
57785
+ flexShrink: 0,
57786
+ border: true,
57787
+ borderStyle: "rounded",
57788
+ borderColor: theme.border,
57789
+ backgroundColor: theme.panel,
57790
+ paddingLeft: 1,
57791
+ paddingRight: 1
57792
+ });
57793
+ for (let row = 0;row < 3; row++) {
57794
+ const rowBox = new core.BoxRenderable(r, {
57795
+ id: `game-row-${row}`,
57796
+ flexDirection: "row",
57797
+ flexShrink: 0,
57798
+ gap: 1
57799
+ });
57800
+ for (let col = 0;col < 3; col++) {
57801
+ const index = row * 3 + col;
57802
+ const cell = state.board[index] ?? null;
57803
+ const isCursor = index === state.cursor && !isGameOver(state);
57804
+ const won = state.winLine?.includes(index) === true;
57805
+ const cellBox = new core.BoxRenderable(r, {
57806
+ id: `game-cell-${index}`,
57807
+ width: cellSize.width,
57808
+ height: cellSize.height,
57809
+ flexShrink: 0,
57810
+ flexGrow: 0,
57811
+ flexDirection: "row",
57812
+ alignItems: "center",
57813
+ justifyContent: "center",
57814
+ border: true,
57815
+ borderStyle: "rounded",
57816
+ borderColor: won ? markColor(state.winner ?? "X", theme) : isCursor ? theme.focus : theme.border,
57817
+ backgroundColor: isCursor || won ? theme.highlight : undefined
57818
+ });
57819
+ const cellText = new core.TextRenderable(r, {
57820
+ id: `game-cell-text-${index}`,
57821
+ content: cell ?? "\xB7",
57822
+ fg: cell === null ? theme.muted : markColor(cell, theme)
57823
+ });
57824
+ cellBox.add(cellText);
57825
+ rowBox.add(cellBox);
57826
+ }
57827
+ board.add(rowBox);
57828
+ }
57829
+ wrap2.add(board);
57830
+ wrap2.add(new core.TextRenderable(r, { id: "game-status", content: statusText(state), marginTop: 1 }));
57831
+ ctx.parent.add(wrap2);
57832
+ }
57833
+
57834
+ // src/tui/games/tic-tac-toe/game.ts
57835
+ function statusOf2(s) {
57836
+ if (s.winner !== null) {
57837
+ return `${s.winner} wins! (r \u2014 new game)`;
57838
+ }
57839
+ if (s.draw) {
57840
+ return "Draw! (r \u2014 new game)";
57841
+ }
57842
+ return s.turn === "X" ? "Your turn \u2014 X" : "Agent's turn \u2014 O";
57843
+ }
57844
+ var ticTacToeGame = {
57845
+ id: "tic-tac-toe",
57846
+ label: "Tic-tac-toe",
57847
+ fresh: () => freshGame(),
57848
+ turn: (state) => asTtt(state).turn === "X" ? "human" : "model",
57849
+ isOver: (state) => isGameOver(asTtt(state)),
57850
+ outcome: (state) => {
57851
+ const s = asTtt(state);
57852
+ if (s.winner === "X") {
57853
+ return "human";
57854
+ }
57855
+ if (s.winner === "O") {
57856
+ return "model";
57857
+ }
57858
+ return s.draw ? "draw" : null;
57859
+ },
57860
+ status: (state) => statusOf2(asTtt(state)),
57861
+ systemPrompt: () => gameSystemPrompt(),
57862
+ stateForModel: (state) => gameUserPrompt(asTtt(state).board),
57863
+ parseMove: (reply, state) => parseModelMove(reply, asTtt(state).board),
57864
+ applyMove: (state, move, by) => placeMark(asTtt(state).board, move, markFor(by)),
57865
+ localMove: (state, by) => bestLocalMove(asTtt(state).board, markFor(by)),
57866
+ pass: (state) => {
57867
+ const s = asTtt(state);
57868
+ return { ...s, turn: s.turn === "X" ? "O" : "X" };
57869
+ },
57870
+ onKey: (state, key) => onKey(asTtt(state), key),
57871
+ render: (state, ctx) => render2(asTtt(state), ctx)
57872
+ };
57873
+ // src/tui/games/model-turn.ts
57874
+ init_single_turn();
57875
+ async function runGameModelTurn(game, state, opts) {
57876
+ const startedAt = performance.now();
57877
+ const turn = await runModelTurn({
57878
+ system: game.systemPrompt(),
57879
+ user: game.stateForModel(state),
57880
+ ...opts.provider !== undefined ? { provider: opts.provider } : {},
57881
+ ...opts.model !== undefined ? { model: opts.model } : {},
57882
+ maxOutputTokens: 256,
57883
+ requestId: "keryx-game",
57884
+ ...opts.providerFactory !== undefined ? { providerFactory: opts.providerFactory } : {},
57885
+ ...opts.env !== undefined ? { env: opts.env } : {}
57886
+ });
57887
+ const totalMs = Math.max(0, Math.round(performance.now() - startedAt));
57888
+ const stats = {
57889
+ provider: turn.provider,
57890
+ model: turn.model,
57891
+ latencyMs: turn.latencyMs,
57892
+ totalMs,
57893
+ inputTokens: turn.usage?.inputTokens,
57894
+ outputTokens: turn.usage?.outputTokens,
57895
+ reasoning: turn.reasoning === true,
57896
+ localFallback: false,
57897
+ error: turn.error !== undefined
57898
+ };
57899
+ if (turn.error !== undefined) {
57900
+ return { move: undefined, error: `model error: ${turn.error.message}`, stats };
57901
+ }
57902
+ if (!turn.credentialAvailable && opts.providerFactory === undefined) {
57903
+ return { move: undefined, error: "no model credential \u2014 configure a provider first (/provider)", stats };
57904
+ }
57905
+ return { move: game.parseMove(turn.text, state), error: undefined, stats };
57906
+ }
57907
+
57908
+ // src/tui/games/agent-panel.ts
57909
+ var MODEL_MAX = 22;
57910
+ function truncate4(value, max) {
57911
+ return value.length > max ? `${value.slice(0, max - 1)}\u2026` : value;
57912
+ }
57913
+ function renderAgentPanel(game, parent, core, renderer, args2) {
57914
+ const theme = getTheme();
57915
+ const box = (opts) => new core.BoxRenderable(renderer, opts);
57916
+ const text = (opts) => new core.TextRenderable(renderer, opts);
57917
+ const card = (id, extra = {}) => box({
57918
+ id,
57919
+ flexDirection: "column",
57920
+ border: true,
57921
+ borderStyle: "rounded",
57922
+ borderColor: theme.border,
57923
+ backgroundColor: theme.panel,
57924
+ paddingLeft: 1,
57925
+ paddingRight: 1,
57926
+ ...extra
57927
+ });
57928
+ const statRow = (owner, label, value, id, valueFg = theme.text) => {
57929
+ const rowBox = box({ flexDirection: "row", gap: 1 });
57930
+ rowBox.add(text({ content: label, fg: theme.muted }));
57931
+ rowBox.add(text({ id, content: value, fg: valueFg }));
57932
+ owner.add(rowBox);
57933
+ };
57934
+ const idle = args2.notice === undefined || args2.notice === "";
57935
+ const statusCard = card("game-status-card", { width: "100%", marginTop: 1 });
57936
+ statusCard.add(text({
57937
+ id: "game-notice",
57938
+ content: args2.modelBusy ? "agent is thinking\u2026" : idle ? "waiting for your move" : args2.notice,
57939
+ fg: args2.modelBusy ? theme.focus : idle ? theme.text : theme.error
57940
+ }));
57941
+ parent.add(statusCard);
57942
+ const sysLines = game.systemPrompt().split(`
57943
+ `);
57944
+ const sysShown = sysLines.length > 6 ? [...sysLines.slice(0, 6), `\u2026 (+${sysLines.length - 6} more)`] : sysLines;
57945
+ const sysCard = card("game-system-card", { width: "100%", marginTop: 1 });
57946
+ sysCard.add(text({ id: "game-system-title", content: "system prompt", fg: theme.muted }));
57947
+ sysCard.add(text({ id: "game-system", content: sysShown.join(`
57948
+ `), fg: theme.muted }));
57949
+ parent.add(sysCard);
57950
+ const lt = args2.lastTurn;
57951
+ const tot = args2.totals;
57952
+ const statsRow = box({ id: "game-stats-row", width: "100%", flexDirection: "row", gap: 1, marginTop: 1 });
57953
+ const lastTurnCard = card("game-last-turn", { flexGrow: 1, flexShrink: 0 });
57954
+ lastTurnCard.add(text({ id: "game-last-turn-title", content: "last turn", fg: theme.muted }));
57955
+ if (lt === undefined) {
57956
+ lastTurnCard.add(text({ id: "game-stats-empty", content: "no turns yet", fg: theme.muted }));
57957
+ } else {
57958
+ const model = lt.provider === "\u2013" ? "\u2013" : truncate4(`${lt.provider}/${lt.model}`, MODEL_MAX);
57959
+ statRow(lastTurnCard, "model", model, "game-stats-model");
57960
+ statRow(lastTurnCard, "first byte", formatMs(lt.latencyMs), "game-stats-latency");
57961
+ statRow(lastTurnCard, "total", formatMs(lt.totalMs), "game-stats-total");
57962
+ statRow(lastTurnCard, "tokens", `in ${formatTokens(lt.inputTokens)} \xB7 out ${formatTokens(lt.outputTokens)}`, "game-stats-tokens");
57963
+ if (lt.reasoning) {
57964
+ statRow(lastTurnCard, "reasoning", "yes", "game-stats-reasoning", theme.focus);
57965
+ }
57966
+ if (lt.localFallback) {
57967
+ statRow(lastTurnCard, "fallback", "local", "game-stats-fallback", theme.error);
57968
+ }
57969
+ if (lt.error) {
57970
+ statRow(lastTurnCard, "error", "yes", "game-stats-error", theme.error);
57971
+ }
57972
+ }
57973
+ const sessionCard = card("game-session", { flexGrow: 1, flexShrink: 0 });
57974
+ sessionCard.add(text({ id: "game-session-title", content: "session", fg: theme.muted }));
57975
+ statRow(sessionCard, "turns", String(tot.turns), "game-session-turns");
57976
+ statRow(sessionCard, "fallbacks", String(tot.localFallbacks), "game-session-fallbacks");
57977
+ statRow(sessionCard, "errors", String(tot.errors), "game-session-errors");
57978
+ statRow(sessionCard, "tokens", `in ${formatTokens(tot.inputTokens)} \xB7 out ${formatTokens(tot.outputTokens)}`, "game-session-tokens");
57979
+ statsRow.add(lastTurnCard);
57980
+ statsRow.add(sessionCard);
57981
+ parent.add(statsRow);
57982
+ }
57983
+
57984
+ // src/tui/games/otui.ts
57562
57985
  function asOtui2(otui) {
57563
57986
  if (otui === undefined || otui === null) {
57564
57987
  return;
@@ -57569,60 +57992,90 @@ function asOtui2(otui) {
57569
57992
  }
57570
57993
  return cand;
57571
57994
  }
57572
- var currentGame = freshGame();
57573
- var modelBusy = false;
57574
- function resetGame() {
57575
- currentGame = freshGame();
57576
- modelBusy = false;
57577
- }
57578
- function markColor(mark) {
57579
- return mark === "X" ? getTheme().ok : getTheme().error;
57580
- }
57581
- function statusText(state) {
57582
- if (state.winner !== null) {
57583
- return `${state.winner} wins! (r \u2014 new game)`;
57584
- }
57585
- if (state.draw) {
57586
- return "Draw! (r \u2014 new game)";
57995
+
57996
+ // src/tui/games/modal.ts
57997
+ var DEFAULT_GAMES = [ticTacToeGame];
57998
+ function presentGamesModal(openModalFn, otui, chrome, options = {}, registry = createRegistry(DEFAULT_GAMES)) {
57999
+ const games = registry.games;
58000
+ const first = games[0];
58001
+ if (first === undefined) {
58002
+ return;
57587
58003
  }
57588
- return state.turn === "X" ? "Your turn \u2014 X" : "Agent's turn \u2014 O";
57589
- }
57590
- function presentGame(openModalFn, otui, chrome, options = {}) {
57591
- const renderer = options.renderer;
57592
58004
  const core = asOtui2(otui);
58005
+ const renderer = options.renderer;
57593
58006
  let handle;
57594
- let cells = [];
57595
- let statusBox;
57596
- let noticeBox;
57597
- let cursor = 4;
58007
+ let activeId = first.id;
58008
+ const states = new Map;
58009
+ const totals = new Map;
58010
+ const lastTurn = new Map;
57598
58011
  let notice;
58012
+ let modelBusy = false;
57599
58013
  let unsubscribeKey;
58014
+ let bodyRef;
58015
+ let bodyWidth = 0;
58016
+ const stateOf = (id) => {
58017
+ const game = registry.get(id);
58018
+ if (game === undefined) {
58019
+ throw new Error(`unknown game: ${id}`);
58020
+ }
58021
+ let state = states.get(id);
58022
+ if (state === undefined) {
58023
+ state = game.fresh();
58024
+ states.set(id, state);
58025
+ }
58026
+ return state;
58027
+ };
58028
+ const totalsOf = (id) => {
58029
+ let t = totals.get(id);
58030
+ if (t === undefined) {
58031
+ t = emptyTurnTotals();
58032
+ totals.set(id, t);
58033
+ }
58034
+ return t;
58035
+ };
57600
58036
  const paint = () => {
57601
- if (statusBox === undefined || noticeBox === undefined || cells.length !== 9) {
58037
+ if (core === undefined || bodyRef === undefined) {
57602
58038
  return;
57603
58039
  }
57604
- const theme = getTheme();
57605
- statusBox.content = statusText(currentGame);
57606
- statusBox.fg = theme.text;
57607
- if (modelBusy) {
57608
- noticeBox.content = "agent is thinking\u2026";
57609
- noticeBox.fg = theme.focus;
57610
- } else {
57611
- noticeBox.content = notice ?? "";
57612
- noticeBox.fg = theme.error;
58040
+ clearTranscriptChildren(bodyRef);
58041
+ const game = registry.get(activeId);
58042
+ if (game === undefined) {
58043
+ return;
57613
58044
  }
57614
- for (const [index, view] of cells.entries()) {
57615
- const cell = currentGame.board[index] ?? null;
57616
- const isCursor = index === cursor && !isGameOver(currentGame) && !modelBusy;
57617
- const won = currentGame.winLine?.includes(index) === true;
57618
- view.text.content = cell ?? "\xB7";
57619
- view.text.fg = cell === null ? theme.muted : markColor(cell);
57620
- view.box.borderColor = won ? markColor(currentGame.winner ?? "X") : isCursor ? theme.focus : theme.border;
57621
- view.box.backgroundColor = isCursor || won ? theme.highlight : undefined;
58045
+ const state = stateOf(activeId);
58046
+ const ctx = {
58047
+ core,
58048
+ renderer,
58049
+ theme: getTheme(),
58050
+ parent: bodyRef,
58051
+ width: bodyWidth
58052
+ };
58053
+ game.render(state, ctx);
58054
+ renderAgentPanel(game, bodyRef, core, renderer, {
58055
+ notice,
58056
+ modelBusy,
58057
+ lastTurn: lastTurn.get(activeId),
58058
+ totals: totalsOf(activeId)
58059
+ });
58060
+ };
58061
+ const restart = () => {
58062
+ const game = registry.get(activeId);
58063
+ if (game === undefined) {
58064
+ return;
57622
58065
  }
58066
+ states.set(activeId, game.fresh());
58067
+ totals.set(activeId, emptyTurnTotals());
58068
+ lastTurn.set(activeId, undefined);
58069
+ notice = undefined;
58070
+ paint();
57623
58071
  };
57624
58072
  const applyModelMove = async () => {
57625
- if (modelBusy || isGameOver(currentGame) || currentGame.turn !== "O") {
58073
+ const game = registry.get(activeId);
58074
+ if (game === undefined) {
58075
+ return;
58076
+ }
58077
+ const state = stateOf(activeId);
58078
+ if (modelBusy || game.isOver(state) || game.turn(state) !== "model") {
57626
58079
  return;
57627
58080
  }
57628
58081
  modelBusy = true;
@@ -57633,30 +58086,51 @@ function presentGame(openModalFn, otui, chrome, options = {}) {
57633
58086
  const deadline = new Promise((resolve3) => {
57634
58087
  timer = setTimeout(() => resolve3("timeout"), timeoutMs);
57635
58088
  });
57636
- const outcome = await Promise.race([
57637
- modelMove(currentGame.board, {
57638
- ...options.provider !== undefined ? { provider: options.provider } : {},
57639
- ...options.model !== undefined ? { model: options.model } : {},
57640
- ...options.providerFactory !== undefined ? { providerFactory: options.providerFactory } : {},
57641
- ...options.env !== undefined ? { env: options.env } : {}
57642
- }),
57643
- deadline
57644
- ]);
58089
+ const outcome = await Promise.race([runGameModelTurn(game, state, options), deadline]);
57645
58090
  if (timer !== undefined) {
57646
58091
  clearTimeout(timer);
57647
58092
  }
57648
58093
  modelBusy = false;
57649
58094
  const timedOut = outcome === "timeout";
57650
- const result = timedOut ? { move: undefined, error: undefined } : outcome;
57651
- let move = result.move;
58095
+ const result = timedOut ? undefined : outcome;
58096
+ let stats;
58097
+ if (timedOut) {
58098
+ stats = {
58099
+ provider: "\u2013",
58100
+ model: "\u2013",
58101
+ latencyMs: undefined,
58102
+ totalMs: timeoutMs,
58103
+ inputTokens: undefined,
58104
+ outputTokens: undefined,
58105
+ reasoning: false,
58106
+ localFallback: true,
58107
+ error: false
58108
+ };
58109
+ } else if (result !== undefined) {
58110
+ stats = result.stats;
58111
+ } else {
58112
+ stats = {
58113
+ provider: "\u2013",
58114
+ model: "\u2013",
58115
+ latencyMs: undefined,
58116
+ totalMs: undefined,
58117
+ inputTokens: undefined,
58118
+ outputTokens: undefined,
58119
+ reasoning: false,
58120
+ localFallback: true,
58121
+ error: true
58122
+ };
58123
+ }
58124
+ let move = result?.move;
57652
58125
  let playedLocally = false;
57653
- if (move === undefined && result.error === undefined) {
57654
- move = bestLocalMove(currentGame.board, "O");
58126
+ if (move === undefined && result?.error === undefined) {
58127
+ move = game.localMove(state, "model");
57655
58128
  playedLocally = move !== undefined;
58129
+ stats = { ...stats, localFallback: playedLocally };
57656
58130
  }
57657
- const placed = move === undefined ? undefined : placeMark(currentGame.board, move, "O");
57658
- currentGame = placed ?? { ...currentGame, turn: "X" };
57659
- if (result.error !== undefined) {
58131
+ const placed = move === undefined ? undefined : game.applyMove(state, move, "model");
58132
+ const next = placed ?? game.pass(state);
58133
+ if (result?.error !== undefined) {
57660
58134
  notice = `agent: ${result.error}`;
57661
58135
  } else if (timedOut && playedLocally) {
57662
58136
  notice = `agent timed out after ${Math.round(timeoutMs / 1000)}s \u2014 played a local move`;
@@ -57665,132 +58139,29 @@ function presentGame(openModalFn, otui, chrome, options = {}) {
57665
58139
  } else {
57666
58140
  notice = undefined;
57667
58141
  }
57668
- paint();
57669
- };
57670
- const userPlace = () => {
57671
- if (modelBusy || isGameOver(currentGame) || currentGame.turn !== "X") {
57672
- return;
57673
- }
57674
- const placed = placeMark(currentGame.board, cursor, "X");
57675
- if (placed === undefined) {
57676
- return;
57677
- }
57678
- currentGame = placed;
57679
- notice = undefined;
57680
- paint();
57681
- if (!isGameOver(currentGame) && currentGame.turn === "O") {
57682
- applyModelMove();
57683
- }
57684
- };
57685
- const moveCursor = (dr, dc) => {
57686
- if (modelBusy) {
57687
- return;
57688
- }
57689
- const row = Math.floor(cursor / 3);
57690
- const col = cursor % 3;
57691
- cursor = (row + dr + 3) % 3 * 3 + (col + dc + 3) % 3;
57692
- paint();
57693
- };
57694
- const restart = () => {
57695
- resetGame();
57696
- cursor = 4;
57697
- notice = undefined;
58142
+ states.set(activeId, next);
58143
+ totals.set(activeId, addTurn(totalsOf(activeId), stats));
58144
+ lastTurn.set(activeId, stats);
57698
58145
  paint();
57699
58146
  };
57700
58147
  handle = openModalFn(otui, chrome, {
57701
58148
  title: "/game",
57702
- tabs: [{ id: "game", label: "Tic-tac-toe" }],
57703
- footer: GAME_FOOTER,
57704
- onArrowKeys: (_key, direction) => {
57705
- moveCursor(0, direction === "left" ? -1 : 1);
57706
- return true;
58149
+ tabs: games.map((game) => ({ id: game.id, label: game.label })),
58150
+ footer: GAMES_FOOTER,
58151
+ onArrowKeys: (key, _direction) => {
58152
+ const game = registry.get(activeId);
58153
+ if (game === undefined || modelBusy) {
58154
+ return false;
58155
+ }
58156
+ const state = stateOf(activeId);
58157
+ return game.onKey(state, { name: key.name, sequence: key.sequence }) !== undefined;
57707
58158
  },
57708
58159
  renderTab: (_tabId, body, ctx) => {
57709
58160
  if (body === undefined || body === null) {
57710
58161
  return;
57711
58162
  }
57712
- const parent = body;
57713
- if (parent.add === undefined || core === undefined) {
57714
- return;
57715
- }
57716
- const theme = getTheme();
57717
- const r = renderer;
57718
- const cellSize = resolveCellSize(ctx.width);
57719
- const wrap2 = new core.BoxRenderable(r, {
57720
- id: "game-wrap",
57721
- width: "100%",
57722
- flexDirection: "column",
57723
- alignItems: "center"
57724
- });
57725
- const legend = new core.BoxRenderable(r, {
57726
- id: "game-legend",
57727
- flexDirection: "row",
57728
- marginBottom: 1
57729
- });
57730
- legend.add(new core.TextRenderable(r, { id: "game-legend-x", content: "X you", fg: theme.ok }));
57731
- legend.add(new core.TextRenderable(r, { id: "game-legend-sep", content: " \xB7 ", fg: theme.muted }));
57732
- legend.add(new core.TextRenderable(r, { id: "game-legend-o", content: "O model", fg: theme.error }));
57733
- wrap2.add(legend);
57734
- const board = new core.BoxRenderable(r, {
57735
- id: "game-board",
57736
- flexDirection: "column",
57737
- flexShrink: 0,
57738
- border: true,
57739
- borderStyle: "rounded",
57740
- borderColor: theme.border,
57741
- backgroundColor: theme.panel,
57742
- paddingLeft: 1,
57743
- paddingRight: 1
57744
- });
57745
- cells = [];
57746
- for (let row = 0;row < 3; row++) {
57747
- const rowBox = new core.BoxRenderable(r, {
57748
- id: `game-row-${row}`,
57749
- flexDirection: "row",
57750
- flexShrink: 0,
57751
- gap: GAME_CELL_GAP
57752
- });
57753
- for (let col = 0;col < 3; col++) {
57754
- const index = row * 3 + col;
57755
- const cellBox = new core.BoxRenderable(r, {
57756
- id: `game-cell-${index}`,
57757
- width: cellSize.width,
57758
- height: cellSize.height,
57759
- flexShrink: 0,
57760
- flexGrow: 0,
57761
- flexDirection: "row",
57762
- alignItems: "center",
57763
- justifyContent: "center",
57764
- border: true,
57765
- borderStyle: "rounded",
57766
- borderColor: theme.border
57767
- });
57768
- const cellText = new core.TextRenderable(r, {
57769
- id: `game-cell-text-${index}`,
57770
- content: "\xB7",
57771
- fg: theme.muted
57772
- });
57773
- cellBox.add(cellText);
57774
- rowBox.add(cellBox);
57775
- cells.push({ box: cellBox, text: cellText });
57776
- }
57777
- board.add(rowBox);
57778
- }
57779
- wrap2.add(board);
57780
- const status = new core.TextRenderable(r, {
57781
- id: "game-status",
57782
- content: "",
57783
- marginTop: 1
57784
- });
57785
- statusBox = status;
57786
- wrap2.add(status);
57787
- const noticeText = new core.TextRenderable(r, {
57788
- id: "game-notice",
57789
- content: ""
57790
- });
57791
- noticeBox = noticeText;
57792
- wrap2.add(noticeText);
57793
- parent.add(wrap2);
58163
+ bodyRef = body;
58164
+ bodyWidth = ctx.width;
57794
58165
  paint();
57795
58166
  },
57796
58167
  onClose: () => {
@@ -57803,39 +58174,92 @@ function presentGame(openModalFn, otui, chrome, options = {}) {
57803
58174
  if (options.onKeypress !== undefined) {
57804
58175
  unsubscribeKey = options.onKeypress((key) => {
57805
58176
  const token = key.name || key.sequence;
57806
- if (token === "up" || token === "k") {
57807
- moveCursor(-1, 0);
57808
- return;
57809
- }
57810
- if (token === "down" || token === "j") {
57811
- moveCursor(1, 0);
57812
- return;
57813
- }
57814
- if (token === "h") {
57815
- moveCursor(0, -1);
58177
+ if (token === "r" || token === "R") {
58178
+ restart();
57816
58179
  return;
57817
58180
  }
57818
- if (token === "l") {
57819
- moveCursor(0, 1);
58181
+ const game = registry.get(activeId);
58182
+ if (game === undefined || modelBusy) {
57820
58183
  return;
57821
58184
  }
57822
- if (token === "return" || token === "enter" || token === "space" || token === " ") {
57823
- userPlace();
58185
+ const state = stateOf(activeId);
58186
+ const next = game.onKey(state, key);
58187
+ if (next === undefined) {
57824
58188
  return;
57825
58189
  }
57826
- if (token === "r" || token === "R") {
57827
- restart();
58190
+ states.set(activeId, next);
58191
+ paint();
58192
+ if (!game.isOver(next) && game.turn(next) === "model") {
58193
+ applyModelMove();
57828
58194
  }
57829
58195
  });
57830
58196
  }
57831
58197
  return {
57832
58198
  close: () => handle?.close(),
57833
58199
  restart,
58200
+ activeGameId: () => activeId,
57834
58201
  modelThinking: () => modelBusy
57835
58202
  };
57836
58203
  }
57837
- function openGameModal(otui, chrome, options = {}) {
57838
- return presentGame((hostOtui, hostChrome, input2) => openModal(hostOtui, hostChrome, input2), otui, chrome, options);
58204
+ function openGamesModal(otui, chrome, options = {}) {
58205
+ return presentGamesModal((hostOtui, hostChrome, input2) => openModal(hostOtui, hostChrome, input2), otui, chrome, options);
58206
+ }
58207
+ // src/tui/balance-panel.ts
58208
+ init_providers();
58209
+ init_shell_config();
58210
+ function formatBalance(balance) {
58211
+ if (balance === undefined) {
58212
+ return "\u2014";
58213
+ }
58214
+ const amount = balance.remaining ?? balance.total;
58215
+ const symbol = balance.currency === "USD" ? "$" : balance.currency === "EUR" ? "\u20AC" : balance.currency === "GBP" ? "\xA3" : `${balance.currency} `;
58216
+ return `${symbol}${amount.toFixed(2)}`;
58217
+ }
58218
+ function mountBalancePanel(sidebarTop, otui, renderer, options) {
58219
+ const core = otui;
58220
+ const r = renderer;
58221
+ const env = envWithSavedApiKeys(options.env ?? process.env);
58222
+ let current;
58223
+ let textNode;
58224
+ const label = new core.TextRenderable(r, {
58225
+ id: "sb-balance-k",
58226
+ content: core.t`${core.dim("Balance")}`,
58227
+ marginTop: 1
58228
+ });
58229
+ sidebarTop.add(label);
58230
+ const value = new core.TextRenderable(r, {
58231
+ id: "sb-balance-v",
58232
+ content: core.t`${core.dim("\u2026")}`,
58233
+ onMouseDown: () => {
58234
+ refresh();
58235
+ }
58236
+ });
58237
+ textNode = value;
58238
+ sidebarTop.add(value);
58239
+ const paint = (balance) => {
58240
+ current = balance;
58241
+ if (textNode !== undefined) {
58242
+ textNode.content = core.t`${core.dim(formatBalance(balance))}`;
58243
+ }
58244
+ };
58245
+ const refresh = async () => {
58246
+ const provider = balanceCapableProvider(options.provider);
58247
+ if (provider === undefined) {
58248
+ paint(undefined);
58249
+ return;
58250
+ }
58251
+ const apiKey = providerApiKey(provider, env);
58252
+ if (apiKey === undefined) {
58253
+ paint(undefined);
58254
+ return;
58255
+ }
58256
+ const base = resolveProviderBaseUrl(provider, env);
58257
+ const withBase = { ...provider, baseUrl: base };
58258
+ const balance = await fetchProviderBalance(options.fetch ?? globalThis.fetch, withBase, apiKey, { ...options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {} });
58259
+ paint(balance);
58260
+ };
58261
+ refresh();
58262
+ return { refresh, current: () => current };
57839
58263
  }
57840
58264
 
57841
58265
  // src/tui/tui-shell.ts
@@ -58175,7 +58599,7 @@ function showComposerChoice(otui, r, dock, request) {
58175
58599
  } catch {}
58176
58600
  dock.visible = false;
58177
58601
  }
58178
- const onKey = (key) => {
58602
+ const onKey2 = (key) => {
58179
58603
  if (key.name === "escape") {
58180
58604
  finish(request.cancelId);
58181
58605
  key.preventDefault();
@@ -58222,7 +58646,7 @@ function showComposerChoice(otui, r, dock, request) {
58222
58646
  key.stopPropagation();
58223
58647
  }
58224
58648
  };
58225
- const unsub = onKeypress2(r, onKey);
58649
+ const unsub = onKeypress2(r, onKey2);
58226
58650
  optionsScroll.focus();
58227
58651
  });
58228
58652
  }
@@ -58663,6 +59087,18 @@ async function createShellChrome(otui, renderer, opts) {
58663
59087
  });
58664
59088
  main.add(scroll);
58665
59089
  const transcript = scroll.content;
59090
+ let liveStatus;
59091
+ const originalTranscriptAdd = transcript.add.bind(transcript);
59092
+ const originalTranscriptRemove = transcript.remove.bind(transcript);
59093
+ transcript.add = (child, index) => {
59094
+ if (liveStatus !== undefined && child !== liveStatus) {
59095
+ originalTranscriptRemove(liveStatus);
59096
+ const added = originalTranscriptAdd(child, index);
59097
+ originalTranscriptAdd(liveStatus);
59098
+ return added;
59099
+ }
59100
+ return originalTranscriptAdd(child, index);
59101
+ };
58666
59102
  const queueDock = new otui.BoxRenderable(r, {
58667
59103
  id: "queue-dock",
58668
59104
  flexShrink: 0,
@@ -58837,7 +59273,6 @@ async function createShellChrome(otui, renderer, opts) {
58837
59273
  const footerRight = new otui.TextRenderable(r, { id: "footer-right", content: otui.t`${otui.dim(opts.status)}` });
58838
59274
  footer.add(footerRight);
58839
59275
  main.add(footer);
58840
- let liveStatus;
58841
59276
  let busyPhase = "waiting for model";
58842
59277
  let busyStartedAt = 0;
58843
59278
  let spinIdx = 0;
@@ -58852,12 +59287,15 @@ async function createShellChrome(otui, renderer, opts) {
58852
59287
  }
58853
59288
  if (!busy) {
58854
59289
  footerLeft.content = otui.t`${otui.dim(opts.footerHint)}`;
59290
+ footerRight.content = otui.t`${otui.dim(opts.status)}`;
58855
59291
  return;
58856
59292
  }
58857
59293
  const frame = SPINNER[spinIdx % SPINNER.length] ?? "\u280B";
58858
59294
  const secs = ((Date.now() - busyStartedAt) / 1000).toFixed(1);
58859
59295
  const line = `${frame} ${busyPhase} \xB7 ${secs}s`;
58860
59296
  footerLeft.content = otui.t`${otui.yellow(line)}`;
59297
+ const perm = opts.permissionMode?.();
59298
+ footerRight.content = otui.t`${otui.dim(perm !== undefined && perm.length > 0 ? `mode ${perm}` : opts.status)}`;
58861
59299
  if (liveStatus !== undefined) {
58862
59300
  liveStatus.content = otui.t`${otui.dim(line)}`;
58863
59301
  }
@@ -61339,6 +61777,7 @@ function attachUsageIo(io, chrome) {
61339
61777
  totalOut += usage.outputTokens ?? 0;
61340
61778
  chrome.setHeaderMeta(`\u2191${fmtTokens(totalIn)} \u2193${fmtTokens(totalOut)}`);
61341
61779
  chrome.setContextTotal(totalIn + totalOut);
61780
+ chrome.setUsage?.(totalIn, totalOut);
61342
61781
  };
61343
61782
  return Object.assign(io, {
61344
61783
  resetUsage() {
@@ -61835,7 +62274,7 @@ function promptApiKeyStep(otui, r, opts) {
61835
62274
  const keyInput = new otui.InputRenderable(r, { id: "kp-input", placeholder: opts.placeholder ?? "sk-...", marginTop: 1 });
61836
62275
  box.add(keyInput);
61837
62276
  keyInput.focus();
61838
- const onKey = (key) => {
62277
+ const onKey2 = (key) => {
61839
62278
  if (key.name === "escape") {
61840
62279
  cleanup();
61841
62280
  resolve3({ kind: "back" });
@@ -61843,7 +62282,7 @@ function promptApiKeyStep(otui, r, opts) {
61843
62282
  key.stopPropagation();
61844
62283
  }
61845
62284
  };
61846
- const unsub = onKeypress4(r, onKey);
62285
+ const unsub = onKeypress4(r, onKey2);
61847
62286
  const cleanup = () => {
61848
62287
  unsub();
61849
62288
  r.root.remove(box);
@@ -61875,7 +62314,7 @@ function pickProviderStep(otui, r, detected) {
61875
62314
  });
61876
62315
  box.add(provSelect);
61877
62316
  provSelect.focus();
61878
- const onKey = (key) => {
62317
+ const onKey2 = (key) => {
61879
62318
  if (key.name === "escape") {
61880
62319
  cleanup();
61881
62320
  resolve3(undefined);
@@ -61883,7 +62322,7 @@ function pickProviderStep(otui, r, detected) {
61883
62322
  key.stopPropagation();
61884
62323
  }
61885
62324
  };
61886
- const unsub = onKeypress4(r, onKey);
62325
+ const unsub = onKeypress4(r, onKey2);
61887
62326
  const cleanup = () => {
61888
62327
  unsub();
61889
62328
  r.root.remove(box);
@@ -61985,7 +62424,7 @@ function pickModelInTui(otui, r, models) {
61985
62424
  sel.options = matches2.length > 0 ? matches2.map((m) => ({ name: m, description: "" })) : [{ name: NO_MATCH, description: "" }];
61986
62425
  filterLine.content = otui.t`${otui.dim(q.length > 0 ? `filter: ${filter} (${matches2.length}/${all.length})` : "type to filter \xB7 \u2191/\u2193 Enter \xB7 Esc to go back")}`;
61987
62426
  };
61988
- const onKey = (key) => {
62427
+ const onKey2 = (key) => {
61989
62428
  if (key.name === "escape") {
61990
62429
  cleanup();
61991
62430
  resolve3(undefined);
@@ -62008,7 +62447,7 @@ function pickModelInTui(otui, r, models) {
62008
62447
  key.stopPropagation();
62009
62448
  }
62010
62449
  };
62011
- const unsub = onKeypress4(r, onKey);
62450
+ const unsub = onKeypress4(r, onKey2);
62012
62451
  const cleanup = () => {
62013
62452
  unsub();
62014
62453
  r.root.remove(box);
@@ -62077,7 +62516,7 @@ function pickSessionInTui(otui, r, sessions) {
62077
62516
  sel.selectedIndex = 0;
62078
62517
  };
62079
62518
  apply();
62080
- const onKey = (key) => {
62519
+ const onKey2 = (key) => {
62081
62520
  if (key.name === "escape") {
62082
62521
  cleanup();
62083
62522
  resolve3(undefined);
@@ -62100,7 +62539,7 @@ function pickSessionInTui(otui, r, sessions) {
62100
62539
  key.stopPropagation();
62101
62540
  }
62102
62541
  };
62103
- const unsub = onKeypress4(r, onKey);
62542
+ const unsub = onKeypress4(r, onKey2);
62104
62543
  const cleanup = () => {
62105
62544
  unsub();
62106
62545
  r.root.remove(box);
@@ -62193,6 +62632,7 @@ async function launchTuiAgentShell(opts) {
62193
62632
  placeholder: "type a task or / for commands \xB7 Enter send \xB7 Shift+Enter newline",
62194
62633
  commands: commandsForMode("agent"),
62195
62634
  headerMeta: "\u21910 \u21930",
62635
+ permissionMode: () => permissionMode,
62196
62636
  filterCommands: (query) => filterCommands(query, "agent"),
62197
62637
  ...opts.versionCheck !== undefined ? { versionCheck: opts.versionCheck } : {}
62198
62638
  });
@@ -62220,6 +62660,12 @@ async function launchTuiAgentShell(opts) {
62220
62660
  sidebar.add(new otui.TextRenderable(r, { id: "sb-model-k", content: otui.t`${otui.dim("Model")}`, marginTop: 1 }));
62221
62661
  const sbModelV = new otui.TextRenderable(r, { id: "sb-model-v", content: otui.t`${otui.dim(`${sel.provider}/${sel.model}`)}` });
62222
62662
  sidebar.add(sbModelV);
62663
+ sidebar.add(new otui.TextRenderable(r, { id: "sb-usage-k", content: otui.t`${otui.dim("Usage")}`, marginTop: 1 }));
62664
+ const sbUsageV = new otui.TextRenderable(r, { id: "sb-usage-v", content: otui.t`${otui.dim("\u21910 \u21930")}` });
62665
+ sidebar.add(sbUsageV);
62666
+ const balancePanel = mountBalancePanel(sidebar, otui, r, {
62667
+ provider: sel.provider
62668
+ });
62223
62669
  mountCwdPanel(otui, r, sidebar, opts.session?.cwd ?? process.cwd());
62224
62670
  sidebar.add(new otui.TextRenderable(r, { id: "sb-workspace-k", content: otui.t`${otui.dim("Workspace")}`, marginTop: 1 }));
62225
62671
  const sbWorkspaceV = new otui.TextRenderable(r, {
@@ -62461,6 +62907,9 @@ async function launchTuiAgentShell(opts) {
62461
62907
  },
62462
62908
  onExactUsage: () => {
62463
62909
  hasExactUsage = true;
62910
+ },
62911
+ setUsage: (input3, output2) => {
62912
+ sbUsageV.content = otui.t`${otui.dim(`\u2191${fmtTokens(input3)} \u2193${fmtTokens(output2)}`)}`;
62464
62913
  }
62465
62914
  });
62466
62915
  let lastUsage;
@@ -63015,10 +63464,12 @@ Staying in the current session.
63015
63464
  });
63016
63465
  })();
63017
63466
  };
63018
- const showGame = () => {
63019
- openGameModal(otui, chrome, {
63467
+ const showGame = (line) => {
63468
+ const timeoutMatch = /\/game\s+(\d+)/.exec(line);
63469
+ openGamesModal(otui, chrome, {
63020
63470
  renderer: r,
63021
- ...inspectorKeys
63471
+ ...inspectorKeys,
63472
+ ...timeoutMatch !== null ? { timeoutMs: Math.max(1, Number(timeoutMatch[1])) * 1000 } : {}
63022
63473
  });
63023
63474
  };
63024
63475
  const showWorkspace = () => {
@@ -63683,7 +64134,7 @@ Staying in the current session.
63683
64134
  return;
63684
64135
  }
63685
64136
  case "game": {
63686
- showGame();
64137
+ showGame(line);
63687
64138
  return;
63688
64139
  }
63689
64140
  case "deferred": {
@@ -63946,7 +64397,7 @@ ${formatThemeList(getThemeId())}`);
63946
64397
  return;
63947
64398
  }
63948
64399
  if (command.name === "/game") {
63949
- showGame();
64400
+ showGame(line);
63950
64401
  return;
63951
64402
  }
63952
64403
  if (command.name === "/mode") {
@@ -64376,6 +64827,9 @@ async function mountChatShell(otui, renderer, opts) {
64376
64827
  sidebar.add(new otui.TextRenderable(r, { id: "sb-model-k", content: otui.t`${otui.dim("Model")}`, marginTop: 1 }));
64377
64828
  const sbModel = new otui.TextRenderable(r, { id: "sb-model-v", content: otui.t`${otui.dim(label())}` });
64378
64829
  sidebar.add(sbModel);
64830
+ mountBalancePanel(sidebar, otui, r, {
64831
+ provider: selection.provider
64832
+ });
64379
64833
  sidebar.add(new otui.TextRenderable(r, { id: "sb-ctx-k", content: otui.t`${otui.dim("Context")}`, marginTop: 1 }));
64380
64834
  const sbContext = new otui.TextRenderable(r, { id: "sb-ctx-v", content: otui.t`${otui.dim("~0 tokens (est)")}` });
64381
64835
  sidebar.add(sbContext);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.61",
3
+ "version": "0.2.63",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {