Skip to content

[performance] Bound and sanitize diagnostic menu and tooltip presentation #53

Description

@douglasjarquin

Severity and category

Severity: Medium-high UI responsiveness and usability risk

Category: AppKit presentation / untrusted command output / memory

Related work: #13 bounds optional menu-bar title length; #50 bounds retained output storage. This issue controls how retained output is projected into AppKit surfaces.

Problem and intended outcome

Pinchos can retain up to max_output bytes independently for stdout and stderr, then place large portions of that content directly into native UI strings.

The lifecycle menu currently creates an NSMenuItem whose title is Value: <fullOutput>. A configured tooltip can substitute {output}, which is the complete retained stdout including newlines. Action diagnostics similarly project command output and error content into menu items.

At the 64-KiB default, a single item can create a very large multi-line menu title and tooltip. At larger user-configured limits, opening a right-click menu can allocate/copy large strings and force AppKit to measure and lay out them on the main actor. Raw command output may also contain tabs, carriage returns, NUL/control characters, bidi controls, or thousands of lines that make the menu unreadable or visually misleading.

Issue #13 deliberately preserves the full value for diagnostics, but “available to diagnostics” should not require rendering the complete byte tail as one menu label.

The intended outcome is a compact, predictable native menu:

  • menu titles and tooltips use bounded, sanitized previews;
  • the user can still retrieve the complete retained stdout/stderr on demand;
  • opening a menu has cost bounded independently of max_output;
  • multi-line and control-heavy output cannot distort the global actions or make the menu unusable;
  • accessibility text remains concise and meaningful.

Current evidence

Relevant implementation in Sources/pinchos/StatusItemController.swift:

  • makeLifecycleMenu obtains ItemRuntimeSnapshot and action snapshots on the main actor;
  • runtime diagnostics append NSMenuItem(title: "Value: \(fullOutput)", ...) with the full retained successful output;
  • command/action diagnostics build additional menu titles from execution text;
  • global Open Config, Reload Config, and Quit Pinchos items are appended after dynamic output-derived content.

Relevant implementation in Sources/pinchos/ManagedItem.swift and PinchosCore/Format.swift:

  • tooltip templates can expand {output} to the full retained output;
  • the resulting string is assigned to the native status-item tooltip;
  • formatting and presentation can be recomputed repeatedly as runtime state changes.

The default retained limit is already large enough to create pathological menu layout. #50 may reduce allocation overhead and add hard budgets, but any retained limit suitable for diagnostics can still be too large for an NSMenuItem.title.

Recommended UX baseline

Define one pure preview formatter with separate, documented limits for:

  • menu value preview;
  • menu stderr/error preview;
  • tooltip preview;
  • action diagnostics.

The formatter should:

  • cap UTF-8 bytes and/or grapheme clusters at a modest value selected through AppKit measurement;
  • cap line count;
  • collapse or visibly escape embedded newlines for menu titles;
  • replace unsafe C0/C1 controls and NUL with visible placeholders;
  • preserve ordinary Unicode without splitting grapheme clusters;
  • append an explicit truncation marker with total byte/line information when known.

Add non-executing menu actions such as Copy Full Output, Copy Full Error, or Copy Diagnostics only when relevant content exists. Clipboard materialization happens on demand and may use the complete retained string. Keep those actions separate from command actions and global lifecycle controls.

A tooltip template that expands beyond the tooltip limit should receive the same deterministic preview/truncation treatment. Full data remains available through the menu copy action.

The exact preview constants should be centralized and covered by performance/visual tests rather than scattered magic numbers.

Bounded scope and non-goals

In scope:

  • Bound every dynamic command-derived NSMenuItem.title and native tooltip.
  • Sanitize control characters and multi-line content for native presentation.
  • Preserve complete retained stdout/stderr through on-demand copy actions.
  • Keep global actions visible, stable, and reachable regardless of output.
  • Add accessible labels/help for truncated previews and copy actions.
  • Avoid eagerly creating duplicate full-output strings while merely opening a menu.
  • Add pure formatter tests and AppKit menu performance tests.
  • Document preview versus retained-output semantics.

Explicit non-goals:

Testable acceptance criteria

  • Opening an item menu has a documented maximum amount of output-derived text to format and lay out, independent of max_output.
  • A 64-KiB single-line value produces a concise preview with an explicit truncation indicator, not a 64-KiB menu title.
  • A value with thousands of lines does not create a thousands-line menu item.
  • NUL, escape, carriage return, tab, bidi-control, and other selected control fixtures are rendered according to a deterministic sanitization policy and cannot obscure the global action section.
  • Preview truncation does not split extended grapheme clusters.
  • Copy Full Output places the complete retained stdout on NSPasteboard exactly as retained.
  • Equivalent copy access exists for relevant stderr/action diagnostics without displaying it all in the title.
  • Copy actions are absent or disabled when no corresponding content exists.
  • Tooltip expansion is bounded and clearly indicates truncation while preserving ordinary configured text.
  • Global Open Config, Reload Config, and Quit Pinchos remain visible and usable for pathological output fixtures.
  • VoiceOver/accessibility inspection receives concise labels and can identify copy actions and truncation state.
  • Menu construction for maximum allowed retained output stays under a measured main-thread latency budget in a release build.
  • Repeated menu opens do not grow retained memory or keep obsolete full-output copies alive.
  • Existing diagnostics fields (state, attempts, duration, exit reason, truncation counts, skipped work) remain available.
  • swift test and swift build -c release pass.

Likely implementation areas

  • New pure DiagnosticPreviewFormatter in Sources/PinchosCore or a UI-free helper module
  • Sources/pinchos/StatusItemController.swift
  • Sources/pinchos/ManagedItem.swift
  • Sources/PinchosCore/Format.swift for bounded tooltip expansion if appropriate
  • Tests/PinchosCoreTests/FormatTests.swift
  • Tests/pinchosTests/StatusItemControllerTests.swift
  • Tests/pinchosTests/RecoveryLifecycleTests.swift
  • README diagnostics and tooltip documentation
  • docs/manual-qa visual/accessibility evidence

Verification plan

  1. Add table-driven formatter tests for long lines, many lines, CRLF, tabs, NUL, escape sequences, bidi controls, emoji ZWJ sequences, combining marks, and exact-boundary cases.
  2. Add headless menu tests that assert title lengths, global action placement, and copy payloads.
  3. Build menus repeatedly with the selected maximum output and measure allocations and main-thread duration.
  4. Run a release GUI with pathological command fixtures and verify menu geometry, responsiveness, tooltip behavior, clipboard contents, and global actions.
  5. Perform VoiceOver/accessibility inspection in a permitted logged-in session.
  6. Coordinate with [performance] Replace copying output tails with bounded buffers and enforce memory budgets #50 to measure end-to-end retained and temporary memory.
  7. Run the full suite and release build.

Risks and dependencies

Clipboard actions are native and cheap, but copying a maximum-size value necessarily materializes it once. That work is user-initiated and bounded by #50, unlike the current automatic menu-open cost.

Sanitization must be for display only. The clipboard should receive the exact retained text unless the user selects a separately named sanitized-copy action.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions