Skip to content

feat(gui): Expandable event rows with detail panel and fired styling (#485)#505

Merged
amiable-dev merged 14 commits intomainfrom
feat/485-expandable-event-rows
Mar 8, 2026
Merged

feat(gui): Expandable event rows with detail panel and fired styling (#485)#505
amiable-dev merged 14 commits intomainfrom
feat/485-expandable-event-rows

Conversation

@amiable-dev
Copy link
Owner

Summary

ADR-014 Phase 3A: Click-to-expand event rows with detail panels.

  • EventRow.svelte: Local expanded state toggled on click. Collapsed: single-line truncated with text-overflow ellipsis. Expanded: detail panel below main line. Chevron indicator (▸/▾) appears on hover
  • Raw event detail panel: Device, Channel, Type, absolute timestamp (HH:MM:SS.mmm)
  • Fired event detail panel: Trigger (type, number, value, device), Action (type, summary), Result (ok/error with message), Latency (formatted ms), Mode, Time
  • Fired entry distinct styling: ⚡ icon replaces dot, "Fired" label in accent color, left border colored by trigger type (note=green, cc=blue, etc.), content shows "trigger → action"
  • event-helpers.ts: formatLatency(), formatAbsoluteTime(), isFiredEvent(), getFiredDotClass()
  • CSS: Highlight border takes precedence over fired border (Learn mode). Performance-safe expand via display toggle (no height calculations)
  • Updated EventStreamPanel test mock with new helper exports

Closes #485

Test plan

  • npx vitest run — 749 tests pass, no regressions
  • EventRow starts collapsed, expands on click, collapses on second click
  • Chevron shows ▸ collapsed, ▾ expanded
  • Raw event detail panel shows Device, Channel, Type, Time
  • Fired event detail panel shows Trigger, Action, Result, Latency, Mode, Time
  • Fired events: ⚡ icon, "Fired" label, left border, trigger→action content
  • Error result shows error message in detail panel
  • Learn button click does not toggle expand (stopPropagation)
  • Highlight class still works for Learn mode
  • 42 new tests across EventRow (21) and event-helpers (21)

🤖 Generated with Claude Code

amiable-dev and others added 2 commits March 7, 2026 22:11
…485)

ADR-014 Phase 3A: Click-to-expand event rows with detail panels.

- EventRow.svelte: Local expanded state, click to toggle, chevron
  indicator (▸/▾), single-line truncation when collapsed
- Raw event detail panel: Device, Channel, Type, absolute timestamp
- Fired event detail panel: Trigger, Action, Result, Latency, Mode, Time
- Fired entry distinct styling: ⚡ icon, "Fired" accent label, left
  border colored by trigger type, "trigger → action" content
- event-helpers.ts: formatLatency(), formatAbsoluteTime(), isFiredEvent(),
  getFiredDotClass()
- 42 new tests (21 EventRow, 21 event-helpers)
- Fix EventStreamPanel test mock to include new helper exports

Closes #485

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… border

Ensures Learn mode highlight (accent border) visually overrides the
fired entry border when both classes are present on the same row.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements ADR-014 Phase 3A in the GUI event stream by making event rows expandable with a detail panel, and adding distinct “MappingFired” styling backed by new event helper utilities and expanded test coverage.

Changes:

  • Added fired-event utilities (formatLatency, formatAbsoluteTime, isFiredEvent, getFiredDotClass) for EventRow formatting/styling.
  • Updated EventRow.svelte to support click-to-expand detail panels and dedicated fired-row visuals (⚡, accent label, trigger→action summary, colored left border).
  • Updated/added Vitest coverage and updated mocks to include the new helper exports.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
conductor-gui/ui/src/lib/utils/event-helpers.ts Adds new formatting + fired-event identification/styling helpers.
conductor-gui/ui/src/lib/utils/event-helpers.test.ts Adds unit tests for the new helper exports.
conductor-gui/ui/src/lib/panels/EventStreamPanel.test.ts Updates helper mocks to include the new exports used by UI.
conductor-gui/ui/src/lib/components/EventRow.test.ts Adds tests for expand/collapse behavior and fired-row presentation.
conductor-gui/ui/src/lib/components/EventRow.svelte Implements expandable rows, detail panels, and fired-row styling.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 171 to +175
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event-detail is always ellipsized (text-overflow: ellipsis / white-space: nowrap), so expanding the row won’t reveal long detail strings. Add an expanded-state override (e.g., unset text-overflow and allow wrapping) so the user can read the full text when expanded.

Copilot uses AI. Check for mistakes.
Comment on lines +166 to +167
case 'encoder':
return 'cc';
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getFiredDotClass() maps daemon trigger type encoder to cc, which makes encoder-fired events blue. The theme/UI treat encoder as its own purple type (--event-encoder in ui/src/theme.css, and .mapping-dot.encoder in MappingRow). Consider returning encoder here (and update EventRow’s border-class logic accordingly) to keep colors consistent.

Suggested change
case 'encoder':
return 'cc';
return 'cc';
case 'encoder':
return 'encoder';

Copilot uses AI. Check for mistakes.

describe('formatAbsoluteTime', () => {
it('formats timestamp to HH:MM:SS.mmm', () => {
// Use a known timestamp: 2025-01-15 12:34:56.789 UTC
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test comment says the constructed Date is "UTC", but new Date(2025, 0, 15, ...) uses local time (and formatAbsoluteTime() uses local getters). Update the comment to avoid confusion about timezone semantics.

Suggested change
// Use a known timestamp: 2025-01-15 12:34:56.789 UTC
// Use a fixed local timestamp: 2025-01-15 12:34:56.789

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +156
* Check if an event is a MappingFired event.
*/
export function isFiredEvent(event: any): boolean {
return event?._type === 'mapping_fired';
}

/**
* Get the fired event dot class based on trigger type.
* Maps daemon trigger type strings to CSS color classes.
*/
export function getFiredDotClass(event: any): string {
const triggerType = event?.trigger?.type;
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module is otherwise strongly typed, but isFiredEvent takes any, which drops type-safety for callers. Prefer unknown + a type guard (or define a MappingFiredEvent interface) so you can safely narrow before reading _type/trigger/action fields.

Suggested change
* Check if an event is a MappingFired event.
*/
export function isFiredEvent(event: any): boolean {
return event?._type === 'mapping_fired';
}
/**
* Get the fired event dot class based on trigger type.
* Maps daemon trigger type strings to CSS color classes.
*/
export function getFiredDotClass(event: any): string {
const triggerType = event?.trigger?.type;
* Narrow type for a "mapping_fired" event coming from the daemon.
* Only includes the fields we actually use here; extend if needed elsewhere.
*/
export interface MappingFiredEvent {
_type: 'mapping_fired';
trigger?: {
type?: string;
};
// action?: unknown; // add fields here if needed by future helpers
}
/**
* Check if an event is a MappingFired event.
* Acts as a type guard so callers can safely narrow before accessing fields.
*/
export function isFiredEvent(event: unknown): event is MappingFiredEvent {
return (
typeof event === 'object' &&
event !== null &&
(event as { _type?: unknown })._type === 'mapping_fired'
);
}
/**
* Get the fired event dot class based on trigger type.
* Maps daemon trigger type strings to CSS color classes.
*/
export function getFiredDotClass(event: unknown): string {
if (!isFiredEvent(event)) {
return 'unknown';
}
const triggerType = event.trigger?.type;

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +46
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The a11y warning is suppressed with svelte-ignore, but the row is only clickable. Since it’s given role="button"/tabindex, add on:keydown handling for Enter/Space (and preventDefault for Space) and remove the ignore so the expand/collapse works for keyboard users.

Copilot uses AI. Check for mistakes.
Comment on lines 110 to 114
/* Highlight takes precedence over fired border (Learn mode) */
.event-row.highlight {
background: var(--accent-tint);
border-left: 2px solid var(--accent);
}
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says highlight border takes precedence, but .event-row.fired.border-* selectors are more specific than .event-row.highlight, so fired border colors will override the accent border when both apply. Increase highlight selector specificity (e.g. .event-row.highlight.fired { border-left-color: ... }) or avoid applying border-* classes when highlight is true.

Copilot uses AI. Check for mistakes.
overflow: hidden;
white-space: nowrap;
}

Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expanded state doesn’t currently untruncate the main row: .event-main keeps overflow: hidden + white-space: nowrap, so the summary line remains clipped even when expanded. Consider adding .event-row.expanded .event-main { white-space: normal; overflow: visible; } (or similar) to match the intended expanded behavior.

Suggested change
/* When expanded, allow the main summary line to wrap and be fully visible */
.event-row.expanded .event-main {
white-space: normal;
overflow: visible;
}

Copilot uses AI. Check for mistakes.
- A11y: Add Enter/Space keyboard handlers, remove svelte-ignore
- CSS: Avoid fired+highlight specificity conflict by not applying fired
  class when highlight is true
- CSS: Untruncate event-detail and event-main when expanded
- Types: Replace `any` with type guard (isFiredEvent returns type predicate),
  add MappingFiredEvent interface
- Encoder dot class returns 'encoder' (purple) instead of 'cc' (blue)
  for consistent color mapping with theme
- Fix test comment: "local timestamp" not "UTC"
- Add border-encoder CSS class for encoder-fired events

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.


You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +40 to +44
function handleKeydown(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
expanded = !expanded;
}
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleKeydown is attached to the entire .event-row, so key events from child controls (notably the Learn <button>) will bubble up and toggle expanded. This makes keyboard activation of Learn also expand/collapse the row. Consider ignoring events where e.target !== e.currentTarget (or where e.target is a BUTTON), and/or add on:keydown|stopPropagation on the Learn button.

Copilot uses AI. Check for mistakes.
on:click={handleClick}
on:keydown={handleKeydown}
role="button"
tabindex="0"
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this div is acting as an expandable button (role="button", tabindex="0"), it should expose its state to assistive tech (e.g., aria-expanded={expanded} and ideally aria-controls pointing at the detail panel). Without this, screen readers won’t announce the collapsed/expanded state.

Suggested change
tabindex="0"
tabindex="0"
aria-expanded={expanded}

Copilot uses AI. Check for mistakes.
}

/**
* Narrow type for a "mapping_fired" event from the daemon.
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says the _type: "mapping_fired" event is “from the daemon”, but in this codebase _type is added on the GUI side (see pushFiredEvent() in stores/events.js) for filtering/display. Updating this comment would prevent confusion about where _type comes from.

Suggested change
* Narrow type for a "mapping_fired" event from the daemon.
* Narrow type for a "mapping_fired" event as represented in the GUI.
*
* Note: the `_type: "mapping_fired"` discriminator is added on the GUI side
* (e.g. in the event store) for filtering and display, and is not part of
* the raw event payload from the Rust daemon.

Copilot uses AI. Check for mistakes.
Comment on lines +126 to +130
it('expands on click to show detail panel', async () => {
const { container } = await importAndRender({ event: RAW_EVENT });
const row = container.querySelector('.event-row');
await fireEvent.click(row);
expect(container.querySelector('.event-row.expanded')).toBeTruthy();
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests call fireEvent.*(container.querySelector(...)) without asserting the element exists first. If the selector ever stops matching, the failure will be a less-informative runtime exception. Prefer screen.getByRole/getByText or assert the query result is non-null before firing events.

Copilot uses AI. Check for mistakes.
amiable-dev and others added 2 commits March 7, 2026 22:47
…w keydown

handleKeydown was calling preventDefault on Enter/Space unconditionally,
which blocked nested Learn button keyboard activation. Now checks
e.target === e.currentTarget to only toggle expand when the row itself
is focused.

Found by LLM Council reasoning-tier verification.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add aria-expanded={expanded} for screen reader support
- Fix _type comment: discriminator is added GUI-side, not from daemon
- Add non-null assertions and existence checks on querySelector in tests
- Keydown bubble and highlight/fired specificity already addressed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +185 to +209
export function getFiredDotClass(event: unknown): string {
if (!isFiredEvent(event)) return 'unknown';
const triggerType = event.trigger?.type;
switch (triggerType) {
case 'note':
case 'short_press':
case 'medium_press':
case 'long_press':
case 'double_tap':
case 'chord':
return 'note';
case 'cc':
return 'cc';
case 'encoder':
return 'encoder';
case 'aftertouch':
return 'aftertouch';
case 'pitch_bend':
return 'pitchbend';
case 'gamepad_button':
case 'gamepad_chord':
return 'gamepad';
default:
return 'unknown';
}
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getFiredDotClass() doesn’t handle fired trigger types gamepad_analog and gamepad_trigger. Those trigger type strings are produced/expected elsewhere in the UI (e.g. CONFIG_TO_FIRED_TYPE in stores/events.js), so these fired events will currently fall into the default case and be styled as unknown (missing the intended left-border color). Consider mapping them to an appropriate class (e.g. gamepad or encoder, depending on desired styling) and adding a test case for each.

Copilot uses AI. Check for mistakes.
Comment on lines +135 to +142
export function formatAbsoluteTime(timestampMs: number): string {
const d = new Date(timestampMs);
const hh = String(d.getHours()).padStart(2, '0');
const mm = String(d.getMinutes()).padStart(2, '0');
const ss = String(d.getSeconds()).padStart(2, '0');
const ms = String(d.getMilliseconds()).padStart(3, '0');
return `${hh}:${mm}:${ss}.${ms}`;
}
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatAbsoluteTime() duplicates the existing formatTimestamp() helper in utils/midi-helpers.ts (same HH:MM:SS.mmm formatting logic). To avoid having two implementations/tests to keep in sync, consider reusing a single shared formatter (either import the existing helper or move the shared logic into a common utility module and use it from both places).

Copilot uses AI. Check for mistakes.
- Add gamepad_analog and gamepad_trigger to getFiredDotClass (→ encoder/purple)
  matching CONFIG_TO_FIRED_TYPE in events.js
- Replace duplicate formatAbsoluteTime with re-export of formatTimestamp
  from midi-helpers.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.


You can also share your feedback on Copilot code review. Take the survey.

return 'gamepad';
case 'gamepad_analog':
case 'gamepad_trigger':
return 'encoder';
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getFiredDotClass() maps gamepad_analog and gamepad_trigger to the encoder class. Elsewhere in the UI, gamepad-related trigger types (including GamepadAnalogStick/GamepadTrigger) are colored as gamepad (e.g. getTriggerDotColor()), so fired gamepad stick/trigger events will appear with the wrong color/border. Consider mapping these fired trigger types to gamepad (and update the corresponding tests) for consistent styling.

Suggested change
return 'encoder';
return 'gamepad';

Copilot uses AI. Check for mistakes.
<div class="detail-row"><span class="detail-label">Trigger</span> <span class="detail-value">{event.trigger?.type ?? '?'} #{event.trigger?.number ?? '?'} v={event.trigger?.value ?? '?'}{event.trigger?.device ? ` (${event.trigger.device})` : ''}</span></div>
<div class="detail-row"><span class="detail-label">Action</span> <span class="detail-value">{event.action?.type ?? '?'}: {event.action?.summary ?? '?'}</span></div>
<div class="detail-row"><span class="detail-label">Result</span> <span class="detail-value" class:error-result={event.result === 'error'}>{event.result ?? '?'}{event.error ? ` — ${event.error}` : ''}</span></div>
<div class="detail-row"><span class="detail-label">Latency</span> <span class="detail-value">{formatLatency(event.latency_us ?? 0)}</span></div>
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latency field uses formatLatency(event.latency_us ?? 0), which will display 0.0ms when latency is missing/null. That looks like a real measured value and can be misleading; it would be better to render an explicit unknown/placeholder (e.g. "—") when latency_us is absent, and only format when the value is present.

Suggested change
<div class="detail-row"><span class="detail-label">Latency</span> <span class="detail-value">{formatLatency(event.latency_us ?? 0)}</span></div>
<div class="detail-row"><span class="detail-label">Latency</span> <span class="detail-value">{event.latency_us != null ? formatLatency(event.latency_us) : ''}</span></div>

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +82
on:click={handleClick}
on:keydown={handleKeydown}
role="button"
tabindex="0"
aria-expanded={expanded}
>
<div class="event-main">
{#if fired}
<span class="event-fired-icon">⚡</span>
{:else}
<span class="event-dot {dotClass}"></span>
{/if}
<span class="event-type {dotClass}" class:fired-label={fired}>{typeLabel}</span>
<span class="event-detail">{detail}</span>
<span class="event-time">{relTime}</span>
{#if showLearnBtn}
<button class="event-learn-btn" on:click|stopPropagation={handleLearn}>Learn</button>
{/if}
<span class="event-chevron">{expanded ? '▾' : '▸'}</span>
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The row is implemented as an interactive element (role="button", tabindex="0", click/keydown handlers) but it contains a nested <button> (Learn). Nested interactive controls can be confusing for screen readers and keyboard users. Consider restructuring so the expand/collapse control is a real <button> (or the row is non-interactive) and the Learn button remains a separate, non-nested interactive element (or otherwise ensure an accessible interaction model).

Suggested change
on:click={handleClick}
on:keydown={handleKeydown}
role="button"
tabindex="0"
aria-expanded={expanded}
>
<div class="event-main">
{#if fired}
<span class="event-fired-icon">⚡</span>
{:else}
<span class="event-dot {dotClass}"></span>
{/if}
<span class="event-type {dotClass}" class:fired-label={fired}>{typeLabel}</span>
<span class="event-detail">{detail}</span>
<span class="event-time">{relTime}</span>
{#if showLearnBtn}
<button class="event-learn-btn" on:click|stopPropagation={handleLearn}>Learn</button>
{/if}
<span class="event-chevron">{expanded ? '' : ''}</span>
>
<div class="event-main">
<button
type="button"
class="event-toggle"
on:click={handleClick}
on:keydown={handleKeydown}
aria-expanded={expanded}
>
{#if fired}
<span class="event-fired-icon">⚡</span>
{:else}
<span class="event-dot {dotClass}"></span>
{/if}
<span class="event-type {dotClass}" class:fired-label={fired}>{typeLabel}</span>
<span class="event-detail">{detail}</span>
<span class="event-time">{relTime}</span>
<span class="event-chevron">{expanded ? '' : ''}</span>
</button>
{#if showLearnBtn}
<button
type="button"
class="event-learn-btn"
on:click={handleLearn}
>
Learn
</button>
{/if}

Copilot uses AI. Check for mistakes.
amiable-dev and others added 7 commits March 7, 2026 23:42
Addresses event stream stalling during long action sequences (e.g.,
25-note Sequence). Proposes dedicated executor task via spawn_blocking
with completion channel, MIDI output recursion guard, and ModeChange
preservation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrites ADR-015 to resolve critical issues from reasoning-tier review:
- Consolidate D1/D3 into single std::thread actor (not spawn_blocking)
- Use try_send to eliminate bidirectional channel deadlock
- Unbounded completion channel to prevent backpressure deadlock
- Acknowledge ModeChange semantic change (deliberate, more intuitive)
- Message fingerprinting for MIDI recursion guard (not coarse flag)
- Config hot-reload via ConfigUpdate control messages
- Document head-of-line blocking as accepted tradeoff
- Add invocation_id for matched/fired event correlation
- Add graceful shutdown with cancellation-aware waits
- Include council review history section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Key changes from review 2 feedback:
- D2: Separate control plane (shutdown, config) from data plane (actions)
  with priority-select pattern — control messages never dropped
- D11: Interruptible sleep loop (50ms intervals), shell command timeouts,
  process-group kill, bounded shutdown latency (5s max)
- D7: Document ModeChange nondeterminism at completion boundaries
- D8: Full raw MIDI bytes + port name for fingerprinting, thru_pairs config
- D6: Explicitly note breaking change in event ordering semantics
- D4: Qualify unbounded completion channel safety assumptions
- D9: Acknowledge global queue unfairness, defer per-device queues

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add D13 (terminal event lifecycle invariant), D14 (mode-change lag
bounds), rewrite D2 (separate signal primitives: AtomicBool, watch,
crossbeam), rewrite D10 (immutable action plans), rewrite D11
(execute_interruptible with AtomicBool), update implementation plan
phases, and add Review 3 to council history.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- D4: biased select with completion priority, handle executor death
- D3: emit mapping_dropped on Disconnected (dangling invocations)
- D5/D12: executor-side timestamps (completed_at, execution_time_us)
- D11: per-action-type cancellation semantics, shell wait_timeout
- D7/D14: update ModeChange guarantees for biased select
- D1: explicit thread-affine Enigo construction requirement
- Constraint #6: clarify immutable action plans, no mid-sequence reload
- Review 4 added to council history

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- getFiredDotClass: gamepad_analog/gamepad_trigger → 'gamepad' for
  consistent styling with getTriggerDotColor()
- EventRow: show '—' for missing latency instead of misleading '0.0ms'
- EventRow: restructure to avoid nested interactive elements — row is
  now a plain div, expand/collapse is a proper <button> with
  aria-expanded, Learn button is a sibling (not nested)
- Update tests for new .event-toggle selector and latency display
- ADR-015: add issue #516 cross-reference

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.


You can also share your feedback on Copilot code review. Take the survey.

let _ = completion_tx.send(ActionCompletion { ... });
}
Err(RecvTimeoutError::Timeout) => continue,
Err(Disconnected) => return,
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this snippet, recv_timeout uses crossbeam_channel::RecvTimeoutError, where the disconnect variant is RecvTimeoutError::Disconnected. Err(Disconnected) won’t compile as written and is inconsistent with the preceding Err(RecvTimeoutError::Timeout) arm—please update the example to the correct variant name/path (or clearly mark it as pseudocode).

Suggested change
Err(Disconnected) => return,
Err(RecvTimeoutError::Disconnected) => return,

Copilot uses AI. Check for mistakes.
Comment on lines +178 to +205
export function getFiredDotClass(event: unknown): string {
if (!isFiredEvent(event)) return 'unknown';
const triggerType = event.trigger?.type;
switch (triggerType) {
case 'note':
case 'short_press':
case 'medium_press':
case 'long_press':
case 'double_tap':
case 'chord':
return 'note';
case 'cc':
return 'cc';
case 'encoder':
return 'encoder';
case 'aftertouch':
return 'aftertouch';
case 'pitch_bend':
return 'pitchbend';
case 'gamepad_button':
case 'gamepad_chord':
return 'gamepad';
case 'gamepad_analog':
case 'gamepad_trigger':
return 'gamepad';
default:
return 'unknown';
}
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MappingFiredPayload.trigger.type is emitted as "encoder" for all ProcessedEvent::EncoderTurned events (including gamepad stick/trigger where cc >= 128). With the current switch, gamepad analog/trigger fired events will be styled as encoder instead of gamepad. Consider mapping trigger.type === 'encoder' to gamepad when trigger.number is in the gamepad range (>= 128), so fired styling matches the gamepad convention.

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +71
<button
type="button"
class="event-toggle"
on:click={handleClick}
on:keydown={handleKeydown}
aria-expanded={expanded}
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since .event-toggle is a native <button>, Enter/Space already trigger click by default. The extra on:keydown toggling risks double-toggling (keydown handler flips expanded, then the native keyboard-initiated click may fire and flip it back), which would break keyboard expand/collapse in real browsers (tests currently only send keyDown). Suggest relying on on:click for keyboard activation, or otherwise ensure the key handler does not result in an additional click toggle.

Copilot uses AI. Check for mistakes.
- ADR-015: fix RecvTimeoutError::Disconnected variant name in pseudocode
- ADR-015: fix invalid ISO-8601 timestamp (24:12 → 00:12)
- getFiredDotClass: check trigger.number >= 128 for encoder→gamepad
  disambiguation (daemon emits 'encoder' for gamepad analog/trigger)
- EventRow: remove on:keydown handler — native <button> already handles
  Enter/Space → click, preventing double-toggle bug
- Update tests for new behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@amiable-dev amiable-dev merged commit 07d7fb2 into main Mar 8, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ADR-014] Phase 3A: Expandable event rows with detail panels

2 participants