Skip to content

[diagnostics] Surface click-command state, failures, and skipped invocations #56

Description

@douglasjarquin

Severity and category

Severity: Medium-high observability and usability defect

Category: Click actions / runtime diagnostics / user trust

Related work: #47 addresses stale click commands across reload; #53 bounds diagnostic presentation; #21 covers optional user notifications. This issue makes the existing click-command runner observable without changing those scopes.

Problem and intended outcome

Pinchos lets each item run an arbitrary command on normal left-click, but the result of that command is effectively invisible.

ManagedItem.processClick starts clickRunner.runIfIdle() and discards the returned outcome. The runner internally retains whether it is active, its last execution, and skipped invocations, but ManagedItem.runtimeSnapshot() exposes only the primary refresh runner. StatusItemController.makeLifecycleMenu also retrieves declarative action snapshots, not the click runner snapshot.

As a result, users cannot tell whether a click command:

  • started or was skipped because another click command was still active;
  • completed successfully;
  • failed to launch because its shell or working directory became unavailable;
  • exited non-zero or by signal;
  • timed out or was cancelled by reload/removal/shutdown;
  • wrote a useful error to stderr;
  • had stdout/stderr truncated;
  • is still running.

This is a serious gap for a command-driven tool whose central promise is that users may choose freely what gets displayed and what actions those items perform. A click that appears to do nothing is indistinguishable from a command failure, a busy-runner skip, or successful background work.

The intended outcome is compact native diagnostics for the configured click command, using the same command-session vocabulary as refresh and declarative action runners while keeping the primary displayed value independent.

Current evidence

Relevant implementation in Sources/pinchos/ManagedItem.swift:

  • clickRunner is created from the item's click command and execution settings.
  • processClick(eventType:) awaits clickRunner.runIfIdle() but does not retain or project the returned outcome beyond what the private runner already stores.
  • runtimeSnapshot() combines the primary runner with item presentation state only.
  • actionSnapshot(at:) exists for declarative actions, but there is no corresponding click snapshot API.
  • click invocation bookkeeping is used for safe update/removal settlement, not user-visible status.

Relevant implementation in Sources/pinchos/StatusItemController.swift:

  • lifecycle menus show primary runtime state and main-runner diagnostics;
  • command actions receive separate diagnostics sections through their snapshots;
  • no section or menu action exposes the click runner.

The README describes click as fire-and-forget. Fire-and-forget should mean the left-click does not block the UI or replace the item's main value; it should not mean failures and skips are permanently undiscoverable.

Required behavior

When an item has a configured click command, its right-click menu should include a compact Click Action diagnostics section.

At minimum, expose:

  • current state: never run, running, completed, error, timed out, or cancelled;
  • last attempted/completed time;
  • last duration;
  • final exit code, signal, timeout, cancellation, or launch-failure reason;
  • skipped/repeated invocation count;
  • stdout/stderr byte counts and truncation flags;
  • a bounded stderr/error preview;
  • bounded stdout preview only when useful;
  • on-demand copy actions for full retained click stdout/stderr, following [performance] Bound and sanitize diagnostic menu and tooltip presentation #53.

The click runner must remain independent from the primary item status:

  • a click failure must not replace the menu-bar value or overwrite the primary runner's last-good output;
  • a successful click must not become the item's displayed refresh value;
  • the item may show a subtle action diagnostic marker only if a separate explicit UX decision is approved; the baseline is discoverability through the right-click menu.

Diagnostics should be retained for the currently configured click runner. Define clear reset semantics:

  • unchanged click command/execution settings retain diagnostics across presentation-only reloads;
  • changing or removing click execution settings cancels the old generation and removes or archives its diagnostics according to one documented rule;
  • removal/shutdown may discard diagnostics only after lifecycle settlement;
  • queued obsolete invocations remain governed by [bug] Prevent a queued left-click from running an obsolete command after live reload #47.

Bounded scope and non-goals

In scope:

  • Add a click-runner snapshot API to the managed-item lifecycle abstraction.
  • Project click state and final execution diagnostics into the native lifecycle menu.
  • Show skipped invocation counts.
  • Use bounded/sanitized previews and on-demand copy behavior from [performance] Bound and sanitize diagnostic menu and tooltip presentation #53.
  • Preserve diagnostic state across reloads when the click runner is unchanged.
  • Define reset behavior when click execution settings change or are removed.
  • Add deterministic lifecycle, menu, and failure tests.
  • Document fire-and-forget versus observable diagnostics semantics.

Explicit non-goals:

Testable acceptance criteria

  • An item with no click command shows no click diagnostics section.
  • A configured click command that has never run shows a concise Never run state.
  • While a click command is active, the menu reports it as running without changing the primary item value or status.
  • A successful click records completion time, duration, exit code 0, output byte counts, and truncation state.
  • A non-zero exit records the exact exit code and bounded stderr preview.
  • Signal termination, timeout, cancellation, shell launch failure, and working-directory launch failure each produce distinct diagnostics.
  • Repeated clicks while the runner is active do not overlap and increment a visible skipped-invocation count.
  • Full retained stdout/stderr can be copied on demand without placing the complete strings in menu titles.
  • Control-heavy and multi-line click output follows [performance] Bound and sanitize diagnostic menu and tooltip presentation #53's sanitization and cannot displace the global action section.
  • A presentation-only config reload retains the click runner and its diagnostics.
  • Changing the click command, shell, environment, working directory, timeout, or output limit follows the documented reset/generation policy and cannot expose obsolete diagnostics as current.
  • Removing click removes its diagnostics after the old runner is safely quiesced.
  • Click failure does not alter lastSuccessfulOutput, lastUpdatedAt, primary error state, staleness, title, or tooltip output.
  • Menu construction remains bounded and responsive at the maximum allowed retained output.
  • VoiceOver/accessibility labels distinguish primary-runner, click-action, and declarative-action diagnostics.
  • swift test and swift build -c release pass.

Likely implementation areas

  • Sources/pinchos/ManagedItem.swift
    • expose clickSnapshot() or a unified named-runner snapshot collection
    • preserve generation/reset semantics
  • Sources/pinchos/StatusItemController.swift
    • add the bounded click diagnostics section and copy actions
  • Sources/PinchosCore/CommandExecution.swift
  • Tests/pinchosTests/RecoveryLifecycleTests.swift
  • Tests/pinchosTests/StatusItemControllerTests.swift
  • Tests/PinchosCoreTests/CommandRunnerTests.swift
  • README click and diagnostics documentation
  • docs/manual-qa native-menu/accessibility evidence

A unified diagnostics model for primary, click, and named action runners may reduce duplication, but it must preserve the semantic distinction that only the primary runner drives the displayed value.

Verification plan

  1. Add headless managed-item tests for every terminal reason, active state, skip count, output bounds, and reload reset/retention rule.
  2. Add menu tests that assert section ordering, concise titles, copy payloads, and global action placement.
  3. Combine click diagnostics with the queued-invocation race fixtures from [bug] Prevent a queued left-click from running an obsolete command after live reload #47.
  4. Run high-output click commands to verify [performance] Replace copying output tails with bounded buffers and enforce memory budgets #50/[performance] Bound and sanitize diagnostic menu and tooltip presentation #53 memory and presentation bounds.
  5. Launch a release GUI and exercise success, failure, timeout, repeated click, reload, removal, and recovery paths.
  6. Perform VoiceOver/accessibility inspection in a permitted GUI session.
  7. Run the full suite and release build.

Risks and dependencies

Diagnostics must not turn a lightweight fire-and-forget action into a second displayed status channel. Keep state retention bounded, render it only when the menu is opened, and reuse existing runner snapshots.

This issue should consume the definitive session result from #48; otherwise click diagnostics could repeat the same preliminary-shell versus final-descendant inconsistency.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions