mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 23:44:41 +00:00
fix(plugins): a retained composer that is not showing takes plugin writes through the draft
On native a retained composer keeps its input mounted under display none, so the ref alone said the input was live and a plugin insertText replaced the stale selection instead of appending to the draft. The input is live only while its retained panel is showing.
This commit is contained in:
+4
-2
@@ -690,8 +690,10 @@ implementations provide it:
|
||||
mounted, checked on session changes and when the last holder lets go. The mounted
|
||||
`AgentControls` plugs its input in with `attachInput` so `insertText` lands at the cursor.
|
||||
Without a live input the model writes the stored prompt with `replaceDraftText`, which also asks
|
||||
a mounted input to show it. A workspace the user left stays mounted, display none, with no
|
||||
input ref: `ComposerInput.isLive()` is how the model tells. Closing a root agent's tab archives
|
||||
a mounted input to show it. A workspace the user left stays mounted, display none. Web
|
||||
drops the input ref there; native keeps the input, with a stale cursor and selection.
|
||||
`ComposerInput.isLive()` is how the model tells: it needs the ref and a retained panel that is
|
||||
showing (`useRetainedPanelActive`). Closing a root agent's tab archives
|
||||
it, so a running agent with no mounted composer at all only exists as a subagent whose tab was
|
||||
closed. The daemon is the authority for `setModel` and `setThinking`. It validates against the
|
||||
catalog of the agent's directory, the one the picker reads, and checks a thinking option against
|
||||
|
||||
@@ -117,6 +117,7 @@ import {
|
||||
} from "@/attachments/service";
|
||||
import { resolveAgentControlsMode } from "@/composer/agent-controls/mode";
|
||||
import { resolveComposerInputMode, type ComposerInputMode } from "@/composer/input-mode";
|
||||
import { useIsInputLive } from "./input/use-input-live";
|
||||
import { resolveActiveSendBehavior } from "./input/state";
|
||||
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
||||
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
|
||||
@@ -1433,13 +1434,14 @@ function ComposerContentImpl({
|
||||
},
|
||||
[onChangeText, textSource],
|
||||
);
|
||||
const isInputLive = useIsInputLive(messageInputRef);
|
||||
const isComposerLockedRef = useRef(isComposerLocked);
|
||||
isComposerLockedRef.current = isComposerLocked;
|
||||
const composerInput = useMemo<ComposerInput>(
|
||||
() => ({
|
||||
locked: isComposerLocked,
|
||||
isLocked: () => isComposerLockedRef.current,
|
||||
isLive: () => messageInputRef.current !== null,
|
||||
isLive: isInputLive,
|
||||
textSource,
|
||||
setText: replaceUserInput,
|
||||
insertText: insertUserInput,
|
||||
@@ -1450,6 +1452,7 @@ function ComposerContentImpl({
|
||||
attachments,
|
||||
insertUserInput,
|
||||
isComposerLocked,
|
||||
isInputLive,
|
||||
replaceUserInput,
|
||||
setSelectedAttachments,
|
||||
textSource,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { RetainedPanelActivity } from "@/components/retained-panel";
|
||||
import { useIsInputLive } from "./use-input-live";
|
||||
|
||||
// App sources compile against the classic JSX runtime, which expects React on the global.
|
||||
beforeEach(() => vi.stubGlobal("React", React));
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
function render(
|
||||
active: boolean,
|
||||
input: { current: unknown },
|
||||
publish: (isLive: () => boolean) => void,
|
||||
) {
|
||||
function Probe() {
|
||||
publish(useIsInputLive(input));
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<RetainedPanelActivity active={active}>
|
||||
<Probe />
|
||||
</RetainedPanelActivity>
|
||||
);
|
||||
}
|
||||
|
||||
it("an input kept mounted in a panel that is not showing is not live", () => {
|
||||
const input: { current: object | null } = { current: {} };
|
||||
let isLive: () => boolean = () => false;
|
||||
const publish = (read: () => boolean) => {
|
||||
isLive = read;
|
||||
};
|
||||
const root = createRoot(document.createElement("div"));
|
||||
|
||||
act(() => root.render(render(true, input, publish)));
|
||||
expect(isLive()).toBe(true);
|
||||
|
||||
act(() => root.render(render(false, input, publish)));
|
||||
expect(isLive()).toBe(false);
|
||||
|
||||
act(() => root.render(render(true, input, publish)));
|
||||
expect(isLive()).toBe(true);
|
||||
|
||||
input.current = null;
|
||||
expect(isLive()).toBe(false);
|
||||
act(() => root.unmount());
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useRetainedPanelActive } from "@/components/retained-panel";
|
||||
|
||||
/**
|
||||
* Whether the composer's input is on screen, as a read a saved plugin command can make later. A
|
||||
* retained composer keeps its input mounted and hidden, and its cursor and selection are then
|
||||
* stale, so only an input in a panel that is showing counts.
|
||||
*/
|
||||
export function useIsInputLive(input: { readonly current: unknown }): () => boolean {
|
||||
const panelActive = useRetainedPanelActive();
|
||||
const panelActiveRef = useRef(panelActive);
|
||||
panelActiveRef.current = panelActive;
|
||||
return useCallback(() => input.current !== null && panelActiveRef.current, [input]);
|
||||
}
|
||||
Reference in New Issue
Block a user