zidane 5.1.0 → 5.1.1

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-lsUMITng.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
@@ -968,15 +711,29 @@ function StatusSpinner({ color }) {
968
711
  });
969
712
  }
970
713
  /**
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.
714
+ * Scrollbar thumb sizing floors. Two-layered so the thumb stays useful
715
+ * across both tiny and tall terminal heights:
716
+ *
717
+ * - {@link MIN_THUMB_RATIO} soft floor as a fraction of the track.
718
+ * `0.2` means "always at least 20% of the visible scroll area".
719
+ * This is what keeps a medium-length transcript from collapsing
720
+ * to a 2-cell pill the moment content modestly exceeds the
721
+ * viewport — only a genuinely long conversation (where the
722
+ * natural ratio is < 20%) ever hits this floor.
723
+ * - {@link MIN_THUMB_HALF_BLOCKS} — hard floor in half-block units
724
+ * (OpenTUI renders the thumb at 2 half-blocks per character cell;
725
+ * `4` = 2 character cells). Catches the degenerate case of a tiny
726
+ * viewport where 20% rounds to zero, keeping the thumb grabbable
727
+ * even on a 5-row chat pane.
728
+ *
729
+ * Effective floor: `max(MIN_THUMB_HALF_BLOCKS, floor(trackSize * MIN_THUMB_RATIO))`.
976
730
  */
977
- const MIN_THUMB_HALF_BLOCKS = 8;
731
+ const MIN_THUMB_HALF_BLOCKS = 4;
732
+ const MIN_THUMB_RATIO = .2;
978
733
  function Transcript({ events, settings, selectedTurnId = null }) {
734
+ const COLOR = useColors();
979
735
  const items = useMemo(() => partitionTranscript(events, settings), [events, settings]);
736
+ const ownership = useMemo(() => turnSelectionOwnership(events), [events]);
980
737
  const scrollboxRef = useRef(null);
981
738
  useEffect(() => {
982
739
  const scrollbox = scrollboxRef.current;
@@ -987,7 +744,8 @@ function Transcript({ events, settings, selectedTurnId = null }) {
987
744
  slider.getVirtualThumbSize = function() {
988
745
  const upstream = original();
989
746
  const virtualTrackSize = slider.height * 2;
990
- return Math.min(virtualTrackSize, Math.max(MIN_THUMB_HALF_BLOCKS, upstream));
747
+ const softFloor = Math.floor(virtualTrackSize * MIN_THUMB_RATIO);
748
+ return Math.min(virtualTrackSize, Math.max(Math.max(MIN_THUMB_HALF_BLOCKS, softFloor), upstream));
991
749
  };
992
750
  return () => {
993
751
  slider.getVirtualThumbSize = original;
@@ -999,7 +757,8 @@ function Transcript({ events, settings, selectedTurnId = null }) {
999
757
  const scrollbox = scrollboxRef.current;
1000
758
  if (!scrollbox) return;
1001
759
  const handle = requestAnimationFrame(() => {
1002
- if (selectedTurnId === anchors.lastTurnId) {
760
+ const ownsLast = anchors.lastTurnId !== void 0 && ownership.get(anchors.lastTurnId) === selectedTurnId;
761
+ if (selectedTurnId === anchors.lastTurnId || ownsLast) {
1003
762
  scrollbox.scrollTop = scrollbox.scrollHeight;
1004
763
  return;
1005
764
  }
@@ -1007,7 +766,11 @@ function Transcript({ events, settings, selectedTurnId = null }) {
1007
766
  if (id) scrollbox.scrollChildIntoView(id);
1008
767
  });
1009
768
  return () => cancelAnimationFrame(handle);
1010
- }, [selectedTurnId, anchors]);
769
+ }, [
770
+ selectedTurnId,
771
+ anchors,
772
+ ownership
773
+ ]);
1011
774
  if (items.length === 0) return /* @__PURE__ */ jsx(EmptyState, {});
1012
775
  return /* @__PURE__ */ jsx("scrollbox", {
1013
776
  ref: scrollboxRef,
@@ -1019,10 +782,17 @@ function Transcript({ events, settings, selectedTurnId = null }) {
1019
782
  },
1020
783
  stickyScroll: true,
1021
784
  stickyStart: "bottom",
785
+ verticalScrollbarOptions: {
786
+ width: 1,
787
+ trackOptions: {
788
+ backgroundColor: "transparent",
789
+ foregroundColor: COLOR.mute
790
+ }
791
+ },
1022
792
  children: items.map((item, i) => item.kind === "event" ? /* @__PURE__ */ jsx(EventLine, {
1023
793
  event: item.event,
1024
794
  previous: item.previous,
1025
- selected: selectedTurnId !== null && item.event.turnId === selectedTurnId,
795
+ selected: isTurnHighlighted(item.event, selectedTurnId, ownership),
1026
796
  anchorId: anchors.ids[i][0]
1027
797
  }, i) : /* @__PURE__ */ jsx(SubagentBlock, {
1028
798
  events: item.events,
@@ -1068,51 +838,6 @@ function computeTurnAnchors(items) {
1068
838
  };
1069
839
  }
1070
840
  /**
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
841
  * Walk the visible-event list once and group consecutive child events
1117
842
  * (`depth > 0`) into runs so we can wrap each run in a single bordered
1118
843
  * subagent box.
@@ -1238,55 +963,6 @@ function rowStyle(paddingLeft) {
1238
963
  alignSelf: "stretch"
1239
964
  };
1240
965
  }
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
966
  function EventLineImpl({ event, depthOffset = 0 }) {
1291
967
  const COLOR = useColors();
1292
968
  const { settings } = useSettings();
@@ -6487,6 +6163,11 @@ function AppShell() {
6487
6163
  useEffect(() => {
6488
6164
  safeModeEnabledRef.current = settings.safeMode;
6489
6165
  }, [settings.safeMode]);
6166
+ useEffect(() => {
6167
+ const fps = clampFps(settings.targetFps);
6168
+ if (renderer.targetFps !== fps) renderer.targetFps = fps;
6169
+ if (renderer.maxFps !== fps) renderer.maxFps = fps;
6170
+ }, [renderer, settings.targetFps]);
6490
6171
  const persistToolResultsRef = useRef(settings.persistToolResults);
6491
6172
  useEffect(() => {
6492
6173
  persistToolResultsRef.current = settings.persistToolResults;
@@ -7586,7 +7267,7 @@ function AppShell() {
7586
7267
  })();
7587
7268
  }, [modal, config.paths.userDir]);
7588
7269
  const hasMultipleAgents = useMemo(() => Object.keys(agentRegistry).length > 1, [agentRegistry]);
7589
- const turnIds = useMemo(() => selectableTurnIds(events), [events]);
7270
+ const turnIds = useMemo(() => selectableTurnIds(events, settings), [events, settings]);
7590
7271
  /** Drop the selection if its turn disappeared (session swap, history reset). */
7591
7272
  useEffect(() => {
7592
7273
  if (selectedTurnId && !turnIds.includes(selectedTurnId)) setSelectedTurnId(null);
@@ -8992,6 +8673,13 @@ let runTuiInvoked = false;
8992
8673
  * Env-var overrides (handy when launching from CI or restricted shells):
8993
8674
  * - `ZIDANE_STORAGE_DIR` — sets `storageDir`
8994
8675
  * - `ZIDANE_PREFIX` — sets `prefix`
8676
+ * - `ZIDANE_DEBUG` — enables `gatherStats` on the renderer and dumps the
8677
+ * final fps / frametime distribution to stderr on exit. Useful when
8678
+ * tuning the renderer fps setting or comparing terminal emulators.
8679
+ *
8680
+ * Renderer fps lives in Settings (`targetFps`, cycle `30 / 60 / 120`);
8681
+ * the on-disk value seeds the renderer at boot and `AppShell` mirrors
8682
+ * subsequent flips onto the live renderer.
8995
8683
  *
8996
8684
  * Hosts building on top of `zidane/tui` typically want their own env vars
8997
8685
  * (e.g. `MYAPP_STORAGE_DIR`); read them in your launch script and forward
@@ -9025,15 +8713,27 @@ async function runTui(options = {}) {
9025
8713
  const exited = new Promise((resolve) => {
9026
8714
  done = resolve;
9027
8715
  });
9028
- createRoot(await createCliRenderer({
8716
+ const bootFps = clampFps(config.initialSettings.targetFps ?? DEFAULT_SETTINGS.targetFps);
8717
+ const gatherStats = !!process.env.ZIDANE_DEBUG;
8718
+ const renderer = await createCliRenderer({
9029
8719
  exitOnCtrlC: true,
9030
8720
  onDestroy: () => done(),
9031
- debounceDelay: 0
9032
- })).render(/* @__PURE__ */ jsx(App, { config }));
8721
+ debounceDelay: 0,
8722
+ targetFps: bootFps,
8723
+ maxFps: bootFps,
8724
+ gatherStats
8725
+ });
8726
+ createRoot(renderer).render(/* @__PURE__ */ jsx(App, { config }));
9033
8727
  await exited;
8728
+ if (gatherStats) try {
8729
+ const s = renderer.getStats();
8730
+ 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`);
8731
+ } catch (err) {
8732
+ process.stderr.write(`[zidane/tui] render stats unavailable: ${err instanceof Error ? err.message : String(err)}\n`);
8733
+ }
9034
8734
  process.exit(0);
9035
8735
  }
9036
8736
  //#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 };
8737
+ 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
8738
 
9039
8739
  //# sourceMappingURL=tui.js.map