From dd57b339fd4eca3929378ebe9d59b083eb9fbcaf Mon Sep 17 00:00:00 2001 From: GeneAI Date: Mon, 31 Aug 2026 00:55:01 -0400 Subject: [PATCH] fix: replace modal workspace confirmation --- .claude-plugin/marketplace.json | 4 +- CHANGELOG.md | 7 +++ plugin/.claude-plugin/plugin.json | 2 +- pyproject.toml | 2 +- src/attune_forms/workspace.py | 17 ++++++- tests/test_workspace.py | 82 ++++++++++++++++++++++++++++++- 6 files changed, 108 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 9ef2fab..44fdba8 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,14 +7,14 @@ }, "metadata": { "description": "Structured agent-user communication: validated forms, decision cards with recommendations, structured pushback, and progress reports \u2014 batch questions instead of asking one at a time.", - "version": "0.11.0" + "version": "0.11.1" }, "plugins": [ { "name": "attune-forms", "description": "The communication grammar for AI agents: batch independent questions into ONE validated form; offer recommendations as decision cards with rationales and per-option tradeoffs; disagree constructively via pushback cards; report multi-step progress with a blocked-item picker. Renders rich HTML where the host supports widgets and degrades cleanly to plain questions everywhere else. Powered by the attune-forms PyPI package via a bundled MCP server.", "source": "./plugin", - "version": "0.11.0", + "version": "0.11.1", "author": { "name": "Smart AI Memory", "email": "admin@smartaimemory.com" diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae833c..e3ed130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ follow [SemVer](https://semver.org/). ## [Unreleased] +## [0.11.1] — 2026-08-31 + +### Fixed +- Consequential workspace actions now use an inline two-click confirmation + instead of `window.confirm`, which is unavailable in sandboxed MCP App + hosts. Choosing another action disarms and restores the pending action. + ## [0.11.0] — 2026-08-30 Project path selection becomes a reusable form capability while preserving diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 352b4a9..0f8fde5 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "attune-forms", - "version": "0.11.0", + "version": "0.11.1", "description": "Structured agent-user communication \u2014 validated forms, decision cards, pushback, progress reports, deliberation, triage boards, confirm gates, rankings, and assumption reviews via the attune-forms MCP server.", "author": { "name": "Smart AI Memory", diff --git a/pyproject.toml b/pyproject.toml index 1137878..8b35a6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "attune-forms" -version = "0.11.0" +version = "0.11.1" description = "Dynamic forms and command workspaces: validated multi-surface interaction documents for AI agents" readme = "README.md" requires-python = ">=3.10" diff --git a/src/attune_forms/workspace.py b/src/attune_forms/workspace.py index 77a75a5..08ccb04 100644 --- a/src/attune_forms/workspace.py +++ b/src/attune_forms/workspace.py @@ -650,12 +650,27 @@ def action_button(action: WorkspaceAction) -> str: ) script = f"""", html, re.DOTALL) + assert script is not None + harness = f""" +const assert = require('node:assert/strict'); +const listeners = {{}}; +const sent = []; +function button(action, label, explicit, consequence) {{ + const attrs = {{'data-workspace-action': action}}; + if (explicit) attrs['data-explicit'] = '1'; + if (consequence) attrs['data-consequence'] = consequence; + return {{ + textContent: label, + disabled: false, + getAttribute: function (name) {{ return attrs[name] || null; }}, + setAttribute: function (name, value) {{ attrs[name] = value; }}, + removeAttribute: function (name) {{ delete attrs[name]; }} + }}; +}} +const edit = button('edit_contract', 'Edit contract', false, null); +const run = button('run_fix', 'Run Fix', true, 'Execute the previewed contract.'); +const status = {{textContent: ''}}; +const root = {{ + addEventListener: function (name, callback) {{ listeners[name] = callback; }}, + contains: function () {{ return true; }}, + getAttribute: function (name) {{ + return name === 'data-workspace-title' ? 'Fix preview' : 'preview'; + }}, + querySelector: function () {{ return status; }}, + querySelectorAll: function (selector) {{ + if (selector === '[data-workspace-action]') return [edit, run]; + if (selector === '[data-explicit="1"][data-confirm-armed="1"]') {{ + return run.getAttribute('data-confirm-armed') === '1' ? [run] : []; + }} + return []; + }} +}}; +global.document = {{getElementById: function () {{ return root; }}}}; +global.sendPrompt = function (value) {{ sent.push(value); }}; +eval({json.dumps(script.group(1))}); +function click(target) {{ + listeners.click({{ + target: {{closest: function () {{ return target; }}}} + }}); +}} +click(run); +assert.equal(sent.length, 0); +assert.equal(run.textContent, 'Confirm Run Fix'); +assert.equal(run.getAttribute('data-confirm-armed'), '1'); +assert.match(status.textContent, /Click again to confirm/); +click(run); +assert.equal(sent.length, 1); +assert.match(sent[0], /"action":"run_fix"/); +assert.match(sent[0], /"confirmed":true/); + +run.disabled = false; +run.removeAttribute('data-confirm-armed'); +run.textContent = 'Run Fix'; +sent.length = 0; +click(run); +click(edit); +assert.equal(run.textContent, 'Run Fix'); +assert.equal(run.getAttribute('data-confirm-armed'), null); +assert.equal(sent.length, 1); +assert.match(sent[0], /"action":"edit_contract"/); +assert.match(sent[0], /"confirmed":false/); +""" + result = subprocess.run( + ["node", "-e", harness], + text=True, + capture_output=True, + check=False, + ) + assert result.returncode == 0, result.stderr + + def test_form_workspace_preserves_explicit_action_semantics() -> None: intake = showcase_views()[0] action = WorkspaceAction(