Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions desktop/src/features/agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,16 @@ with a TypeScript lookup table or an id comparison in a component.
Once the Advanced toggle is visible, its expanded state is exclusively
user-controlled: provider, harness, and required-env changes must never
open it automatically in defaults, create, or edit flows. In Create mode,
the defaults summary follows preferred-harness changes saved while the
dialog is open, and its configured state includes required credentials as
well as provider/model values. If no available harness can resolve, Create
starts in Customize and lets unavailable catalog entries be selected only
to expose their setup guidance; submission remains blocked.
Advanced-only required credentials mark the collapsed Advanced toggle
without opening it in Global Defaults and Edit, and block incomplete saves.
`Run on` belongs in Advanced directly after **Who can send instructions**;
keep it out of the basic create fields. The defaults summary follows
preferred-harness changes saved while the dialog is open, and its configured
state includes required credentials as well as provider/model values. If no
available harness can resolve, Create starts in Customize and lets unavailable
catalog entries be selected only to expose their setup guidance; submission
remains blocked.
Advanced-only required credentials and incomplete remote **Run on** setup
mark the collapsed Advanced toggle without opening it, and block incomplete
saves.
Runtime-file credentials satisfy Global Defaults just as they do Create and
Edit. In Edit,
selecting Custom command keeps its required command field beside the harness
Expand Down
10 changes: 5 additions & 5 deletions desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ type AgentDefinitionDialogProps = {
) => Promise<unknown>;
/** Publishes saved changes when the edited agent is shared in the catalog. */
publishCatalogUpdatesOnSave?: boolean;
/** Rendered below the form fields in create mode only ("Where to run"). */
createRunSection?: React.ReactNode;
/** Extra create-mode submit gate (e.g. incomplete provider config). */
createSubmitBlocked?: boolean;
Expand Down Expand Up @@ -962,9 +961,6 @@ export function AgentDefinitionDialog({
onSaved={selectSavedHarness}
open={isAddHarnessOpen}
/>

{isCreateMode ? createRunSection : null}

<div className="space-y-3">
<button
aria-expanded={showAdvancedFields}
Expand All @@ -973,7 +969,8 @@ export function AgentDefinitionDialog({
type="button"
>
<span>Advanced</span>
{localModeGate.missingEnvKeys.some((key) =>
{(isCreateMode && createSubmitBlocked) ||
localModeGate.missingEnvKeys.some((key) =>
advancedRequiredEnvKeys.includes(key),
) ? (
<span
Expand Down Expand Up @@ -1002,6 +999,9 @@ export function AgentDefinitionDialog({
transition={advancedFieldsTransition}
>
<PersonaAdvancedFields
afterRespondTo={
isCreateMode ? createRunSection : undefined
}
Comment thread
klopez4212 marked this conversation as resolved.
Comment thread
klopez4212 marked this conversation as resolved.
Comment thread
klopez4212 marked this conversation as resolved.
behaviorDraft={behaviorDraft}
disabled={isPending}
envVars={envVars}
Expand Down
5 changes: 5 additions & 0 deletions desktop/src/features/agents/ui/PersonaAdvancedFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function PersonaAdvancedFields({
behaviorDraft,
disabled,
envVars,
afterRespondTo,
inheritedEnvVars = {},
model,
modelTuningRuntimeId = "",
Expand All @@ -55,6 +56,8 @@ export function PersonaAdvancedFields({
behaviorDraft: PersonaBehaviorDraft;
disabled: boolean;
envVars: EnvVarsValue;
/** Optional create-only field rendered after instruction permissions. */
afterRespondTo?: React.ReactNode;
/** Env vars to display as inherited defaults in tuning-field placeholders.
* For templates, pass `globalConfig.env_vars` (the fallback layer). */
inheritedEnvVars?: EnvVarsValue;
Expand Down Expand Up @@ -153,6 +156,8 @@ export function PersonaAdvancedFields({
variant="persona"
/>

{afterRespondTo}

<div className="grid gap-5 sm:grid-cols-2">
<div className="space-y-1.5">
<label
Expand Down
33 changes: 19 additions & 14 deletions desktop/src/features/agents/ui/WhereToRunSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useBackendProvidersQuery } from "@/features/agents/hooks";
import { probeBackendProvider } from "@/shared/api/tauri";

import { ProviderConfigFields } from "./ProviderConfigFields";
import { PersonaDropdownField } from "./PersonaDropdownField";
import {
applyProbeResult,
emptyWhereToRunDraft,
Expand All @@ -23,6 +24,16 @@ export function WhereToRunSection({
}) {
const backendProviders = useBackendProvidersQuery().data ?? [];
const [probeError, setProbeError] = React.useState<string | null>(null);
const runOnOptions = React.useMemo(
() => [
{ label: "This computer", value: "local" },
...backendProviders.map((provider) => ({
label: provider.id,
value: provider.id,
})),
],
[backendProviders],
);
const isProviderMode = draft.runOn !== "local";
const selectedBackendProvider = React.useMemo(
() =>
Expand Down Expand Up @@ -51,7 +62,7 @@ export function WhereToRunSection({
? (selectedBackendProvider?.binaryPath ?? null)
: null;
React.useEffect(() => {
if (!selectedBinaryPath) {
if (!selectedBinaryPath || draft.probedProvider) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve in-flight probes across Advanced toggles

When a user collapses Advanced before the provider probe resolves and immediately reopens it, unmount cleanup only marks the local callback cancelled; it cannot cancel the Tauri probe_backend_provider invocation, and draft.probedProvider is still null, so remounting starts a second provider process that can also run for up to 10 seconds. Fresh evidence relative to the earlier thread is that the new slow-probe test stops after collapsing and never reopens Advanced, so the completed-probe cache does not cover this remaining in-flight path. Keep the in-flight probe owner or cache outside the unmounted disclosure.

Useful? React with 👍 / 👎.

setProbeError(null);
return;
}
Expand All @@ -70,7 +81,7 @@ export function WhereToRunSection({
return () => {
cancelled = true;
};
}, [selectedBinaryPath]);
}, [selectedBinaryPath, draft.probedProvider]);

if (backendProviders.length === 0) return null;

Expand All @@ -80,25 +91,19 @@ export function WhereToRunSection({
<label className="text-sm font-medium" htmlFor="agent-run-on">
Run on
</label>
<select
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs"
<PersonaDropdownField
disabled={isPending}
id="agent-run-on"
onChange={(event) =>
onValueChange={(runOn) =>
onDraftChange({
...emptyWhereToRunDraft,
runOn: event.target.value,
runOn,
})
}
options={runOnOptions}
placeholder="Choose where to run"
value={draft.runOn}
>
<option value="local">This computer</option>
{backendProviders.map((provider) => (
<option key={provider.id} value={provider.id}>
{provider.id}
</option>
))}
</select>
/>
</div>

{isProviderMode && selectedBackendProvider ? (
Expand Down
18 changes: 9 additions & 9 deletions desktop/src/features/channels/ui/ChannelPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { BotActivityComposerAction } from "@/features/channels/ui/BotActivityBar
import { ChannelComposerActivityAccessory } from "@/features/channels/ui/ChannelComposerActivityAccessory";
import {
containsWelcomePersonaMention,
WelcomeComposerBanner,
WelcomeComposerGuidanceLayer,
WELCOME_COMPOSER_BANNER_DISMISS_DURATION_SECONDS,
WELCOME_COMPOSER_BANNER_HIDE_BUFFER_MS,
WELCOME_COMPOSER_BANNER_SUCCESS_SETTLE_MS,
Expand Down Expand Up @@ -737,18 +737,18 @@ export const ChannelPane = React.memo(function ChannelPane({
hasComposerBottomActivity && "composer-dock--with-activity",
)}
>
{isActiveWelcomeChannel && !timeoutState.active ? (
<WelcomeComposerGuidanceLayer
settingUp={welcomeKickoffSettingUp}
state={welcomeComposerBannerState}
>
{welcomeKickoffStage}
</WelcomeComposerGuidanceLayer>
) : null}
{timeoutState.active ? (
<ComposerTimeoutBanner
expiresAtMs={timeoutState.expiresAtMs}
/>
) : isActiveWelcomeChannel ? (
<div className="relative">
{welcomeKickoffStage}
<WelcomeComposerBanner
settingUp={welcomeKickoffSettingUp}
state={welcomeComposerBannerState}
/>
</div>
) : null}
<ComposerDockBackdrop gutterClassName="inset-x-5" />
<MessageComposer
Expand Down
27 changes: 27 additions & 0 deletions desktop/src/features/channels/ui/WelcomeComposerBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { Bot, Check } from "lucide-react";

import { ComposerDockGlassBackdrop } from "@/features/messages/ui/ComposerDockBackdrop";
import { cn } from "@/shared/lib/cn";

const WELCOME_PERSONA_NAMES = ["Fizz"] as const;
Expand Down Expand Up @@ -414,3 +415,29 @@ export function WelcomeComposerBanner({
</AnimatePresence>
);
}

type WelcomeComposerGuidanceLayerProps = WelcomeComposerBannerProps & {
children: React.ReactNode;
};

export function WelcomeComposerGuidanceLayer({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document the exported guidance layer

WelcomeComposerGuidanceLayer is a newly exported component consumed across feature boundaries by ChannelPane, but it has no doc comment describing its absolute-positioning and layering contract. Add documentation for this public API as required by the repository rules.

AGENTS.md reference: AGENTS.md:L113-L116

Useful? React with 👍 / 👎.

children,
settingUp,
state,
}: WelcomeComposerGuidanceLayerProps) {
return (
<div
className="absolute inset-x-0 bottom-full z-[-1]"
Comment thread
klopez4212 marked this conversation as resolved.
data-testid="welcome-composer-guidance-layer"
>
<div className="relative">
<ComposerDockGlassBackdrop
className="absolute inset-x-5 top-0 bottom-3 z-0 rounded-t-2xl"
testId="welcome-composer-guidance-backdrop"
/>
{children}
<WelcomeComposerBanner settingUp={settingUp} state={state} />
</div>
</div>
);
}
27 changes: 26 additions & 1 deletion desktop/src/features/messages/ui/ComposerDockBackdrop.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
import { cn } from "@/shared/lib/cn";

type ComposerDockGlassBackdropProps = {
className?: string;
testId?: string;
};

/**
* Applies the composer dock's shared blur without adding color or layout.
* Reuse it for composer-adjacent surfaces that need the same glass treatment.
*/
export function ComposerDockGlassBackdrop({
Comment thread
klopez4212 marked this conversation as resolved.
className,
testId,
}: ComposerDockGlassBackdropProps) {
return (
<div
aria-hidden="true"
className={cn(
"pointer-events-none backdrop-blur-md dark:backdrop-blur-xl",
className,
)}
data-testid={testId}
/>
);
}

type ComposerDockBackdropProps = {
gutterClassName: string;
};
Expand All @@ -21,7 +46,7 @@ export function ComposerDockBackdrop({
)}
data-testid="composer-dock-backdrop"
>
<div className="h-full w-full rounded-2xl backdrop-blur-md dark:backdrop-blur-xl" />
<ComposerDockGlassBackdrop className="h-full w-full rounded-2xl" />
</div>
<div
aria-hidden="true"
Expand Down
75 changes: 74 additions & 1 deletion desktop/tests/e2e/onboarding.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,23 @@ async function expectWelcomeComposerBannerLayout(page: Page) {
const bannerBox = await banner.boundingBox();
const personaMentionBox = await personaMention.boundingBox();
const composerBox = await composer.boundingBox();
const dockBackdropBox = await page
.getByTestId("composer-dock-backdrop")
.locator("div")
.boundingBox();
const guidanceLayer = page.getByTestId("welcome-composer-guidance-layer");
const guidanceBackdrop = page.getByTestId(
"welcome-composer-guidance-backdrop",
);
const guidanceBackdropBox = await guidanceBackdrop.boundingBox();

if (!bannerBox || !personaMentionBox || !composerBox) {
if (
!bannerBox ||
!personaMentionBox ||
!composerBox ||
!dockBackdropBox ||
!guidanceBackdropBox
) {
throw new Error("Could not measure welcome composer banner layout");
}

Expand All @@ -212,11 +227,40 @@ async function expectWelcomeComposerBannerLayout(page: Page) {
).toBe(0);
expect(bannerBox.y).toBeLessThan(composerBox.y);
expect(bannerBox.y + bannerBox.height).toBeGreaterThan(composerBox.y);
expect(Math.abs(dockBackdropBox.y - composerBox.y)).toBeLessThanOrEqual(1);
expect(guidanceBackdropBox.y).toBeLessThanOrEqual(bannerBox.y);
expect(
Math.abs(
guidanceBackdropBox.y + guidanceBackdropBox.height - composerBox.y,
),
).toBeLessThanOrEqual(1);
const [guidanceZIndex, backdropZIndex] = await Promise.all([
guidanceLayer.evaluate((element) =>
Number(window.getComputedStyle(element).zIndex),
),
page
.getByTestId("composer-dock-backdrop")
.evaluate((element) => Number(window.getComputedStyle(element).zIndex)),
]);
expect(guidanceZIndex).toBeLessThan(backdropZIndex);
expect(
await page
.getByTestId("channel-composer-overlay")
.getByTestId("welcome-composer-guidance-layer")
.count(),
).toBe(1);
expect(
await page
.getByTestId("composer-dock-backdrop")
.getByTestId("welcome-composer-guidance-layer")
.count(),
).toBe(0);

const radii = await banner.evaluate((element) => {
const styles = window.getComputedStyle(element);
return {
backdropFilter: styles.backdropFilter,
backgroundColor: styles.backgroundColor,
bottomLeft: styles.borderBottomLeftRadius,
bottomRight: styles.borderBottomRightRadius,
filter: styles.filter,
Expand All @@ -227,11 +271,24 @@ async function expectWelcomeComposerBannerLayout(page: Page) {
willChange: styles.willChange,
};
});
const composerBackgroundColor = await composer.evaluate(
(element) => window.getComputedStyle(element).backgroundColor,
);
const dockBackdropFilter = await page
.getByTestId("composer-dock-backdrop")
.locator("div")
.evaluate((element) => window.getComputedStyle(element).backdropFilter);
const guidanceBackdropFilter = await guidanceBackdrop.evaluate(
(element) => window.getComputedStyle(element).backdropFilter,
);

expect(radii.topLeft).toBe(radii.topRight);
expect(radii.bottomLeft).toBe("0px");
expect(radii.bottomRight).toBe("0px");
expect(radii.backdropFilter).toBe("none");
expect(radii.backgroundColor).not.toBe(composerBackgroundColor);
expect(dockBackdropFilter).not.toBe("none");
expect(guidanceBackdropFilter).toBe(dockBackdropFilter);
expect(radii.filter).toBe("none");
expect(radii.transform).toBe("none");
expect(radii.willChange).toBe("auto");
Expand Down Expand Up @@ -385,6 +442,11 @@ async function expectWelcomeComposerBannerCompletesAfterPersonaMention(
) {
const banner = page.getByTestId("welcome-composer-guide-banner");
const channelIntro = page.getByTestId("message-channel-intro");
const composer = page.getByTestId("message-composer");
const initialComposerBox = await composer.boundingBox();
if (!initialComposerBox) {
throw new Error("Could not measure the Welcome composer");
}

await page.getByTestId("message-input").fill("Thanks @Fizz");
await page.getByTestId("send-message").click();
Expand All @@ -403,6 +465,17 @@ async function expectWelcomeComposerBannerCompletesAfterPersonaMention(
await expect(banner).toContainText("Nice work.");
await expect(banner).not.toContainText("Try mentioning");
await expect(channelIntro).toBeVisible();
const completeComposerBox = await composer.boundingBox();
expect(completeComposerBox).not.toBeNull();
expect(
Math.abs((completeComposerBox?.y ?? 0) - initialComposerBox.y),
).toBeLessThanOrEqual(1);
await expect(banner).toHaveCount(0, { timeout: 6_000 });
const hiddenComposerBox = await composer.boundingBox();
expect(hiddenComposerBox).not.toBeNull();
expect(
Math.abs((hiddenComposerBox?.y ?? 0) - initialComposerBox.y),
).toBeLessThanOrEqual(1);
}

async function getMockChannels(page: Page) {
Expand Down
Loading