zidane 5.1.0 → 5.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui.js CHANGED
@@ -1,16 +1,16 @@
1
1
  import { S as resolvePersistDir, b as cleanupPersistedSession, d as createAgent } from "./tools-d1yeA6xK.js";
2
2
  import { s as McpOAuthProvider, t as connectMcpServers } from "./mcp-BgwK6ySj.js";
3
- import { C as summaryToTurn, a as selectFilesFromSession, r as buildPostCompactAttachments, s as compactConversation, t as loginMcpServer } from "./login-BiuHyuEh.js";
3
+ import { C as summaryToTurn, a as selectFilesFromSession, r as buildPostCompactAttachments, s as compactConversation, t as loginMcpServer } from "./login-B7b7NNJQ.js";
4
4
  import { n as formatTokenUsage } from "./stats-DvCtBRwK.js";
5
5
  import { n as loadSession, t as createSession } from "./session-pS4Vt4dl.js";
6
6
  import { createTuiStore } from "./session/sqlite.js";
7
- import { $ as InteractionsProvider, $t as findGitRoot, C as useSafeModeQueue, Cn as detectAuth, D as isOnSafelist, E as getSafelist, F as supportsOAuth, Ft as deriveSessionTitle, Gt as toolResultText, Ht as stripSpawnTokensLine, I as buildModelCatalog, It as eventsFromTurns, J as useMcpAuthDispatch, Kt as buildUnifiedDiff, L as filterModelCatalog, Ln as getContextWindow, Lt as lastContextSizeFromTurns, Mt as useConfig, N as splitPromptSegments, Nt as resolveConfig, P as runOAuthLogin, R as indexOfEntry, Rt as listSessionMeta, S as useSafeModeActions, Sn as shouldAutoCompact, T as addToSafelist, Tt as resolveTheme, Un as piIdOf, V as discoverProjectMcps, Vt as selectableTurnIds, W as createFileMcpCredentialStore, Wt as toolCallPreview, X as getMcpAuthStatus, Xt as filetypeFromPath, Y as useMcpAuthState, Yt as extractEditPayload, _ as discoverProjectSkills, _t as DEFAULT_SETTINGS, a as ThemeProvider, an as matchesBinding, ar as buildBuildSystem, at as pendingInteractionsFromTurns, b as writeSessionExport, bn as useCompletion, bt as SettingsProvider, c as useSurfaces, ct as useInteractionsQueue, d as finalizeStreamingMarkdown, dn as createSkillsCompletionProvider, dt as ageString, f as finalizeStreamingMarkdownForOwner, fn as uniqueSkillNamesFromReferences, ft as compactPath, g as defaultSkillScanPaths, gt as useEnabledToggleSet, h as buildSkillsConfig, ht as listProjectFiles, i as turnAsText, it as makeRequestInteraction, j as suggestSafelistEntry, jt as ConfigProvider, kn as setProviderCredential, m as useStreamBuffer, mn as createFilesCompletionProvider, mt as shortId, n as deleteTurnSafely, nt as createInteractionTools, o as useColors, or as buildPlanSystem, p as turnContextSize, pt as fmtTokens, q as McpAuthProvider, r as truncateTurnsAt, rn as ensureKeybindingsFile, s as useSelectStyle, st as useInteractionsActions, tt as buildResumedToolResultsTurn, u as useTheme, ut as generateSessionTitle, vt as SETTINGS_CHOICES, wt as resolveChipColor, x as SafeModeProvider, xn as tryOpenBrowser, xt as useSettings, yt as SETTINGS_TOGGLES, z as buildMcpServers, zn as modelSupportsReasoning } from "./turn-operations-BzOIM6Of.js";
7
+ import { $ as getMcpAuthStatus, $t as toolResultText, A as isOnSafelist, An as tryOpenBrowser, B as filterModelCatalog, Bt as eventsFromTurns, C as writeSessionExport, Cn as createFilesCompletionProvider, Ct as SettingsProvider, E as useSafeModeQueue, Ft as ConfigProvider, Gt as listSessionMeta, H as buildMcpServers, Ht as isTurnHighlighted, I as splitPromptSegments, It as useConfig, Jn as modelSupportsReasoning, Kn as getContextWindow, L as runOAuthLogin, Lt as resolveConfig, Mn as detectAuth, O as addToSafelist, Ot as resolveChipColor, P as suggestSafelistEntry, Q as useMcpAuthState, Qn as piIdOf, Qt as toolCallPreview, R as supportsOAuth, Rn as setProviderCredential, St as SETTINGS_TOGGLES, T as useSafeModeActions, Tt as useSettings, Ut as isVisible, V as indexOfEntry, Vt as isEditErrorResult, W as discoverProjectMcps, Wt as lastContextSizeFromTurns, X as McpAuthProvider, Xt as stripSpawnTokensLine, Yt as selectableTurnIds, Z as useMcpAuthDispatch, _ as useStreamBuffer, _t as shortId, a as TOOL_DISPLAY, an as filetypeFromPath, at as createInteractionTools, b as discoverProjectSkills, bn as createSkillsCompletionProvider, bt as DEFAULT_SETTINGS, c as ThemeProvider, cn as findGitRoot, ct as pendingInteractionsFromTurns, d as useSurfaces, dt as useInteractionsQueue, en as turnSelectionOwnership, fn as ensureKeybindingsFile, g as turnContextSize, gr as buildPlanSystem, gt as fmtTokens, h as finalizeStreamingMarkdownForOwner, hr as buildBuildSystem, ht as compactPath, i as turnAsText, in as extractEditPayload, ir as accentColor, it as buildResumedToolResultsTurn, jn as shouldAutoCompact, k as getSafelist, kn as useCompletion, kt as resolveTheme, l as useColors, m as finalizeStreamingMarkdown, mn as matchesBinding, mt as ageString, n as deleteTurnSafely, nt as InteractionsProvider, o as displayNameFor, p as useTheme, pt as generateSessionTitle, q as createFileMcpCredentialStore, qt as marginTopFor, r as truncateTurnsAt, s as formatToolCall, st as makeRequestInteraction, tn as buildUnifiedDiff, u as useSelectStyle, ut as useInteractionsActions, v as buildSkillsConfig, vt as listProjectFiles, w as SafeModeProvider, wt as clampFps, xn as uniqueSkillNamesFromReferences, xt as SETTINGS_CHOICES, y as defaultSkillScanPaths, yt as useEnabledToggleSet, z as buildModelCatalog, zt as deriveSessionTitle } from "./turn-operations-2cgnXS2d.js";
8
8
  import { Buffer } from "node:buffer";
9
9
  import { homedir } from "node:os";
10
10
  import { createContext, createElement, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
11
- import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
12
11
  import { RGBA, SyntaxStyle, addDefaultParsers, createCliRenderer, defaultTextareaKeyBindings, getTreeSitterClient } from "@opentui/core";
13
12
  import { createRoot, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
13
+ import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
14
14
  //#region src/tui/modal.tsx
15
15
  const ModalContext = createContext(null);
16
16
  function ModalRoot({ children }) {
@@ -204,19 +204,6 @@ function EmptyState$1() {
204
204
  })]
205
205
  });
206
206
  }
207
- /**
208
- * Resolve a profile's `accent` token to a concrete theme color via the
209
- * caller's color palette. Exposed for the Footer badge so all surfaces
210
- * stay in sync with the picker's row tinting.
211
- */
212
- function accentColor(accent, COLOR) {
213
- switch (accent) {
214
- case "brand": return COLOR.brand;
215
- case "warn": return COLOR.warn;
216
- case "model": return COLOR.model;
217
- default: return COLOR.accent;
218
- }
219
- }
220
207
  //#endregion
221
208
  //#region src/tui/theme.ts
222
209
  /**
@@ -360,250 +347,6 @@ function useChipHighlights(textareaRef, references, chipStyle) {
360
347
  ]);
361
348
  }
362
349
  //#endregion
363
- //#region src/tui/tool-formatters.ts
364
- const TOOL_DISPLAY = {
365
- read_file: {
366
- displayName: "Read",
367
- format: (input) => {
368
- const path = stringField(input, "path");
369
- if (!path) return null;
370
- const meta = [];
371
- const offset = numberField(input, "offset");
372
- const limit = numberField(input, "limit");
373
- if (offset !== void 0 && limit !== void 0 && limit > 0) meta.push(`L${offset}–${offset + limit - 1}`);
374
- else if (offset !== void 0) meta.push(`from L${offset}`);
375
- else if (limit !== void 0 && limit > 0) meta.push(`${limit} lines`);
376
- return {
377
- target: path,
378
- meta
379
- };
380
- }
381
- },
382
- list_files: {
383
- displayName: "List",
384
- format: (input) => {
385
- return { target: stringField(input, "path") ?? "." };
386
- }
387
- },
388
- glob: {
389
- displayName: "Glob",
390
- format: (input) => {
391
- const pattern = stringField(input, "pattern");
392
- if (!pattern) return null;
393
- const meta = [];
394
- const limit = numberField(input, "limit");
395
- if (limit !== void 0) meta.push(`limit ${limit}`);
396
- return {
397
- target: pattern,
398
- meta
399
- };
400
- }
401
- },
402
- grep: {
403
- displayName: "Grep",
404
- format: (input) => {
405
- const pattern = stringField(input, "pattern");
406
- if (!pattern) return null;
407
- const target = `/${pattern}/`;
408
- const meta = [];
409
- const path = stringField(input, "path");
410
- if (path && path !== ".") meta.push(`in ${path}`);
411
- const glob = stringField(input, "glob");
412
- if (glob) meta.push(glob);
413
- const type = stringField(input, "type");
414
- if (type) meta.push(`type:${type}`);
415
- if (input["-i"] === true) meta.push("case-insensitive");
416
- const mode = stringField(input, "output_mode");
417
- if (mode && mode !== "files_with_matches") meta.push(mode);
418
- return {
419
- target,
420
- meta
421
- };
422
- }
423
- },
424
- shell: {
425
- displayName: "Shell",
426
- format: (input) => {
427
- const command = stringField(input, "command");
428
- if (!command) return null;
429
- return { target: truncate(command.trim(), 200) };
430
- }
431
- },
432
- edit: {
433
- displayName: "Edit",
434
- format: (input) => {
435
- const path = stringField(input, "path");
436
- if (!path) return null;
437
- return {
438
- target: path,
439
- meta: input.replace_all === true ? ["replace all"] : []
440
- };
441
- }
442
- },
443
- multi_edit: {
444
- displayName: "Multi-edit",
445
- format: (input) => {
446
- const path = stringField(input, "path");
447
- if (!path) return null;
448
- const edits = Array.isArray(input.edits) ? input.edits.length : 0;
449
- return {
450
- target: path,
451
- meta: edits > 0 ? [`${edits} hunk${edits === 1 ? "" : "s"}`] : []
452
- };
453
- }
454
- },
455
- write_file: {
456
- displayName: "Write",
457
- format: (input) => {
458
- const path = stringField(input, "path");
459
- if (!path) return null;
460
- const content = stringField(input, "content");
461
- const meta = [];
462
- if (content !== void 0) {
463
- const bytes = byteLengthUtf8(content);
464
- meta.push(`${formatBytes(bytes)}`);
465
- }
466
- return {
467
- target: path,
468
- meta
469
- };
470
- }
471
- },
472
- spawn: {
473
- displayName: "Spawn",
474
- format: (input) => {
475
- const task = stringField(input, "task");
476
- if (!task) return null;
477
- return { target: truncate(task.replace(/\s+/g, " ").trim(), 120) };
478
- }
479
- },
480
- tool_search: {
481
- displayName: "Search tools",
482
- format: (input) => {
483
- const query = stringField(input, "query");
484
- const names = Array.isArray(input.names) ? input.names.length : 0;
485
- if (query) return { target: `“${query}”` };
486
- if (names > 0) return { target: `${names} tool${names === 1 ? "" : "s"}` };
487
- return null;
488
- }
489
- },
490
- skills_use: {
491
- displayName: "Activate skill",
492
- format: (input) => {
493
- const name = stringField(input, "name");
494
- if (!name) return null;
495
- return { target: name };
496
- }
497
- },
498
- skills_read: {
499
- displayName: "Read skill",
500
- format: (input) => {
501
- const name = stringField(input, "name");
502
- const path = stringField(input, "path");
503
- if (!name) return null;
504
- return { target: path ? `${name}/${path}` : name };
505
- }
506
- },
507
- skills_run_script: {
508
- displayName: "Run script",
509
- format: (input) => {
510
- const name = stringField(input, "name");
511
- const script = stringField(input, "script");
512
- if (!name || !script) return null;
513
- const meta = [`skill ${name}`];
514
- const args = Array.isArray(input.args) ? input.args : null;
515
- if (args && args.length > 0) meta.push(truncate(args.map(String).join(" "), 80));
516
- return {
517
- target: script,
518
- meta
519
- };
520
- }
521
- },
522
- ask_user: {
523
- displayName: "Ask user",
524
- format: (input) => {
525
- const questions = Array.isArray(input.questions) ? input.questions.length : 0;
526
- if (questions === 0) return null;
527
- return { target: `${questions} question${questions === 1 ? "" : "s"}` };
528
- }
529
- },
530
- present_plan: {
531
- displayName: "Present plan",
532
- format: (input) => {
533
- const title = stringField(input, "title");
534
- if (!title) return null;
535
- return { target: title };
536
- }
537
- }
538
- };
539
- /**
540
- * Resolve the display verb for a tool. Native tools use their curated
541
- * entry from {@link TOOL_DISPLAY}; everything else gets a Title-Case
542
- * version of the raw name (`my_host_tool` → `My Host Tool`) so an MCP /
543
- * host tool still reads cleanly in the transcript.
544
- *
545
- * MCP convention: every tool surfaced by `mcp/connectMcpServers` is
546
- * namespaced as `mcp_<server>_<tool>` (see `src/mcp/index.ts`). The
547
- * `mcp_` prefix is plumbing — strip it before title-casing so the
548
- * label reads as `Github Create Issue` instead of `Mcp Github Create
549
- * Issue`. The server name becomes the leading words, which doubles as
550
- * a free visual grouping affordance ("everything starting with
551
- * `Github` came from the github MCP server").
552
- */
553
- function displayNameFor(name) {
554
- const entry = TOOL_DISPLAY[name];
555
- if (entry) return entry.displayName;
556
- return titleCase(name.startsWith("mcp_") ? name.slice(4) : name);
557
- }
558
- /**
559
- * Run a tool's curated formatter and return the result, or `null` when
560
- * no formatter is registered / the input shape doesn't match. Renderer
561
- * decides what to do with `null` — typically: show `↳ <displayName>`
562
- * with no target / meta tail.
563
- */
564
- function formatToolCall(name, input) {
565
- const entry = TOOL_DISPLAY[name];
566
- if (!entry) return null;
567
- try {
568
- return entry.format(input);
569
- } catch {
570
- return null;
571
- }
572
- }
573
- function stringField(input, key) {
574
- const v = input[key];
575
- return typeof v === "string" && v.length > 0 ? v : void 0;
576
- }
577
- function numberField(input, key) {
578
- const v = input[key];
579
- return typeof v === "number" && Number.isFinite(v) ? v : void 0;
580
- }
581
- /** `snake_case` / `kebab-case` / lowercase → `Title Case`. */
582
- function titleCase(s) {
583
- return s.split(/[-_\s]+/).filter(Boolean).map((w) => w[0]?.toUpperCase() + w.slice(1).toLowerCase()).join(" ");
584
- }
585
- function truncate(s, max) {
586
- return s.length <= max ? s : `${s.slice(0, max - 1)}…`;
587
- }
588
- function byteLengthUtf8(s) {
589
- let bytes = 0;
590
- for (let i = 0; i < s.length; i++) {
591
- const code = s.charCodeAt(i);
592
- if (code < 128) bytes += 1;
593
- else if (code < 2048) bytes += 2;
594
- else if (code >= 55296 && code <= 56319) {
595
- bytes += 4;
596
- i++;
597
- } else bytes += 3;
598
- }
599
- return bytes;
600
- }
601
- function formatBytes(bytes) {
602
- if (bytes < 1024) return `${bytes} B`;
603
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
604
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
605
- }
606
- //#endregion
607
350
  //#region src/tui/components.tsx
608
351
  /**
609
352
  * Memoized so a flush that mutates only the trailing event doesn't force the
@@ -967,39 +710,19 @@ function StatusSpinner({ color }) {
967
710
  children: useSpinnerFrame()
968
711
  });
969
712
  }
970
- /**
971
- * Minimum scrollbar thumb size, in half-block units (OpenTUI's
972
- * `SliderRenderable` renders the vertical thumb at 2 half-blocks per
973
- * character cell). `8` half-blocks = 4 character cells — always large
974
- * enough to read + grab with the mouse, never so large that it
975
- * dominates the track on short transcripts.
976
- */
977
- const MIN_THUMB_HALF_BLOCKS = 8;
978
713
  function Transcript({ events, settings, selectedTurnId = null }) {
714
+ const COLOR = useColors();
979
715
  const items = useMemo(() => partitionTranscript(events, settings), [events, settings]);
716
+ const ownership = useMemo(() => turnSelectionOwnership(events), [events]);
980
717
  const scrollboxRef = useRef(null);
981
- useEffect(() => {
982
- const scrollbox = scrollboxRef.current;
983
- if (!scrollbox) return;
984
- const slider = scrollbox.verticalScrollBar?.slider;
985
- if (!slider || typeof slider.getVirtualThumbSize !== "function") return;
986
- const original = slider.getVirtualThumbSize.bind(slider);
987
- slider.getVirtualThumbSize = function() {
988
- const upstream = original();
989
- const virtualTrackSize = slider.height * 2;
990
- return Math.min(virtualTrackSize, Math.max(MIN_THUMB_HALF_BLOCKS, upstream));
991
- };
992
- return () => {
993
- slider.getVirtualThumbSize = original;
994
- };
995
- }, []);
996
718
  const anchors = useMemo(() => computeTurnAnchors(items), [items]);
997
719
  useEffect(() => {
998
720
  if (!selectedTurnId) return;
999
721
  const scrollbox = scrollboxRef.current;
1000
722
  if (!scrollbox) return;
1001
723
  const handle = requestAnimationFrame(() => {
1002
- if (selectedTurnId === anchors.lastTurnId) {
724
+ const ownsLast = anchors.lastTurnId !== void 0 && ownership.get(anchors.lastTurnId) === selectedTurnId;
725
+ if (selectedTurnId === anchors.lastTurnId || ownsLast) {
1003
726
  scrollbox.scrollTop = scrollbox.scrollHeight;
1004
727
  return;
1005
728
  }
@@ -1007,7 +730,11 @@ function Transcript({ events, settings, selectedTurnId = null }) {
1007
730
  if (id) scrollbox.scrollChildIntoView(id);
1008
731
  });
1009
732
  return () => cancelAnimationFrame(handle);
1010
- }, [selectedTurnId, anchors]);
733
+ }, [
734
+ selectedTurnId,
735
+ anchors,
736
+ ownership
737
+ ]);
1011
738
  if (items.length === 0) return /* @__PURE__ */ jsx(EmptyState, {});
1012
739
  return /* @__PURE__ */ jsx("scrollbox", {
1013
740
  ref: scrollboxRef,
@@ -1019,10 +746,17 @@ function Transcript({ events, settings, selectedTurnId = null }) {
1019
746
  },
1020
747
  stickyScroll: true,
1021
748
  stickyStart: "bottom",
749
+ verticalScrollbarOptions: {
750
+ width: 1,
751
+ trackOptions: {
752
+ backgroundColor: "transparent",
753
+ foregroundColor: COLOR.mute
754
+ }
755
+ },
1022
756
  children: items.map((item, i) => item.kind === "event" ? /* @__PURE__ */ jsx(EventLine, {
1023
757
  event: item.event,
1024
758
  previous: item.previous,
1025
- selected: selectedTurnId !== null && item.event.turnId === selectedTurnId,
759
+ selected: isTurnHighlighted(item.event, selectedTurnId, ownership),
1026
760
  anchorId: anchors.ids[i][0]
1027
761
  }, i) : /* @__PURE__ */ jsx(SubagentBlock, {
1028
762
  events: item.events,
@@ -1068,51 +802,6 @@ function computeTurnAnchors(items) {
1068
802
  };
1069
803
  }
1070
804
  /**
1071
- * Per-event visibility — filters honor user toggles and the
1072
- * `hideSubagentOutput` setting. When subagent output is hidden:
1073
- * - Child-agent events are filtered down to the `spawn-start` /
1074
- * `spawn-end` markers so the user still sees "🌱 working… 🌳 done".
1075
- * - The parent's `tool-result` for `spawn` is hidden too. Its body
1076
- * duplicates `spawn-end`'s stats line *and* the parent's next markdown
1077
- * turn ("Here's what the sub-agent found: …"). Showing it again
1078
- * produced an extra `┃ [sub-agent child-1] Completed …` block that
1079
- * the user just wanted gone.
1080
- *
1081
- * Exported so the visibility matrix can be unit-tested without rendering.
1082
- */
1083
- /** Tools whose `tool-result` event is suppressed when `showEditDiffs` is on. */
1084
- const EDIT_TOOL_NAMES = new Set([
1085
- "edit",
1086
- "multi_edit",
1087
- "write_file"
1088
- ]);
1089
- function isVisible(event, settings) {
1090
- if (settings.hideSubagentOutput) {
1091
- if (isChild(event)) return event.kind === "spawn-start" || event.kind === "spawn-end";
1092
- if (event.kind === "tool-result" && event.tool === "spawn") return false;
1093
- }
1094
- if (settings.showEditDiffs && event.kind === "tool-result" && event.tool && EDIT_TOOL_NAMES.has(event.tool) && !isEditErrorResult(event.text)) return false;
1095
- switch (event.kind) {
1096
- case "thinking": return settings.showThinking;
1097
- case "tool": return settings.toolCallDisplay !== "hidden";
1098
- case "tool-result": return settings.showToolResults;
1099
- default: return true;
1100
- }
1101
- }
1102
- /**
1103
- * Recognize a tool-result body as a failure. The three edit tools all
1104
- * return short, deterministic error prefixes on the failure path:
1105
- * - `edit` → "Edit error: …"
1106
- * - `multi_edit` → "multi_edit error: …"
1107
- * - `write_file` → succeeds on permission errors only via exception →
1108
- * the loop wraps as "Tool failed: …" so we match that prefix too.
1109
- *
1110
- * Exported for unit-testability of the visibility matrix.
1111
- */
1112
- function isEditErrorResult(text) {
1113
- return text.startsWith("Edit error:") || text.startsWith("multi_edit error:") || text.startsWith("Tool failed:");
1114
- }
1115
- /**
1116
805
  * Walk the visible-event list once and group consecutive child events
1117
806
  * (`depth > 0`) into runs so we can wrap each run in a single bordered
1118
807
  * subagent box.
@@ -1238,55 +927,6 @@ function rowStyle(paddingLeft) {
1238
927
  alignSelf: "stretch"
1239
928
  };
1240
929
  }
1241
- /**
1242
- * Default top-margin per kind. Spacing intent:
1243
- * - `info` / `markdown` / `tool` / `error` / `spawn-start` open a new block
1244
- * so they each get one row of breathing room above.
1245
- * - `thinking` / `tool-result` / `spawn-end` continue the previous block
1246
- * and stay flush.
1247
- *
1248
- * Context-aware overrides live in `marginTopFor` — e.g. consecutive tool
1249
- * round-trips collapse to a tight list regardless of whether outputs are shown.
1250
- */
1251
- const MARGIN_TOP = {
1252
- "separator": 0,
1253
- "user-prompt": 1,
1254
- "info": 1,
1255
- "thinking": 0,
1256
- "tool": 1,
1257
- "tool-result": 0,
1258
- "error": 1,
1259
- "markdown": 1,
1260
- "spawn-start": 1,
1261
- "spawn-end": 0,
1262
- "compact-summary": 1
1263
- };
1264
- const TOOL_KINDS = new Set(["tool", "tool-result"]);
1265
- /**
1266
- * Resolve the top margin for an event given the one rendered just before it.
1267
- *
1268
- * Context-aware rules:
1269
- *
1270
- * - A `tool` / `tool-result` event right after another `tool` / `tool-result`
1271
- * collapses to a tight list — call→result pairs and back-to-back calls
1272
- * read as one logical block.
1273
- * - A parent-level event (`depth === 0`) right after a subagent event
1274
- * (`depth > 0`) collapses too. The subagent's `🌳` end marker (and, in
1275
- * show mode, the subagent box's bottom border) already provides the
1276
- * separation; adding the event's default `marginTop` on top would
1277
- * produce the visible "line jump" between a subagent's outcome and the
1278
- * parent's follow-up. Either form of marker is enough — we don't want
1279
- * both.
1280
- *
1281
- * Exported so the spacing matrix can be unit-tested without rendering.
1282
- */
1283
- function marginTopFor(event, previous) {
1284
- if (TOOL_KINDS.has(event.kind) && previous && TOOL_KINDS.has(previous.kind)) return 0;
1285
- const eventDepth = event.depth ?? 0;
1286
- const previousDepth = previous?.depth ?? 0;
1287
- if (eventDepth === 0 && previousDepth > 0) return 0;
1288
- return MARGIN_TOP[event.kind] ?? 0;
1289
- }
1290
930
  function EventLineImpl({ event, depthOffset = 0 }) {
1291
931
  const COLOR = useColors();
1292
932
  const { settings } = useSettings();
@@ -6487,6 +6127,11 @@ function AppShell() {
6487
6127
  useEffect(() => {
6488
6128
  safeModeEnabledRef.current = settings.safeMode;
6489
6129
  }, [settings.safeMode]);
6130
+ useEffect(() => {
6131
+ const fps = clampFps(settings.targetFps);
6132
+ if (renderer.targetFps !== fps) renderer.targetFps = fps;
6133
+ if (renderer.maxFps !== fps) renderer.maxFps = fps;
6134
+ }, [renderer, settings.targetFps]);
6490
6135
  const persistToolResultsRef = useRef(settings.persistToolResults);
6491
6136
  useEffect(() => {
6492
6137
  persistToolResultsRef.current = settings.persistToolResults;
@@ -7586,7 +7231,7 @@ function AppShell() {
7586
7231
  })();
7587
7232
  }, [modal, config.paths.userDir]);
7588
7233
  const hasMultipleAgents = useMemo(() => Object.keys(agentRegistry).length > 1, [agentRegistry]);
7589
- const turnIds = useMemo(() => selectableTurnIds(events), [events]);
7234
+ const turnIds = useMemo(() => selectableTurnIds(events, settings), [events, settings]);
7590
7235
  /** Drop the selection if its turn disappeared (session swap, history reset). */
7591
7236
  useEffect(() => {
7592
7237
  if (selectedTurnId && !turnIds.includes(selectedTurnId)) setSelectedTurnId(null);
@@ -8992,6 +8637,13 @@ let runTuiInvoked = false;
8992
8637
  * Env-var overrides (handy when launching from CI or restricted shells):
8993
8638
  * - `ZIDANE_STORAGE_DIR` — sets `storageDir`
8994
8639
  * - `ZIDANE_PREFIX` — sets `prefix`
8640
+ * - `ZIDANE_DEBUG` — enables `gatherStats` on the renderer and dumps the
8641
+ * final fps / frametime distribution to stderr on exit. Useful when
8642
+ * tuning the renderer fps setting or comparing terminal emulators.
8643
+ *
8644
+ * Renderer fps lives in Settings (`targetFps`, cycle `30 / 60 / 120`);
8645
+ * the on-disk value seeds the renderer at boot and `AppShell` mirrors
8646
+ * subsequent flips onto the live renderer.
8995
8647
  *
8996
8648
  * Hosts building on top of `zidane/tui` typically want their own env vars
8997
8649
  * (e.g. `MYAPP_STORAGE_DIR`); read them in your launch script and forward
@@ -9025,15 +8677,27 @@ async function runTui(options = {}) {
9025
8677
  const exited = new Promise((resolve) => {
9026
8678
  done = resolve;
9027
8679
  });
9028
- createRoot(await createCliRenderer({
8680
+ const bootFps = clampFps(config.initialSettings.targetFps ?? DEFAULT_SETTINGS.targetFps);
8681
+ const gatherStats = !!process.env.ZIDANE_DEBUG;
8682
+ const renderer = await createCliRenderer({
9029
8683
  exitOnCtrlC: true,
9030
8684
  onDestroy: () => done(),
9031
- debounceDelay: 0
9032
- })).render(/* @__PURE__ */ jsx(App, { config }));
8685
+ debounceDelay: 0,
8686
+ targetFps: bootFps,
8687
+ maxFps: bootFps,
8688
+ gatherStats
8689
+ });
8690
+ createRoot(renderer).render(/* @__PURE__ */ jsx(App, { config }));
9033
8691
  await exited;
8692
+ if (gatherStats) try {
8693
+ const s = renderer.getStats();
8694
+ process.stderr.write(`[zidane/tui] render stats: fps=${s.fps.toFixed(1)} avgFrame=${s.averageFrameTime.toFixed(2)}ms min=${s.minFrameTime.toFixed(2)}ms max=${s.maxFrameTime.toFixed(2)}ms frames=${s.frameCount}\n`);
8695
+ } catch (err) {
8696
+ process.stderr.write(`[zidane/tui] render stats unavailable: ${err instanceof Error ? err.message : String(err)}\n`);
8697
+ }
9034
8698
  process.exit(0);
9035
8699
  }
9036
8700
  //#endregion
9037
- export { AgentPickerModal, App, AuthScreen, ChatScreen, CompletionPopup, EffortPickerModal, Footer, InteractionBlock, McpsSettingsModal, Modal, ModalRoot, ModelPickerModal, SessionDetailsModal, SessionsScreen, SettingsModal, SkillsSettingsModal, Spinner, StatusSpinner, TitleOverlay, ToggleListModal, Transcript, TurnDetailsModal, accentColor, buildMdStyle, hintsLength, isVisible, marginTopFor, onInputSubmit, renderHintSpans, runTui, splitPromptSegments, useMdStyle, useModal, useModalAwareFocus };
8701
+ export { AgentPickerModal, App, AuthScreen, ChatScreen, CompletionPopup, EffortPickerModal, Footer, InteractionBlock, McpsSettingsModal, Modal, ModalRoot, ModelPickerModal, SessionDetailsModal, SessionsScreen, SettingsModal, SkillsSettingsModal, Spinner, StatusSpinner, TOOL_DISPLAY, TitleOverlay, ToggleListModal, Transcript, TurnDetailsModal, accentColor, buildMdStyle, displayNameFor, formatToolCall, hintsLength, isEditErrorResult, isTurnHighlighted, isVisible, marginTopFor, onInputSubmit, renderHintSpans, runTui, selectableTurnIds, splitPromptSegments, turnSelectionOwnership, useMdStyle, useModal, useModalAwareFocus };
9038
8702
 
9039
8703
  //# sourceMappingURL=tui.js.map