fix(tui): pin marketplace sources and keep transcript drags out of the composer

Install sources in the bundled first-party marketplace snapshot pointed at
the moving `refs/heads/main` archive while the catalog displayed a reviewed
revision, so the bytes a user installed were not the bytes the snapshot
described and `/plugin update` re-downloaded a whole-repository tarball that
changed with every unrelated commit. Pin each source to the snapshot's
revision; freshness comes from bumping the pin, which the marketplace-sync
workflow already reports weekly, and `update` now finds no change until the
pin moves. The store test and acceptance fixture contracts are unchanged.

A transcript selection or scrollbar drag released over the composer was
consumed by the composer handler: the caret moved, a composer selection
started and the transcript drag flag never cleared. Let the surface that
started the drag finish it.

Document local-time and DST evaluation, missed-run coalescing, overlap
protection and approval behavior for automations.

Validation: tui::mouse_ui tests and plugins::marketplace tests passed
(0 failed); the regenerated snapshot is byte-identical to the sync script's
output for the pinned revision.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Hunter BandClaude Fable 5.1 committed 2026-09-12 20:04:02 -07:00
1 parent d1545dfcfa
commit 080cae8492
5 files changed
+77 -7

No files matched your search

@@ -8,28 +8,28 @@
"plugins": [
{
"name": "computer-use",
"source": "https://codeload.github.com/Hmbown/codewhale-plugin-marketplace/tar.gz/refs/heads/main#path=plugins/computer-use",
"source": "https://codeload.github.com/Hmbown/codewhale-plugin-marketplace/tar.gz/0b7f680c7e74762e0bf369717f3d44cd137603d4#path=plugins/computer-use",
"version": "0.2.1",
"description": "See the screen and operate it: accessibility-first control, screenshots and zoom on macOS, Windows and Linux, plus macOS recording and HarmonyOS devices over hdc.",
"homepage": "https://github.com/Hmbown/codewhale-plugin-marketplace"
},
{
"name": "whalewiki",
"source": "https://codeload.github.com/Hmbown/codewhale-plugin-marketplace/tar.gz/refs/heads/main#path=plugins/whalewiki",
"source": "https://codeload.github.com/Hmbown/codewhale-plugin-marketplace/tar.gz/0b7f680c7e74762e0bf369717f3d44cd137603d4#path=plugins/whalewiki",
"version": "0.1.0",
"description": "An evidence-bound wiki for the repo: generated pages sealed against the source files they were written from, computed staleness, a zero-install CI drift gate, and a read-only MCP surface.",
"homepage": "https://github.com/Hmbown/codewhale-plugin-marketplace"
},
{
"name": "cloudflare-docs",
"source": "https://codeload.github.com/Hmbown/codewhale-plugin-marketplace/tar.gz/refs/heads/main#path=plugins/cloudflare-docs",
"source": "https://codeload.github.com/Hmbown/codewhale-plugin-marketplace/tar.gz/0b7f680c7e74762e0bf369717f3d44cd137603d4#path=plugins/cloudflare-docs",
"version": "0.1.0",
"description": "Cloudflare's official remote docs MCP: current Workers/Pages/storage/platform documentation search. No credentials required.",
"homepage": "https://github.com/Hmbown/codewhale-plugin-marketplace"
},
{
"name": "codewhale-skills",
"source": "https://codeload.github.com/Hmbown/codewhale-plugin-marketplace/tar.gz/refs/heads/main#path=skills",
"source": "https://codeload.github.com/Hmbown/codewhale-plugin-marketplace/tar.gz/0b7f680c7e74762e0bf369717f3d44cd137603d4#path=skills",
"version": "1.0.0",
"description": "The Codewhale skill set as one reviewed plugin: implement, debug, test, review, verify, webapp-testing, release, handoff, research and the rest of the bundled workflow skills. Each arrives namespaced as codewhale-skills:<name>. These skills also ship inside the Codewhale binary; install this bundle to carry a reviewed copy to a host that only loads plugins.",
"homepage": "https://github.com/Hmbown/codewhale-plugin-marketplace"
+11
View File
@@ -409,6 +409,17 @@ pub(crate) fn handle_composer_mouse(app: &mut App, mouse: MouseEvent) -> bool {
if !app.view_stack.is_empty() {
return false;
}
// A transcript selection or scrollbar drag that ends over the composer
// belongs to the surface that started it: the transcript handler must
// still see the release to clear its drag state and publish the text.
if matches!(
mouse.kind,
MouseEventKind::Drag(MouseButton::Left) | MouseEventKind::Up(MouseButton::Left)
) && (app.viewport.transcript_selection.dragging
|| app.viewport.transcript_scrollbar_dragging)
{
return false;
}
// Use outer area for hit-testing (includes border).
let Some(area) = app.viewport.last_composer_area else {
return false;
@@ -163,3 +163,49 @@ fn middle_click_respects_overlays_missing_composer_and_remote_clipboard() {
assert_eq!(app.status_message, status, "{mode}");
}
}
#[test]
fn transcript_drag_released_over_composer_keeps_caret_and_clears_drag() {
let (mut app, text) = composer();
app.input = "draft".into();
app.cursor_position = 5;
app.history = vec![HistoryCell::User {
content: "SELECTED_LINE".into(),
}];
app.resync_history_revisions();
app.viewport.transcript_cache.ensure(
&app.history,
&app.history_revisions,
80,
app.transcript_render_options(),
);
app.viewport.transcript_selection.anchor = Some(TranscriptSelectionPoint {
line_index: 0,
column: 0,
});
app.viewport.transcript_selection.head = Some(TranscriptSelectionPoint {
line_index: 0,
column: 80,
});
app.viewport.transcript_selection.dragging = true;
let expected = selection_to_text(&app).unwrap();
// Dragging across the composer must not move its caret or start a
// composer selection.
handle_mouse_event(
&mut app,
event(MouseEventKind::Drag(MouseButton::Left), text.x + 2, text.y),
);
assert_eq!(app.cursor_position, 5);
assert!(app.selection_anchor.is_none());
assert!(app.viewport.transcript_selection.dragging);
// Releasing over the composer finishes the transcript drag.
handle_mouse_event(
&mut app,
event(MouseEventKind::Up(MouseButton::Left), text.x + 2, text.y),
);
assert!(!app.viewport.transcript_selection.dragging);
assert_eq!(app.cursor_position, 5);
assert_eq!(app.input, "draft");
assert_eq!(app.clipboard.read_primary_text(), Some(expected));
assert_eq!(app.clipboard.last_written_text(), Some("REGULAR_CLIPBOARD"));
}
+10
View File
@@ -2661,6 +2661,16 @@ has no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps
existing permission settings, custom schedules, and additional workspace
entries unless the corresponding supported field is explicitly changed.
Schedules are evaluated in the machine's local time zone against the wall
clock. A wall time that does not exist on a spring-forward day is skipped and
an ambiguous fall-back time fires once. Occurrences missed while Codewhale was
closed, asleep, or still running the previous occurrence are coalesced: the
next start runs one catch-up occurrence and then continues from the next
future slot, never replaying every missed slot. An occurrence never starts
while an earlier run of the same automation is still queued or running. Each
run is recorded durably with its status, timing and error; a run that needs a
tool approval has no operator to ask and fails once the approval wait expires.
Choosing a concrete model pins both the model and its exact configured
provider, including named custom routes. Later changes to the active provider
do not move that automation's pin. The default-model choice and legacy
+6 -3
View File
@@ -26,9 +26,12 @@ for candidate in catalog["plugins"]:
relative = spec[5:]
if any(part in ("", ".", "..") or not all(c.isascii() and (c.isalnum() or c in "-_.") for c in part) for part in relative.split("/")):
raise SystemExit(f"unsafe bundle path: {relative}")
# Mutable update channel; installed content is hash-bound and re-reviewed
# when changed. The catalog snapshot itself records its exact source SHA.
candidate["source"] = f"https://codeload.github.com/Hmbown/codewhale-plugin-marketplace/tar.gz/refs/heads/main#path={relative}"
# Pin every install source to the reviewed marketplace revision so the
# bytes a user installs are the bytes this snapshot describes. Freshness
# comes from bumping the pin (the marketplace-sync workflow reports drift
# against `main` weekly); `/plugin update` re-downloads the same archive
# and reports no change until the pin moves.
candidate["source"] = f"https://codeload.github.com/Hmbown/codewhale-plugin-marketplace/tar.gz/{revision}#path={relative}"
snapshot = {"repository": REPOSITORY, "revision": revision, "catalog": catalog}
rendered = json.dumps(snapshot, indent=2, ensure_ascii=False) + "\n"
output = ROOT / "crates/tui/assets/first-party-marketplace.json"