Skip to content
Merged
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
21 changes: 17 additions & 4 deletions apps/web/src/components/Settings/Common/SettingRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import React from 'react';

import { cn } from '@/components/Common/cn';

interface SettingRowProps {
/** Primary label for the setting. Omit when the section heading already names it. */
title?: React.ReactNode;
Expand All @@ -18,12 +20,15 @@ interface SettingRowProps {
className?: string;
/** Reduces vertical padding for subordinate settings. */
density?: 'default' | 'compact';
/** Places controls beside the label or in a full-width row below it. */
layout?: 'inline' | 'stacked';
}

/**
* A single setting row inside a {@link SettingSection} card. Renders the
* title (and optional description) on the left and a control on the right.
* The row itself is borderless — dividers come from the parent section.
* title (and optional description) with its controls. Inline rows place the
* control on the right; stacked rows place it full-width below the text. The
* row itself is borderless — dividers come from the parent section.
*/
export const SettingRow: React.FC<SettingRowProps> = ({
title,
Expand All @@ -33,10 +38,18 @@ export const SettingRow: React.FC<SettingRowProps> = ({
children,
className = '',
density = 'default',
layout = 'inline',
}) => {
const stacked = layout === 'stacked';

return (
<div
className={`flex items-center justify-between gap-3 px-3 ${density === 'compact' ? 'py-1.5' : 'py-2.5'} ${className}`.trim()}
className={cn(
'flex gap-3 px-3',
stacked ? 'flex-col items-stretch' : 'items-center justify-between',
density === 'compact' ? 'py-1.5' : 'py-2.5',
className,
)}
>
<div className="flex min-w-0 flex-1 items-center gap-2">
{leading && <div className="shrink-0">{leading}</div>}
Expand All @@ -59,7 +72,7 @@ export const SettingRow: React.FC<SettingRowProps> = ({
)}
</div>
</div>
<div className="shrink-0">{children}</div>
<div className={stacked ? 'w-full min-w-0' : 'shrink-0'}>{children}</div>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,35 @@ afterEach(() => {
});

describe('ExternalAgentRuntimeSettings', () => {
it('places responsive token controls below the full-width description', async () => {
await act(async () => {
root.render(<ExternalAgentRuntimeSettings />);
});

const tokenInput = container.querySelector<HTMLInputElement>(
'#agentlet-connection-token',
);
if (!tokenInput) throw new Error('Connection token input not found');

const controls = tokenInput.parentElement;
const controlRow = controls?.parentElement;
const settingRow = controlRow?.parentElement;
expect(settingRow?.classList.contains('flex-col')).toBe(true);
expect(settingRow?.classList.contains('items-stretch')).toBe(true);
expect(controlRow?.classList.contains('w-full')).toBe(true);
expect(controls?.classList.contains('flex-wrap')).toBe(true);
expect(controls?.classList.contains('w-full')).toBe(true);
expect(tokenInput.classList.contains('flex-1')).toBe(true);
expect(tokenInput.classList.contains('basis-56')).toBe(true);

const buttons = controls?.querySelectorAll('button') ?? [];
expect(buttons.length).toBeGreaterThan(0);
for (const button of buttons) {
expect(button.classList.contains('shrink-0')).toBe(true);
expect(button.classList.contains('whitespace-nowrap')).toBe(true);
}
});

it('preserves the process limit when saving the idle timeout', async () => {
await act(async () => {
root.render(<ExternalAgentRuntimeSettings />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ export function ExternalAgentRuntimeSettings() {
return (
<>
<SettingRow
layout="stacked"
title={
<span className="flex items-center gap-1">
{t('settings.agentletConnectionToken')}
Expand All @@ -265,10 +266,10 @@ export function ExternalAgentRuntimeSettings() {
: t('settings.agentletConnectionTokenDescriptionLoading')
}
>
<div className="flex max-w-lg flex-wrap items-center justify-end gap-2">
<div className="flex w-full flex-wrap items-center gap-2">
<TextInput
id="agentlet-connection-token"
className="w-56"
className="min-w-40 flex-1 basis-56"
type="password"
value={tokenInput}
onChange={(event) => setTokenInput(event.target.value)}
Expand All @@ -287,6 +288,7 @@ export function ExternalAgentRuntimeSettings() {
variant="outline"
tone="info"
size="sm"
className="shrink-0 whitespace-nowrap"
onClick={() => void saveConnectionToken()}
disabled={
!tokenInput.trim() ||
Expand All @@ -301,6 +303,7 @@ export function ExternalAgentRuntimeSettings() {
<Button
variant="outline"
size="sm"
className="shrink-0 whitespace-nowrap"
onClick={() => void clearConnectionToken()}
disabled={tokenSaving || !tokenConfig.writable}
>
Expand All @@ -311,6 +314,7 @@ export function ExternalAgentRuntimeSettings() {
variant="outline"
tone="info"
size="sm"
className="shrink-0 whitespace-nowrap"
onClick={() => void copyConnectionCommand()}
disabled={tokenLoading || copyingCommand || !tokenConfig}
>
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/web-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ Space Shortcut retains the `spacePreview` node type and renders the canonical ic

### Settings information architecture

The tabbed Settings modal has three product-owned surfaces. **Agent** presents ordinary external Profile management first, then the Utility Agent used only for Huabu summaries, titles, labels, keywords, and related auxiliary work, conditional Built-In Pi provider/model setup, Agent behavior, and external-Agent runtime controls while preserving each component's existing store and API ownership; the Utility Agent explanation is contained in its Profile row. New conversations independently use the browser-local recent conversational Agent, falling back to the first selectable external Profile when that cache is absent or stale. **Capabilities** presents Huabu-managed image generation, Web Search, YouTube transcript, and Ink OCR configuration as peer cards with uniform spacing and without an extra “Other Capabilities” grouping; the larger Image Generation form starts collapsed behind an in-card header, and the surface copy does not imply external-Agent tool parity. **General** contains application, Canvas, input, update, and deployment preferences that do not configure an Agent or service capability. The hidden Built-In repair deep link remains focused on Pi provider/model setup and returns to the Agent surface after closing.
The tabbed Settings modal has three product-owned surfaces. **Agent** presents ordinary external Profile management first, then the Utility Agent used only for Huabu summaries, titles, labels, keywords, and related auxiliary work, conditional Built-In Pi provider/model setup, Agent behavior, and external-Agent runtime controls while preserving each component's existing store and API ownership; the Utility Agent explanation is contained in its Profile row. New conversations independently use the browser-local recent conversational Agent, falling back to the first selectable external Profile when that cache is absent or stale. **Capabilities** presents Huabu-managed image generation, Web Search, YouTube transcript, and Ink OCR configuration as peer cards with uniform spacing and without an extra “Other Capabilities” grouping; the larger Image Generation form starts collapsed behind an in-card header, and the surface copy does not imply external-Agent tool parity. **General** contains application, Canvas, input, update, and deployment preferences that do not configure an Agent or service capability. The hidden Built-In repair deep link remains focused on Pi provider/model setup and returns to the Agent surface after closing. Shared `SettingRow` entries use the inline label-and-control layout by default and opt into the stacked layout when a full-width description must remain independent from a responsive control group below it. The Agentlet connection-token entry uses that stacked contract; its input grows into available space and its content-sized actions wrap only at control boundaries.

Agent Profile management uses `components/Settings/agent-profiles/` and the canonical `acpProfilesStore`. It has ordinary command Profile creation/editing/deletion and no Agent Team templates, Configs or setup lifecycle. The manual editor reads the agentlet-backed catalogue; automatic defaults arrive through the same persisted Profile list used by selectors. Opening an editor focuses the nested Agent view while retaining the existing Profile revision and save contracts. See [Agent Profiles](./agent-profiles.md).

Expand Down
Loading