Skip to content

Add Library drag assignment to Dashboard panels and variables #428

Closed

Description

@BorisTyshkevich

Status

Drag path shipped in PR #484 (merged 2026-07-27). The only unmet acceptance
bullet is 9 — the keyboard-accessible Add to dashboard… command — which was
deferred to #483 by owner decision the same day. So there is no remaining work
tracked in this issue: it stays open only as the umbrella for that deferred
bullet, and closes when #483 lands (or immediately, if the bullet is considered
#483's to own).

Assignment is therefore pointer-only on main today. That is the accessibility
gap #483 exists to close, and the reason bullet 9 was not silently dropped.

Required foundations have shipped:

This issue owns one item of #447 phase 3 — the drag/drop of Library SQL onto
Variables rows. It does not own that phase. Corrected 2026-07-27; the original
wording ("owns #447 phase 3") over-claimed and would have read as though merging
#484 completed the phase.

#447 phase 3's other six items stay with #447: the paste/copy workflow in the
variable editor, rewritten shipped examples on inferred variables, import/export
coverage for variableConfigs, docs and E2E coverage, the final dead-code audit for
removed filter terminology, and reconciling or closing superseded #450.

#465 is not a prerequisite. It restores the two-String-column shape check on an individual variable tab's Run action; assignment here only copies SQL and preserves the current validation/runtime behaviour.

The old curated-filter/provider design no longer exists. There are no filter-role queries, persisted filter objects, source-query IDs, target lists, or filter configuration dialogs.

Goal

A Library query drag has three destination-dependent meanings:

Library query -> Dashboard row / Panels group -> create an independent panel-owned query copy and tile
Library query -> existing Variables row        -> copy its SQL text into that Dashboard variable's option SQL
Library query -> main SQL editor               -> insert the existing `( … )` subquery text at the drop position

The third path is already implemented by PR #40 and must remain behaviourally unchanged. This issue extends the same Library drag source with stable identity for Dashboard assignment without breaking the existing editor drop.

Drag payload and source rules

Only rows in the lower Library list may be assigned to Dashboards. A Library query has zero Dashboard owners according to buildQueryOwnershipIndex.

History rows remain draggable only for the existing editor-subquery insertion and are never Dashboard-assignment sources.

A Library drag must publish two independent payloads:

  1. the existing SUBQUERY_MIME SQL snapshot used by the SQL editor;
  2. a new internal identity payload used only by Dashboard/Variables targets.

Suggested identity payload:

interface LibraryQueryDragPayload {
  kind: 'library-query';
  workspaceId: string;
  queryId: string;
}

Requirements:

  • Dashboard assignment must never trust SQL or a complete saved-query object from dataTransfer;
  • Dashboard assignment re-resolves the query by ID from the latest committed workspace inside mutateWorkspace;
  • editor insertion continues to use the SQL snapshot and toSubquery(...) exactly as today;
  • initiating a drag must not open the Library query;
  • normal click, star, rename, delete, and existing editor drop behaviour remain usable;
  • workspace switch, missing source, or a source that is no longer a Library query cancels safely.

Do not cancel merely because the same workspace changed after drag start. Re-read the latest workspace and apply the semantic operation when the source and target still exist.

Supported destinations

Panel assignment

Accept on:

Dashboard row -> panel assignment
Panels group  -> panel assignment

Both destinations have the same persistence semantics.

Variable option-SQL assignment

Accept on an individual inferred Variables row:

Variables
  country : String   <- valid destination

The Variables group itself is not a destination because it does not identify which variable receives the SQL.

An orphaned configuration that is no longer inferred is not a valid assignment destination. It can still be edited or deleted through its existing UI.

Existing editor insertion

The main SQL editor remains an accepted destination through SUBQUERY_MIME:

  • insert at the actual drop position;
  • wrap with the existing ( … ) subquery formatting;
  • make the edit undoable;
  • perform no workspace mutation.

Rejected destinations

Do not accept Dashboard assignment on:

  • an individual panel row;
  • the Variables group row;
  • an orphan-only variable row;
  • unrelated tree rows or schema objects;
  • Dashboard tiles/canvas content;
  • History rows as sources;
  • Dashboard-owned query rows as sources.

Moving/copying members between Dashboards and tree reordering remain out of scope.

Drag feedback

During a Library drag:

  • highlight eligible Dashboard rows, Panels groups, and inferred Variables rows;
  • show a distinct active-target state on dragenter/dragover;
  • do not present the Variables group itself as eligible;
  • clear all assignment drag state on drop, cancellation, Escape, workspace switch, or drag end;
  • preserve the editor's existing drop feedback;
  • auto-expand a collapsed Dashboard after a short bounded hover delay so Panels and Variables rows become reachable;
  • cancel the hover timer when leaving;
  • auto-expansion is UI state only and must not navigate to the Dashboard or mutate workspace content;
  • dropping must not trigger Dashboard-name navigation, row click actions, disclosure clicks, or trailing action menus.

Panel assignment

Dropping on a Dashboard row or Panels group performs one atomic mutation over the latest StoredWorkspaceV5.

Suggested application command:

copyLibraryQueryToPanel({
  latest,
  sourceQueryId,
  dashboardId,
  newQueryId,
  newTileId,
}): StoredWorkspaceV5 | null

Required transformation:

  1. verify the active/latest workspace has the payload's exact workspaceId;
  2. resolve the source query by exact ID;
  3. verify it still has zero Dashboard owners;
  4. resolve exactly one target Dashboard by stable ID;
  5. mint collision-free query and tile IDs against the latest document;
  6. clone with the shipped cloneQueryForDashboardOwner({ source, newId }) helper;
  7. append the clone to workspace.queries[];
  8. append { id: newTileId, queryId: newQueryId } to the target Dashboard's tiles[];
  9. do not create redundant tile title/description overrides;
  10. normalise the target Dashboard through the active layout implementation and regenerate fallback layout data where required;
  11. increment only the target Dashboard revision, exactly once;
  12. preserve every non-target Dashboard and the source Library query unchanged;
  13. validate the complete workspace candidate;
  14. commit through app.mutateWorkspace(...).

The source remains in Library and is never modified, including its favourite state.

Repeated drops are allowed. Every successful drop creates a new independent query ID and tile ID, even when the same source is dropped repeatedly into one Dashboard.

After success:

  • repaint Library and Dashboard tree projections;
  • expand the target Dashboard and Panels group;
  • focus/select the new panel row;
  • do not automatically open Dashboard View/Edit or execute the query.

Owner revision (2026-07-27). The drop DOES now open the new panel's owned
query copy in the SQL editor, and focuses the new row in the tree. Landing on an
unchanged-looking Dashboard read as "did anything happen?". The Dashboard itself
is still never opened in View/Edit and nothing is executed.

Variable option-SQL assignment

Dropping on an inferred Variables row copies only the source query's SQL text into the target Dashboard's variableConfigs[variableName] entry.

Required semantics:

  • identify the target by exact (dashboardId, variableName);
  • re-derive the target variable from the latest target Dashboard's panel queries;
  • require that the exact variable name is still inferred at commit time;
  • preserve exact case-sensitive variable-name identity;
  • copy SQL text only;
  • create no saved-query clone, tile, role, source ID, owner, target list, or provider mapping;
  • leave the Library query byte-for-byte/canonical-value unchanged;
  • later edits to the Library query do not affect the copied variable SQL;
  • increment only the target Dashboard revision, exactly once;
  • commit through the existing withVariableConfig / Dashboard-variable-config mutation path or an equivalent id-addressed transform over mutateWorkspace.

A blank/whitespace-only Library query is not a deletion gesture: reject the drop as a no-op with a clear message. Removing variable option SQL remains the explicit blank-Save/delete behaviour of the variable editor.

Copy the SQL as authored. The existing local option-SQL rules remain authoritative:

No configuration dialog is required.

Open variable-tab interaction

A drop must never silently overwrite an unsaved variable-tab draft.

  • no open tab: commit normally;
  • matching clean variable tab: commit and reconcile it to the committed SQL;
  • matching dirty variable tab: reject the assignment, keep the draft unchanged, and focus/select that tab with a clear message.

The variable tab is identified by (dashboardId, variableName), as shipped by #457.

After a successful variable assignment:

  • repaint the Variables row/control state;
  • keep the Dashboard tree expansion state;
  • focus/select the target variable row;
  • do not automatically switch to the Query surface or run the option SQL.

Owner revision (2026-07-27). The drop DOES now open that variable's
Variable: <name> tab, so the assigned option SQL is there to edit and run. The
option SQL is still not executed automatically.

Commit-window addendum (2026-07-27). The dirty-tab rule below is enforced
inside the mutateWorkspace transform, which closes the queue-and-load window.
It cannot close the window during workspace.commit(...) itself — the write is
already durable by then. That case is REPORTED rather than hidden: the outcome
carries draftDiverged, and the drop shows a non-dismissing toast offering
Discard draft. Refusing to adopt while reporting a clean success was the
failure mode this rule exists to prevent.

Existing editor-subquery path

The existing PR #40 behaviour is part of this issue's integration contract, not new implementation scope.

Adding the identity payload must not change:

  • Library and History rows inserting into the main SQL editor;
  • SUBQUERY_MIME handling;
  • trailing semicolon/FORMAT cleanup performed by toSubquery;
  • drop-position insertion;
  • undo/redo behaviour;
  • editor-only operation with no persistence side effects.

The same Library drag may therefore be interpreted differently by different targets: the editor consumes SQL text; Dashboard destinations consume stable identity.

Concurrency and stale-state rules

Use the current read-before-write model from #343:

enter mutateWorkspace queue
-> load latest committed workspace
-> re-resolve source and target
-> apply one semantic transform
-> validate whole candidate
-> atomically commit
-> project committed truth

Do not introduce or claim repository compare-and-swap. Simultaneous same-instant multi-tab commits remain outside the repository contract.

Abort without mutation when:

  • the active workspace ID differs from the payload;
  • the source query was deleted or became Dashboard-owned;
  • the target Dashboard disappeared or has duplicate IDs;
  • the target variable is no longer inferred;
  • generated IDs cannot be made collision-free;
  • validation or persistence fails;
  • a dirty target variable tab would be overwritten.

Never commit an orphan panel query without its tile, a tile without its query, or a partial variable update.

Keyboard-accessible alternative

DEFERRED to #483 (owner decision, 2026-07-27). This section is the spec for
that issue; PR #484 implements the drag path only. Mobile is also out of scope
for the whole gesture, so no isMobile branches were added — touch drag is
already a non-goal below.

Each Library row must expose an equivalent command:

Add to dashboard…

The chooser contains:

  1. Dashboard selection;
  2. destination kind: Panel or Variable;
  3. for Variable, one of that Dashboard's currently inferred exact variable names.

The chooser calls the same application commands as drag/drop and produces identical data.

Do not recreate the removed curated-filter configuration form. There is no parameter picker, target-panel picker, default picker, selection-mode picker, or provider mapping.

The existing editor-subquery drag remains separate from this assignment chooser.

Suggested boundaries

Pure/application mutation layer

  • copyLibraryQueryToPanel
  • copyLibraryQuerySqlToVariable

Both operate over an explicit latest workspace and return a complete candidate or a typed abort.

Library drag adapter

Publishes both MIME payloads, suppresses row opening during drag, and exposes the keyboard command.

Dashboard-tree drop controller

Owns eligible targets, hover expansion, visual state, payload decoding, and command dispatch. It never mutates workspace documents directly.

Existing SQL editor adapter

Remains unchanged except for regression coverage proving the second MIME payload does not interfere with SUBQUERY_MIME.

Tests

Payload/source

  • Library rows publish both identity and subquery MIME payloads;
  • History rows publish only the existing subquery payload;
  • Dashboard-owned queries cannot start Library assignment;
  • drag start does not open a query;
  • star/rename/delete remain usable;
  • stale/missing/no-longer-Library source cancels safely.

Targeting

  • Dashboard row and Panels group accept panel assignment;
  • inferred Variables rows accept SQL assignment;
  • Variables group, orphan rows, member rows, and unrelated UI reject assignment;
  • hover auto-expands without navigating;
  • drop/cancel/Escape/workspace switch clears every visual state;
  • drop does not trigger row navigation or action-menu handlers.

Panel mutation

  • source remains unchanged and in Library;
  • clone preserves SQL, Spec version, name, description, panel data, variants, time ranges, and extension fields;
  • clone receives a new ID, panel role, and no favourite;
  • exactly one new tile references it;
  • layout/fallback normalisation is correct;
  • target revision increments once;
  • non-target Dashboards remain unchanged;
  • repeated drops create independent copies;
  • validation/persistence failure leaves the workspace unchanged.

Variable mutation

  • exact inferred variable receives the source SQL;
  • no query/tile/owner is created;
  • source remains unchanged;
  • Dashboard revision increments once;
  • case-sensitive variable identity is preserved;
  • orphan and disappeared variable targets abort;
  • blank source SQL does not delete an existing configuration;
  • parameterised or locally invalid SQL is preserved and follows existing diagnostics;
  • result-shape behaviour remains unchanged pending Validate dashboard-variable option SQL on editor Run #465;
  • clean open variable tab reconciles;
  • dirty open variable tab is preserved and assignment is rejected.

Existing editor regression

  • Library drop still inserts a ( … ) subquery at the drop position;
  • History drop still works;
  • editor insertion remains undoable;
  • no Dashboard mutation occurs;
  • the additional identity MIME does not alter editor behaviour.

Concurrency/accessibility

  • same-workspace changes during drag are rebased over latest committed state;
  • workspace switch aborts;
  • source/target deletion aborts without partial writes;
  • keyboard Add to dashboard… matches drag semantics for Panel and Variable destinations.

Acceptance criteria

  • One Library drag supports the three destination semantics documented above.
  • Dashboard/Panels drop creates one independent panel-owned query copy and tile.
  • Variables-row drop copies SQL only into the exact Dashboard variable configuration.
  • Main-editor drop retains the existing subquery-insertion behaviour.
  • Source Library queries are never modified or removed from Library.
  • Assignment uses stable IDs and the latest committed workspace, never names or stale serialized query objects.
  • All persistence mutations are atomic, revision-correct, layout-safe, and fully validated.
  • Dirty variable drafts cannot be overwritten by a drop.
  • A keyboard-accessible Add to dashboard… command provides equivalent Panel/Variable assignment. — DEFERRED to Keyboard-accessible "Add to dashboard…" command for Library queries #483 (owner decision, 2026-07-27). The drag path ships without it, so assignment is pointer-only until Keyboard-accessible "Add to dashboard…" command for Library queries #483 lands.

Non-goals

  • Reimplementing the existing editor-subquery drop.
  • Restoring curated filters, filter-role queries, provider queries, or configuration dialogs.
  • Creating a variable that is not already inferred from panel SQL.
  • Moving/copying existing Dashboard-owned members.
  • Sharing one panel query among members.
  • Reordering Dashboards, Variables, or Panels by this drag.
  • Dropping directly onto the Dashboard canvas or an individual tile.
  • Touch-specific drag interaction.
  • Automatically opening or executing the destination after assignment.
  • Persisting source-copy lineage.

Activity

  1. added
    inboxFiled mid-task; not yet triaged into the roadmap
    enhancementNew feature or request
    and removed
    inboxFiled mid-task; not yet triaged into the roadmap
    on Jul 24, 2026
  2. added 2 commits that reference this issue on Jul 25, 2026
  3. changed the title [-]Add copy-on-drag assignment from Library to Dashboards[/-] [+]Add Library drag assignment to Dashboard panels and variables[/+] on Jul 26, 2026
  4. BorisTyshkevich commented on Jul 27, 2026

    @BorisTyshkevich
    CollaboratorAuthor

    Ship log — #428

    Single-phase issue (no ## Phases, no ### Phase N headings). One branch off
    main, one PR.

    Phase Status PR
    — (whole issue, drag path) shipped #484

    Handoff

    Landed (branch feat/library-drag-assignment-428, commit 20a28b8)

    New modules, matching the issue's own "Suggested boundaries":

    • src/core/library-drag.ts — the identity payload (LibraryQueryDragPayload,
      encode/decode) and the LibraryDropTarget union. Fail-closed decode: a foreign
      or malformed payload is one null, not a parse guard per call site.
    • src/dashboard/application/library-assignment.ts — the two pure transforms
      copyLibraryQueryToPanel / copyLibraryQuerySqlToVariable, each returning a
      complete candidate or one of eight typed aborts.
    • src/application/library-assignment-service.ts — the mutateWorkspace
      plumbing, modelled on dashboard-variable-config.ts, plus
      libraryAssignmentMessage (the abort→sentence table, kept out of the view).
    • src/ui/tabs.ts gains reconcileVariableTab.
    • DashboardTreeRow.dropTarget in application/dashboard-tree-model.ts;
      the drop controller in ui/dashboard-tree.ts; the dual payload in
      ui/saved-history.ts; LIBRARY_QUERY_MIME in ui/dnd-mime.ts.

    Owner decisions taken this run (NOT in the issue body)

    1. Acceptance bullet 9 — the keyboard Add to dashboard… chooser — is DEFERRED
      to Keyboard-accessible "Add to dashboard…" command for Library queries #483.
      This PR ships the drag path only. Until Keyboard-accessible "Add to dashboard…" command for Library queries #483 lands, assignment is
      pointer-only, which is a real accessibility gap and is stated as such in Keyboard-accessible "Add to dashboard…" command for Library queries #483.
      src/ui/library-assign-menu.ts was planned and then not built; Keyboard-accessible "Add to dashboard…" command for Library queries #483 carries
      the design (two-stage openMenu) and the reminder to add that file to
      tests/unit/typography-contract.test.js's source list.
    2. No mobile support, and therefore NO isMobile branches anywhere. Touch
      drag is already a stated non-goal, so no drag can start on touch and the drop
      target needs no gate — adding one would be unreachable branches the coverage
      config forbids. draggable stays unconditional on Library AND History rows;
      removing it on mobile would have broken the shipped PR feat(editor): drag a Library/History query into the editor as a ( … ) subquery #40 editor drop, which
      this issue's integration contract forbids changing.
    3. The editor-subquery path is untouched in production code. The only work
      there is regression coverage.

    Decisions taken under ambiguity

    • Eligibility is a row FIELD (row.dropTarget), resolved in the tree model,
      not a rule in the view. ui/dashboard-tree.ts is at a 100/100/95/90 per-file
      gate and the model owns every other structural decision. A first draft put a
      libraryDropTarget(row) function in src/core with a hand-redeclared
      structural row type; that re-typed DashboardTreeInvalid as a bare string, so
      renaming the literal in the model would have silently stopped rejecting orphans.
    • A CONFLICTED variable accepts a drop; an ORPHANED one does not. A conflict
      is a type disagreement between panels, orthogonal to where option SQL is
      stored; an orphan has nothing to bind to.
    • The dirty-variable-tab gate runs INSIDE the mutateWorkspace transform, via
      an injected readTabs(). A pre-dispatch check is a snapshot: the primitive
      queues behind serializeWrite then awaits loadById, and a keystroke in that
      window flips the tab dirty after the check. src/application may import
      src/state.ts (dashboard-variable-config.ts already does), so this is
      satisfiable — an earlier plan wrongly argued it was not.
    • No Escape handler. The browser consumes keydown during a native drag and
      delivers dragend; a key listener would be a branch no test could reach. Escape
      is covered by dragend, and there is a test saying so.
    • Drag feedback deliberately causes no repaint. Eligibility is static
      data-droptarget markup revealed by one dash-dragging class on the list; the
      active target is re-applied from app._dashTreeDropKey after any paint. An
      AppState signal (the first plan) would have repainted on drag start and on
      every hover expand — and replaceChildren() destroys the row under the pointer.
    • Hover auto-expand is decoupled from eligibility. A Dashboard row opens
      itself and both groups; the Variables group hover-expands while still
      rejecting drops. Gating on eligibility left variable rows unreachable by drag.
    • The hover timer carries its owning row key and a new row TAKES OVER a
      pending timer, because dragenter on the new target fires before dragleave
      on the old one.
    • endLibraryDrag is wired into cancelDashboardTreeClicks, so a workspace
      switch / role change / dispose cancels a pending expansion. Without it a timer
      armed before a refresh fires afterwards and re-adds an expandedDashboardIds
      entry the refresh just pruned.
    • Panel assignment routes through applyCommand('add-query-instance'), not a
      hand-rolled tiles.push + normalize: only the canonical add path seeds
      placement from spec.dashboard.sizeHints, which grafana-grid@1 requires. The
      as Extract<…,{ok:true}> cast is documented — that command's only failure is an
      unresolvable queryId, and the resolver is built over the collection the clone
      was just added to.
    • Ids come from the injected crypto.randomUUID seam, not deriveOwnedQueryId.
      Derived q-own-… ids keep migration/import idempotent; a drag is neither, and a
      content-derived id would fight repeated drops. The id-collision abort stays.
    • blank-sql is checked LAST, after the Dashboard and variable resolve, so a
      blank query dropped on a deleted Dashboard does not tell the user to go fix a
      tab on a Dashboard that no longer exists.
    • Eligible rows get a faint dashed outline, not --bg-highlight. That token
      already means "the current Dashboard/member"; reusing it made every eligible row
      indistinguishable from the open one for the whole drag.
    • A successful drop moves the roving keyboard row but does not steal DOM focus.
      After a mouse gesture, yanking focus is worse than not.

    Review findings folded in (independent read-only pass, no blockers): the
    cancelDashboardTreeClicks teardown, the service reporting its committed sql
    instead of the UI re-deriving it from a projection, the hover-timer hand-over, the
    blank-sql ordering, the eligible-highlight collision, dropping the unreachable
    ondrop guard, and four tests that asserted less than their names claimed
    (timer count, no-repaint, star/rename/delete actually invoked).

    Deferred acceptance criteria — bullet 9 → #483. Bullets 1–8 claimed.

    Reconciled: CHANGELOG.md [Unreleased], an ADR-0003 addendum
    (#428, 2026-07-27), the dash-empty copy in ui/dashboard.ts (which said
    dragging a query onto a Dashboard "does not exist yet"), and #447's ship log —
    its phase 3 is no longer blocked on this issue.

    Follow-ups filed: #483 (enhancement, accessibility — the keyboard chooser).

    Gate: 5753 unit tests / 178 files, per-file coverage met, tsc --noEmit,
    check:arch and check:schemas clean, build 1,829,855 bytes. e2e 281 passed on
    chromium + webkit (tests/e2e/dashboard-tree.html needed a third, UNTILED query —
    both seeded queries were tiled, so its Library was empty and there was no
    .saved-row to drag). The drag CSS is asserted against real computed styles in
    Chromium/WebKit, since happy-dom sees no cascade at all.

    Follow-up round (2026-07-27, commit e7be508)

    Three items from owner review of the open PR.

    1. CI was red — and the local gate could not have caught it.
    tests/unit/dashboard-tree.test.ts dereferenced a .find() result without
    proving it exists; tsc --noEmit failed before vitest ran, so the build and e2e
    jobs never started.

    The reason it shipped: this sandbox has npm ignore-scripts=true set
    globally, so npm test silently skips pretest — check:schemas,
    check:arch and check:types never run. A green local npm test is NOT the
    repo's gate here, despite CLAUDE.md hard rule 1 describing tsc as "wired into the
    pretest step" (true of package.json, false of this machine). Run the three
    explicitly before pushing. Saved as a memory. Related trap: npx tsc --noEmit | head reports head's exit code, so a failure reads as a pass.

    2. A drop now opens what it created (owner request, REVERSES a spec line).
    #428's "After success" said not to switch surfaces; landing on an
    unchanged-looking Dashboard read as "did anything happen?". Now:

    • the tree expands to the created row, makes it the keyboard row, and focuses
      it — renderDashboardTree restores focus only when the tree already held it,
      and after a mouse drop it does not, so previously the row was the arrow-key
      origin while off-screen;
    • the assigned document opens in the editor: for a panel its new owned copy
      (never the Library original — editing that would not touch the panel), for a
      variable its Variable: <name> tab.

    Still true: the Dashboard is never opened in View/Edit, and nothing is executed.
    The issue body carries an "Owner revision" note on both affected bullets.

    3. The dirty-variable-tab race was NOT closed by the in-transform gate.
    The gate closes the queue-and-load window. mutateWorkspace then awaits
    workspace.commit(candidate), and a keystroke in that window passes every
    check. reconcileVariableTab correctly refused to adopt — and the caller ignored
    the refusal and reported success. Result: committed SQL, a diverging dirty draft,
    no signal, and the next Save silently reverting the assignment. Precisely the
    state the PR claimed to prevent. A blocked or slow IndexedDB transaction widens
    the window materially.

    No check can close it: the commit is the repository's atomic write and UI state
    cannot be held still across it. So the outcome carries draftDiverged,
    re-read after the commit resolves, and the drop surfaces it — a toast that does
    not auto-dismiss (an action suppresses the timer) with a Discard draft
    action wired to a new discardVariableDraft. reconcileVariableTab still refuses
    a dirty tab; nothing discards typing automatically.

    Why the old test could not see it: it dirtied the tab before the transform
    ran. The fake mutateWorkspace now has a duringCommit hook, exercised at both
    the service and drop-controller levels.

    Gate: 5760 unit tests, check:types / check:arch / check:schemas all run
    explicitly and clean, build 1,830,478 bytes, e2e 281 passed on chromium + webkit.

    Merged (2026-07-27)

    PR #484 merged into main. Final branch state bca4414, on a merge of main
    through #482 (f2cb8cb) — the only conflict was CHANGELOG.md and was purely
    additive (both the #428 and #463 entries kept); main's changes to
    dashboard-tree.ts and dashboard.ts were comment-only. CI green on the new
    merge base: 5812 unit tests, check:types/check:arch/check:schemas clean,
    281 e2e on chromium + webkit.

    Two late review fixes are in that final state:

    • draftDiverged means the draft DIFFERS, not merely that it is dirty
      (bca4414). Typing and undoing back to the assigned text left the flag set with
      byte-identical content — a false alarm about a Save that could not change
      anything. The in-transform GATE deliberately still keys on plain dirtiness:
      Add Library drag assignment to Dashboard panels and variables #428's own wording is "matching dirty variable tab: reject", and refusing there
      discards nothing. The asymmetry is between "refuse to act" (conservative is
      right) and "warn the user" (precision matters).
    • The PR description was rewritten; it still asserted the superseded "a drop must
      not leave the Dashboard" rule that item 2 of the follow-up round reversed.

    #428 remains OPEN deliberately: acceptance bullet 9 (the keyboard
    Add to dashboard… command) is unmet and tracked in #483. Until that lands,
    Library→Dashboard assignment is pointer-only. Close #428 only when #483 ships, or
    close it now and let #483 carry the remaining scope — an owner call, not taken
    here.

  5. added 3 commits that reference this issue on Jul 27, 2026
  6. added a commit that references this issue on Jul 29, 2026
  7. added 5 commits that reference this issue on Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions