/** * Native JS UI bundle for the kandev-plugin-e2e fixture plugin * (docs/plans/plugins/PLUGIN-API.md). A self-contained ES module — no * imports, no bundled React — that calls `window.registerKandevPlugin(id, * plugin)` at evaluation time and, on `initialize(registry, host)`, * registers a nav item, a top-level route, a `task-sidebar` slot component, * a `main-top-bar` slot component, a `task.created` WS handler, and the * `open-demo` keybinding (declared in manifest.yaml's `ui.keybindings`, * default `mod+shift+j`) which opens a `host.openModal(...)` demo modal * containing a `host.ui` Tooltip. Uses only host.React/host.jsx/host.ui. * * The plugin page also renders a `host.theme` readout kept current purely * through `host.onThemeChange`, so an e2e can flip the app theme for real and * prove the subscription fires in a browser (jsdom's MutationObserver is a * shim, and jsdom cannot open a Radix tooltip from synthetic hover at all), * plus a button that fires `host.toast.error` against the real sonner * instance the app mounts. * * It also registers the plugin-hooks surface this fixture exists to drive * end to end: a "Notes" task panel (registerTaskPanel, mobile-enabled) that * round-trips a single per-user document through host.storage * (get on mount, debounced set, subscribe to pick up a write from another * tab/surface), task-card indicator/tag components, generic task-row metadata, * and task-menu actions under the "edit" and "primary" groups. It registers a * task-list facet (registerTaskListFacet) whose values a spec drives through * `window.__e2eFacetValues`, so /tasks facet sort and grouping can be exercised * against real plugin registrations. It also * registers one composer action on all three composer slots * (chat-input-actions, task-create-input-actions, new-session-input-actions), * which is how the e2e suite exercises PluginComposerCapability against real * native composers rather than a mock. * * The task-created counter lives in module scope (not component state) with * a tiny listener set, so it survives across route navigations (the page * component unmounts/remounts as the user navigates away and back). */ /* eslint-disable max-lines, max-lines-per-function -- The fixture is the literal self-contained, dependency-free browser bundle exercised and hashed by E2E. */ (function () { var moduleCount = 0; var listeners = new Set(); // Task-list facet plumbing. Values come from `window.__e2eFacetValues` // (taskId -> [{value,label,color}]) so a spec can drive multi-value // membership, colors, and the untagged fallback against task ids the // fixture cannot know ahead of time. `window.__e2eFacetNotify()` fires // the subscription so the reactive re-read path is exercised for real. var facetListeners = new Set(); function emit() { listeners.forEach(function (fn) { fn(moduleCount); }); } function incrementCount() { moduleCount += 1; emit(); } function useCounter(React) { var state = React.useState(moduleCount); var count = state[0]; var setCount = state[1]; React.useEffect(function () { setCount(moduleCount); listeners.add(setCount); return function () { listeners.delete(setCount); }; }, []); return count; } var FIXTURE_REPOSITORY_ID = "fixture-repository"; var FIXTURE_HELLO_PATH = "/plugins/e2e-hello"; var FIXTURE_SIDEBAR_SECTION = "sidebar-footer"; var PROVIDER_ID = "fixture-source-control"; var PULL_REQUEST_URL = "https://bitbucket.example.test/projects/TEAM/repos/fixture/pull-requests/42"; var REPOSITORY_URL = "https://bitbucket.example.test/scm/TEAM/fixture.git"; function fixtureRepository() { return { id: FIXTURE_REPOSITORY_ID, repositoryId: FIXTURE_REPOSITORY_ID, owner: "TEAM", ownerOrProject: "TEAM", name: "fixture", repositoryName: "fixture", fullName: "TEAM/fixture", url: REPOSITORY_URL, cloneUrl: REPOSITORY_URL, providerHost: "bitbucket.example.test", defaultBranch: "main", private: true, }; } function abortableRefresh(signal) { return new Promise(function (_resolve, reject) { if (signal.aborted) { reject(new Error("fixture review refresh aborted")); return; } signal.addEventListener( "abort", function () { reject(new Error("fixture review refresh aborted")); }, { once: true }, ); }); } window.registerKandevPlugin("kandev-plugin-e2e", { initialize: function (registry, host) { var React = host.React; var jsx = host.jsx; var ui = host.ui; // Reads host.theme once on mount, then tracks it purely through // host.onThemeChange — so the readout only stays correct if the // subscription actually fires. Also counts notifications, proving the // host does not re-notify on unrelated class churn. function useHostTheme() { var state = React.useState(function () { return { theme: host.theme, changes: 0 }; }); var value = state[0]; var setValue = state[1]; React.useEffect(function () { return host.onThemeChange(function (next) { setValue(function (prev) { return { theme: next, changes: prev.changes + 1 }; }); }); }, []); return value; } function PluginPage() { var count = useCounter(React); var connectionState = React.useState("Not checked"); var connection = connectionState[0]; var setConnection = connectionState[1]; var themeState = useHostTheme(); function checkConnection() { host.api .invokeAction("connection-status", { workspaceId: host.store.getState().workspaces.activeId || undefined, }) .then(function (result) { setConnection( result.connected ? "Connected" : result.error || "Connection unavailable", ); }) .catch(function (error) { setConnection(error instanceof Error ? error.message : "Connection unavailable"); }); } return jsx( "div", { id: "hello-plugin-page-root" }, jsx("h1", { id: "hello-plugin-page" }, "Hello E2E"), jsx("span", { id: "hello-task-counter" }, String(count)), jsx( "button", { id: "fixture-connection-status", "data-testid": "fixture-connection-status", type: "button", onClick: checkConnection, }, "Check Bitbucket connection", ), jsx( "span", { id: "fixture-connection-result", "data-testid": "fixture-connection-result" }, connection, ), jsx( "span", { "data-testid": "hello-theme-readout", "data-theme-changes": String(themeState.changes), }, themeState.theme, ), // host.toast is a per-plugin Proxy over sonner; unit tests only ever // see it against a mocked sonner, so this button is how an e2e // proves a real toast renders and that .error does NOT file a // frontend-error report. jsx( "button", { "data-testid": "hello-toast-error", onClick: function () { host.toast.error("Plugin toast error"); }, }, "toast error", ), ); } function FixtureReviewPanel(props) { return jsx( "section", { "data-testid": "fixture-review-panel-" + props.presentation, "data-review-key": props.reviewKey, }, jsx( "h2", null, "Bitbucket pull request #" + props.reviewKey.replace("pull-request-", ""), ), jsx("p", null, "Provider-neutral fixture review panel"), ); } function FixtureReviewSelector() { return jsx("span", { "data-testid": "fixture-review-selector" }, "Bitbucket"); } function openLinkResult(context) { host.openTaskLinkDialog({ title: "Link Bitbucket pull request", description: "Use a Bitbucket pull request URL for this task.", inputLabel: "Pull request", placeholder: PULL_REQUEST_URL, emptyError: "Enter a Bitbucket pull request URL.", failureMessage: "Failed to link Bitbucket pull request.", successMessage: "Bitbucket pull request linked", inputTestId: "fixture-link-pull-request-input", errorTestId: "fixture-link-pull-request-error", submitTestId: "fixture-link-pull-request-submit", onSubmit: function (reference) { return host.api .invokeAction("link-pull-request", { workspaceId: context.workspaceId, taskId: context.taskId, body: { pullRequestUrl: reference }, }) .then(function (result) { if (!result.linked) { throw new Error(result.error || "Connection unavailable"); } }); }, }); return Promise.resolve(); } function SidebarSlot() { return jsx("div", { id: "hello-sidebar" }, "Hello E2E sidebar"); } function MainTopBarSlot(props) { var slotProps = props.slotProps || {}; var label = "Hello " + slotProps.currentPage; return jsx( ui.Button, { id: "hello-main-top-bar", variant: "outline", size: "icon-sm", "aria-label": label, }, jsx( "svg", { className: "h-4 w-4", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "aria-hidden": "true", }, jsx("path", { d: "M5 12h14M12 5l7 7l-7 7" }), ), jsx("span", { className: "sr-only" }, label), ); } function ChatTopBarStatus(props) { var slotProps = props.slotProps || {}; if (slotProps.presentation !== "mobile") return null; return jsx( "span", { "data-testid": "e2e-chat-top-bar-status", "data-task-id": slotProps.taskId || "", "data-workspace-id": slotProps.workspaceId || "", "data-active-session-id": slotProps.activeSessionId || "", "data-session-ids": (slotProps.sessionIds || []).join(","), "data-presentation": slotProps.presentation || "unknown", className: "min-w-0 max-w-full truncate rounded-md border px-3 py-2 text-xs text-muted-foreground", }, "Fixture archive synchronization is ready", ); } function ChatTopBarAction(props) { var slotProps = props.slotProps || {}; var activeState = React.useState(false); var active = activeState[0]; var setActive = activeState[1]; if (slotProps.presentation !== "mobile") return null; return jsx( ui.Button, { type: "button", variant: "outline", className: "cursor-pointer", "data-testid": "e2e-chat-top-bar-action", "data-presentation": slotProps.presentation || "unknown", "data-activated": active ? "true" : "false", onClick: function () { setActive(true); }, }, active ? "Fixture action complete" : "Run fixture task action", ); } // Debounce delay for the Notes panel's autosave — short, so e2e specs // don't need to wait long for a write to reach host.storage. var NOTES_SAVE_DEBOUNCE_MS = 150; function useNotesValue(taskId, panelId) { var state = React.useState(""); var value = state[0]; var setValue = state[1]; var loadedState = React.useState(false); var loaded = loadedState[0]; var setLoaded = loadedState[1]; React.useEffect( function () { var cancelled = false; // Each notification (subscribe's refresh callback, plus the initial // call) starts its own host.storage.get. Two in flight at once can // resolve out of order, so `cancelled` alone (unmount-only) isn't // enough — an older read landing after a newer one would overwrite // it. Only the read matching the current generation may commit. var refreshGeneration = 0; function refresh() { var generation = ++refreshGeneration; host.storage.get("task", taskId, "note").then( function (entry) { if (cancelled || generation !== refreshGeneration) return; setValue(entry ? entry.value : ""); setLoaded(true); }, function () { if (cancelled || generation !== refreshGeneration) return; setLoaded(true); }, ); } refresh(); // Scope echo suppression to this panel instance (panelId) rather // than the host's shared tab-wide default writerId. The kanban // "Enhance notes" action below writes this same note using that // shared default (it has no ongoing subscription of its own to // protect) — if this panel used the same default, the action's // write would look like this panel's own echo and get silently // dropped instead of refreshing the panel. var unsubscribe = host.storage.subscribe( { scope: "task", scopeId: taskId, key: "note", writerId: panelId }, refresh, ); return function () { cancelled = true; unsubscribe(); }; }, [taskId, panelId], ); return [value, setValue, loaded]; } function NotesPanel(props) { var taskId = props.taskId; var panelId = props.panelId; var notesValue = useNotesValue(taskId, panelId); var value = notesValue[0]; var setValue = notesValue[1]; var loaded = notesValue[2]; var debounceRef = React.useRef(null); function handleChange(e) { var next = e.target.value; setValue(next); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(function () { host.storage .set("task", taskId, "note", next, { writerId: panelId }) .catch(function () { // surface the failed autosave instead of dropping it silently }); }, NOTES_SAVE_DEBOUNCE_MS); } if (!loaded) { return jsx("div", { "data-testid": "e2e-notes-panel-loading" }, "Loading notes…"); } return jsx("textarea", { "data-testid": "e2e-notes-panel", "data-presentation": props.presentation, value: value, onChange: handleChange, }); } function promptDuration(startedAt, completedAt, translated) { var started = Date.parse(startedAt); var completed = Date.parse(completedAt); if (Number.isNaN(started) || Number.isNaN(completed)) return ""; var seconds = Math.max(0, Math.floor((completed - started) / 1000)); var hours = Math.floor(seconds / 3600); var minutes = Math.floor((seconds % 3600) / 60); var remaining = seconds % 60; if (hours > 0) { return ( hours + translated.t("durationHours") + " " + minutes + translated.t("durationMinutes") + " " + remaining + translated.t("durationSeconds") ); } if (minutes > 0) { return ( minutes + translated.t("durationMinutes") + " " + remaining + translated.t("durationSeconds") ); } return remaining + translated.t("durationSeconds"); } function PromptHistoryPluginRow(props) { var message = props.message; var turn = props.turn; var translated = props.translated; var duration = turn && turn.completedAt ? promptDuration(turn.startedAt, turn.completedAt, translated) : ""; var favorite = props.conversation.history.useMessageFavorite(props.sessionId, message.id); var isExpanded = props.expanded === message.id; return jsx( "article", { "data-testid": "fixture-prompt-history-row", "data-message-id": message.id, "data-favorite": favorite ? "true" : "false", className: "border-b p-3", }, jsx( "button", { type: "button", className: "min-h-11 w-full text-left", onClick: function () { props.setExpanded(isExpanded ? null : message.id); }, }, translated.t("prompt", { values: { index: message.promptIndex || 0 } }), message.senderTaskId ? " · " + translated.t("sentByAgent") : "", duration ? jsx("span", { "data-testid": "fixture-prompt-history-duration" }, " · " + duration) : "", ), isExpanded ? jsx( "div", { "data-testid": "fixture-prompt-history-detail" }, jsx(ui.PromptMentionText, { text: message.content, interactive: true }), jsx( "button", { type: "button", className: "min-h-11", onClick: function () { props.conversation.openMessage(message.id); }, }, translated.t("openPrompt"), ), ) : null, ); } function PromptHistoryPluginPanel(props) { var translated = host.i18n.useTranslation(); var messages = props.conversation.history.useSessionMessages({ sessionId: props.sessionId, taskId: props.taskId, authorTypes: ["user"], sort: "desc", pageSize: 2, }); var turns = props.conversation.history.useSessionTurns(props.sessionId, props.taskId); var expandedState = React.useState(null); var expanded = expandedState[0]; var setExpanded = expandedState[1]; if (messages.loading || turns.loading) { return jsx( "div", { "data-testid": "fixture-prompt-history-loading" }, translated.t("loading"), ); } if (messages.error || turns.error) { return jsx( "button", { type: "button", "data-testid": "fixture-prompt-history-error", onClick: function () { messages.retry(); turns.retry(); }, }, translated.t("retry"), ); } if (messages.messages.length === 0) { return jsx( "div", { "data-testid": "fixture-prompt-history-empty" }, translated.t("empty"), ); } var rows = messages.messages.map(function (message) { var turn = turns.turns.find(function (candidate) { return candidate.id === message.turnId; }); return jsx(PromptHistoryPluginRow, { key: message.id, message: message, turn: turn, translated: translated, conversation: props.conversation, sessionId: props.sessionId, expanded: expanded, setExpanded: setExpanded, }); }); if (messages.hasMore) { rows.push( jsx( "button", { key: "load-more", type: "button", className: "min-h-11 w-full", "data-testid": "fixture-prompt-history-load-more", disabled: messages.loadingMore, onClick: function () { messages.loadMore(); }, }, translated.t("loadOlder"), ), ); } return jsx( "div", { "data-testid": "fixture-prompt-history-panel", "data-removed": messages.removed ? "true" : "false", "data-presentation": props.presentation, className: "h-full min-w-0 overflow-y-auto overflow-x-hidden", }, rows, ); } function CardIndicator(props) { var slotProps = props.slotProps || {}; return jsx( "span", { "data-testid": "e2e-card-indicator", "data-task-id": slotProps.taskId }, "N", ); } function CardTags(props) { var slotProps = props.slotProps || {}; return jsx( "span", { "data-testid": "e2e-card-tags", "data-task-id": slotProps.taskId }, "tags", ); } function RowMetadata(props) { var slotProps = props.slotProps || {}; return jsx( "span", { "data-testid": "e2e-row-metadata", "data-task-id": slotProps.taskId, "data-surface": slotProps.surface, }, "fixture metadata", ); } function WorkspaceActionsSlot(props) { var slotProps = props.slotProps || {}; return jsx( "button", { type: "button", "data-testid": "e2e-sidebar-workspace-actions", "data-workspace-id": slotProps.workspaceId, "data-workspace-label": slotProps.workspaceLabel || "", "data-presentation": slotProps.presentation || "unknown", "aria-label": "Fixture workspace action", className: slotProps.presentation === "mobile" ? "flex h-11 w-11 cursor-pointer items-center justify-center rounded-md border" : "flex h-6 w-6 cursor-pointer items-center justify-center rounded-md border", onClick: function (event) { event.currentTarget.setAttribute("data-clicked", "true"); }, }, "W", ); } function StatusSlot(props) { var slotProps = props.slotProps || {}; var id = slotProps.placement === "left" ? "hello-status-left" : "hello-status-right"; return jsx( "span", { id: id }, "Hello status " + String(slotProps.presentation || "unknown") + " " + String(slotProps.activeTaskId || "no-task"), ); } // Drives PluginComposerCapability through native composers. Capturing // the capability and using it after later renders mirrors a real voice // integration whose recording completes asynchronously. var COMPOSER_BUTTON_STYLE = { fontSize: "9px", lineHeight: "1", padding: "2px 3px", minWidth: "16px", }; function ComposerAction(props) { var slotProps = props.slotProps || {}; var composer = slotProps.composer; var statusState = React.useState(""); var status = statusState[0]; var setStatus = statusState[1]; var capturedRef = React.useRef(null); function record(result) { setStatus(result && result.status ? result.status : String(result)); } return jsx( "span", { "data-testid": "e2e-composer-action", "data-surface": String(slotProps.surface), "data-presentation": String(slotProps.presentation), "data-task-id": String(slotProps.taskId || ""), "data-session-id": String(slotProps.activeSessionId || ""), "data-disabled": String(Boolean(slotProps.disabled)), "data-submittable": String(Boolean(slotProps.submittable)), "data-status": status, }, jsx( "button", { type: "button", "data-testid": "e2e-composer-insert", style: COMPOSER_BUTTON_STYLE, onClick: function () { record(composer.insertText("DICTATED")); }, }, "insert", ), jsx( "button", { type: "button", "data-testid": "e2e-composer-capture", style: COMPOSER_BUTTON_STYLE, onClick: function () { capturedRef.current = composer; setStatus("captured"); }, }, "capture", ), jsx( "button", { type: "button", "data-testid": "e2e-composer-insert-captured", style: COMPOSER_BUTTON_STYLE, onClick: function () { record( capturedRef.current ? capturedRef.current.insertText("DICTATED") : { status: "not-captured" }, ); }, }, "insert captured", ), jsx( "button", { type: "button", "data-testid": "e2e-composer-submit", style: COMPOSER_BUTTON_STYLE, onClick: function () { var target = capturedRef.current || composer; target.submit().then(record); }, }, "submit", ), jsx( "button", { type: "button", "data-testid": "e2e-composer-focus", style: COMPOSER_BUTTON_STYLE, onClick: function () { record(composer.focus()); }, }, "focus", ), ); } function FixtureBitbucketIcon(props) { return jsx( "svg", { className: props.className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "aria-hidden": props["aria-hidden"] || true, "data-testid": "fixture-bitbucket-icon", }, jsx("path", { d: "M4 5h16l-2.5 14h-11z" }), jsx("path", { d: "M9 9h6l-1 6h-4z" }), ); } registry.registerNavItem({ id: "e2e-hello", label: "Hello E2E", path: FIXTURE_HELLO_PATH, section: "main", }); registry.registerNavItem({ id: "e2e-insights-tools", label: "E2E Insights Tools", path: FIXTURE_HELLO_PATH, section: FIXTURE_SIDEBAR_SECTION, }); // Three more sidebar-footer items so this one plugin install alone // produces P = 4 (budget MAX_INLINE_PLUGIN_FOOTER_ITEMS = 3, plus one // over-budget item) — enough to drive the desktop footer's overflow // trigger and menu with the real Radix DropdownMenu in a browser (see // plugins.spec.ts's overflow test). The budget counts destinations, // not distinct plugins, so one plugin registering 4 items exercises // the same partition as 4 plugins registering 1 each. Labeled // "E2E Overflow Item N" rather than a numbered suffix of the first // item's own label ("E2E Insights Tools") so Playwright's default // substring name matching can't accidentally match more than one of // these from an existing test written against the first item's label. registry.registerNavItem({ id: "e2e-insights-tools-2", label: "E2E Overflow Item 2", path: FIXTURE_HELLO_PATH, section: FIXTURE_SIDEBAR_SECTION, }); registry.registerNavItem({ id: "e2e-insights-tools-3", label: "E2E Overflow Item 3", path: FIXTURE_HELLO_PATH, section: FIXTURE_SIDEBAR_SECTION, }); registry.registerNavItem({ id: "e2e-insights-tools-4", label: "E2E Overflow Item 4", path: FIXTURE_HELLO_PATH, section: FIXTURE_SIDEBAR_SECTION, }); registry.registerRoute(FIXTURE_HELLO_PATH, PluginPage); registry.registerComponent("task-sidebar", SidebarSlot); registry.registerComponent("main-top-bar", MainTopBarSlot); registry.registerComponent("chat-top-bar", ChatTopBarStatus); registry.registerComponent("chat-top-bar", ChatTopBarAction); registry.registerComponent("app-status-bar-left", StatusSlot); registry.registerComponent("app-status-bar-right", StatusSlot); registry.registerWsHandler("task.created", function () { incrementCount(); }); registry.registerRepositoryProvider({ id: PROVIDER_ID, label: "Bitbucket", icon: FixtureBitbucketIcon, listRepositories: function () { return Promise.resolve([fixtureRepository()]); }, matchesURL: function (url) { if (typeof url !== "string") return false; try { return new URL(url).hostname === "bitbucket.example.test"; } catch (_error) { return false; } }, listBranches: function (_context) { return Promise.resolve([{ name: "main" }, { name: "feature/provider-contract" }]); }, inspectURL: function (_context) { return Promise.resolve({ providerId: PROVIDER_ID, providerHost: "bitbucket.example.test", ownerOrProject: "TEAM", repositoryId: FIXTURE_REPOSITORY_ID, repositoryName: "fixture", cloneUrl: REPOSITORY_URL, defaultBranch: "main", baseBranch: "main", headBranch: "feature/provider-contract", pullRequest: { number: 42, title: "Provider-neutral contract" }, }); }, }); registry.registerTaskAction({ id: "link-bitbucket-pull-request", label: "Bitbucket Pull Request", icon: FixtureBitbucketIcon, placement: "link", group: "Link", run: openLinkResult, }); registry.registerReviewProvider({ id: PROVIDER_ID, label: "Bitbucket", icon: FixtureBitbucketIcon, changeRequestNoun: "Pull Request", order: 50, getSnapshot: function (taskId) { return [ { providerId: PROVIDER_ID, reviewKey: "pull-request-42", title: "Bitbucket Pull Request #42", url: PULL_REQUEST_URL, connectionScope: "https://bitbucket.example.test", repositoryId: FIXTURE_REPOSITORY_ID, changeRequestNumber: 42, state: "OPEN", statusBadge: { label: "Open" }, taskId: taskId, }, { providerId: PROVIDER_ID, reviewKey: "pull-request-43", title: "Bitbucket Pull Request #43", url: "https://bitbucket.example.test/projects/TEAM/repos/fixture/pull-requests/43", connectionScope: "https://bitbucket.example.test", repositoryId: FIXTURE_REPOSITORY_ID, changeRequestNumber: 43, state: "OPEN", statusBadge: { label: "Open" }, taskId: taskId, }, ]; }, subscribe: function () { return function () {}; }, refresh: function (_taskId, signal) { return abortableRefresh(signal); }, ReviewPanel: FixtureReviewPanel, Selector: FixtureReviewSelector, }); registry.registerTranslations({ en: { promptHistoryTitle: "Prompt history fixture", loading: "Loading prompt history", retry: "Retry", empty: "No prompts yet", prompt: "Prompt {{index}}", sentByAgent: "Sent by agent", openPrompt: "Open prompt", loadOlder: "Load older prompts", durationSeconds: "s", durationMinutes: "m", durationHours: "h", }, pseudo: { promptHistoryTitle: "Ƥřǿɱƥŧ ħīşŧǿřẏ ƒīẋŧŭřḗ", loading: "Ŀǿȧḓīƞɠ ƥřǿɱƥŧ ħīşŧǿřẏ", retry: "Řḗŧřẏ", empty: "Ƞǿ ƥřǿɱƥŧş ẏḗŧ", prompt: "Ƥřǿɱƥŧ {{index}}", sentByAgent: "Şḗƞŧ ƀẏ ȧɠḗƞŧ", openPrompt: "Ǿƥḗƞ ƥřǿɱƥŧ", loadOlder: "Ŀǿȧḓ ǿŀḓḗř ƥřǿɱƥŧş", durationSeconds: "ş", durationMinutes: "ɱ", durationHours: "ħ", }, }); registry.registerTaskPanel({ id: "prompt-history-plugin", title: "Prompt history fixture", titleKey: "promptHistoryTitle", icon: "history", Component: PromptHistoryPluginPanel, mobileEnabled: true, visible: function (context) { return context.sessionKind === "managed"; }, }); registry.registerTaskPanel({ id: "notes", title: "Notes", icon: "book", Component: NotesPanel, mobileEnabled: true, }); registry.registerTaskListFacet({ id: "fixture-tags", label: "Fixture tag", getValues: function (context) { var byTask = window.__e2eFacetValues || {}; if (byTask.__throwFor === context.taskId) throw new Error("fixture facet boom"); return byTask[context.taskId] || []; }, subscribe: function (listener) { facetListeners.add(listener); window.__e2eFacetNotify = function () { facetListeners.forEach(function (fn) { fn(); }); }; return function () { facetListeners.delete(listener); }; }, }); registry.registerComponent("chat-input-actions", ComposerAction); registry.registerComponent("task-create-input-actions", ComposerAction); registry.registerComponent("new-session-input-actions", ComposerAction); registry.registerComponent("task-card-indicators", CardIndicator); registry.registerComponent("task-card-tags", CardTags); registry.registerComponent("task-row-metadata", RowMetadata); registry.registerComponent("sidebar-workspace-actions", WorkspaceActionsSlot); registry.registerTaskMenuAction({ id: "enhance-notes", label: "Enhance notes", group: "edit", run: function (context) { return host.storage .set("task", context.taskId, "note", "Enhanced via plugin action") .then(function () { // Keep the received responsive context observable to the E2E // fixture without changing the plugin-facing action contract. return host.storage.set( "task", context.taskId, "menu-presentation", context.presentation, ); }); }, }); registry.registerTaskMenuAction({ id: "inspect-task-metadata", label: "Inspect task metadata", group: "primary", run: function (context) { return host.storage.set( "task", context.taskId, "primary-menu-presentation", context.presentation, ); }, }); registry.registerTaskMenuAction({ id: "task-shortcuts", label: "Task shortcuts", group: "primary", items: function () { return [ { id: "record-presentation", label: "Record menu presentation", run: function (childContext) { return host.storage.set( "task", childContext.taskId, "primary-submenu-presentation", childContext.presentation, ); }, }, ]; }, run: function (context) { return host.storage.set( "task", context.taskId, "primary-submenu-presentation", context.presentation, ); }, }); registry.registerKeybinding("open-demo", function () { function DemoModalContent() { var completedState = React.useState(false); var completed = completedState[0]; var setCompleted = completedState[1]; var longRows = []; for (var index = 0; index < 32; index += 1) { longRows.push( jsx( "p", { key: "long-modal-row-" + index }, "Long plugin modal content row " + String(index + 1) + " keeps the opaque plugin surface growing beyond the viewport.", ), ); } // The Tooltip is the point of this modal in e2e: PluginModalHost // mounts outside AppShell, so without its own TooltipProvider a // Tooltip here throws on render and the error boundary swallows the // whole modal. jsdom cannot open a Radix tooltip from synthetic // hover, so real pointer hover is only assertable here. return jsx( "div", { id: "hello-demo-modal", "data-testid": "hello-demo-modal", style: { display: "grid", gap: "8px" }, }, "Hello from the plugin modal", jsx( ui.Tooltip, null, jsx(ui.TooltipTrigger, { "data-testid": "hello-modal-tooltip-trigger" }, "hover me"), jsx(ui.TooltipContent, null, "Tooltip inside a plugin modal"), ), jsx("div", { "data-testid": "hello-long-modal-content" }, longRows), jsx( "button", { type: "button", "data-testid": "hello-long-modal-final-action", style: { minHeight: "44px", padding: "8px 12px" }, onClick: function () { setCompleted(true); }, }, completed ? "Plugin modal action complete" : "Complete plugin modal action", ), ); } host.openModal({ title: "Demo Modal", content: DemoModalContent, size: "md", }); }); }, }); })();