Skip to content

Commit 0fab97f

Browse files
committed
feat(ui): Title and Owns are chips in the composer, not an Options panel
Options opened a boxed panel under the prompt: two labelled fields and a paragraph of help, heavier than the choices it held. Title and Owns are now chips in the composer bar, next to the agent picker, edited in place: click one to type, and Enter, Escape, or leaving it turns it back into a chip that shows what was set ("Title Fix the parser", "Owns 2 paths"). Enter finishes the field without launching, Escape does not close the New agent dialog, and focus returns to the prompt. Owns stays a one-row text area, so a pasted list with one path per line still parses. The help text is the chip's tooltip and the field's description. The Options icon had no other use and is gone.
1 parent 928b5e3 commit 0fab97f

4 files changed

Lines changed: 158 additions & 87 deletions

File tree

‎src/components/workbench/NewAgentForm.test.tsx‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ function chooseAgent(name: "Claude Code" | "Codex") {
3131
expect(screen.getByRole("menuitem", { name: /^Agent\b/ }).textContent).toContain(name);
3232
}
3333

34-
/** Title and owned paths sit behind Options; open it on first use. */
34+
/** Title and owned paths are chips in the composer bar; click one to type in it. */
3535
function titleField() {
36-
if (!screen.queryByLabelText("Title")) fireEvent.click(screen.getByRole("button", { name: "Options" }));
36+
if (!screen.queryByLabelText("Title")) fireEvent.click(screen.getByRole("button", { name: /^Title/ }));
3737
return screen.getByLabelText("Title");
3838
}
3939

4040
function ownsField() {
41-
if (!screen.queryByLabelText("Owns (files or folders)")) fireEvent.click(screen.getByRole("button", { name: "Options" }));
41+
if (!screen.queryByLabelText("Owns (files or folders)")) fireEvent.click(screen.getByRole("button", { name: /^Owns/ }));
4242
return screen.getByLabelText("Owns (files or folders)");
4343
}
4444

@@ -266,4 +266,19 @@ describe("NewAgentForm", () => {
266266
fireEvent.keyDown(prompt, { key: "Enter" });
267267
await waitFor(() => expect(createTask).toHaveBeenCalledWith(expect.objectContaining({ prompt: "fix it" })));
268268
});
269+
270+
it("edits Title and Owns in place as chips, then shows what was set", () => {
271+
renderForm();
272+
fireEvent.change(titleField(), { target: { value: "Fix parser" } });
273+
// Enter finishes the field without launching, and the prompt gets focus back.
274+
fireEvent.keyDown(screen.getByLabelText("Title"), { key: "Enter" });
275+
expect(screen.queryByLabelText("Title")).toBeNull();
276+
expect(document.activeElement).toBe(screen.getByLabelText("Prompt"));
277+
expect(screen.getByRole("button", { name: "Title Fix parser" })).toBeTruthy();
278+
279+
fireEvent.change(ownsField(), { target: { value: "src/parser/, docs/SCHEMA.md" } });
280+
fireEvent.keyDown(screen.getByLabelText("Owns (files or folders)"), { key: "Escape" });
281+
expect(screen.getByRole("button", { name: "Owns 2 paths" })).toBeTruthy();
282+
expect(createTask).not.toHaveBeenCalled();
283+
});
269284
});

‎src/components/workbench/NewAgentForm.tsx‎

Lines changed: 104 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef, useState, type FormEvent } from "react";
1+
import { useEffect, useId, useRef, useState, type ChangeEvent, type FormEvent, type KeyboardEvent } from "react";
22

33
import {
44
createTask,
@@ -9,7 +9,6 @@ import {
99
type TaskPermission,
1010
} from "../../ipc";
1111
import AgentMenu from "./AgentMenu";
12-
import { SlidersIcon } from "./icons";
1312
import { deriveTitle, errorText, parseClaims } from "./state";
1413

1514
type Props = {
@@ -46,7 +45,6 @@ export default function NewAgentForm({
4645
const [autoHandoff, setAutoHandoff] = useState(true);
4746
const [busy, setBusy] = useState(false);
4847
const [error, setError] = useState<string | null>(null);
49-
const [options, setOptions] = useState(false);
5048
const [split, setSplit] = useState(false);
5149
const promptRef = useRef<HTMLTextAreaElement>(null);
5250

@@ -148,36 +146,6 @@ export default function NewAgentForm({
148146
}
149147
}}
150148
/>
151-
{options && (
152-
<div className="new-agent__options">
153-
<label className="new-agent__field">
154-
<span>Title</span>
155-
<input
156-
className="new-agent__name"
157-
type="text"
158-
aria-label="Title"
159-
placeholder={autoTitle || "From the prompt's first line"}
160-
value={title}
161-
onChange={(e) => setTitle(e.target.value)}
162-
/>
163-
</label>
164-
<label className="new-agent__field">
165-
<span>Owns</span>
166-
<textarea
167-
rows={1}
168-
className="new-agent__claims"
169-
aria-label="Owns (files or folders)"
170-
placeholder="e.g. src/parser/, docs/SCHEMA.md"
171-
value={claimsText}
172-
onChange={(e) => setClaimsText(e.target.value)}
173-
/>
174-
</label>
175-
<p className="new-agent__hint">
176-
Other agents are told not to touch owned paths, and a commit that changes another agent&rsquo;s files
177-
needs confirmation. End a folder with <span className="mono">/</span>.
178-
</p>
179-
</div>
180-
)}
181149
{claims.length > 0 && (
182150
<ul className="claim-chips" aria-label="Owned paths">
183151
{claims.map((claim) => (
@@ -207,15 +175,26 @@ export default function NewAgentForm({
207175
onEffortChange={setEffort}
208176
disabled={disabled}
209177
/>
210-
<button
211-
type="button"
212-
className={`composer__tool${options || claims.length > 0 || title ? " composer__tool--on" : ""}`}
213-
aria-expanded={options}
214-
onClick={() => setOptions((v) => !v)}
215-
>
216-
<SlidersIcon />
217-
Options
218-
</button>
178+
<ChipField
179+
label="Title"
180+
inputLabel="Title"
181+
value={title}
182+
onChange={setTitle}
183+
placeholder={autoTitle || "From the prompt's first line"}
184+
hint="Optional. Defaults to the prompt's first line."
185+
onDone={() => promptRef.current?.focus()}
186+
/>
187+
<ChipField
188+
label="Owns"
189+
inputLabel="Owns (files or folders)"
190+
value={claimsText}
191+
onChange={setClaimsText}
192+
placeholder="src/parser/, docs/SCHEMA.md"
193+
summary={claims.length === 0 ? "" : claims.length === 1 ? claims[0] : `${claims.length} paths`}
194+
multiline
195+
hint="Other agents are told not to touch owned paths, and a commit that changes another agent's files needs confirmation. End a folder with /."
196+
onDone={() => promptRef.current?.focus()}
197+
/>
219198
{allowSplit && (
220199
<button
221200
type="button"
@@ -244,3 +223,86 @@ export default function NewAgentForm({
244223
</form>
245224
);
246225
}
226+
227+
/**
228+
* An optional setting as a quiet chip in the composer bar, edited in place:
229+
* click to type, and Enter, Escape, or leaving the field turns it back into a
230+
* chip that shows the value.
231+
*/
232+
function ChipField({
233+
label,
234+
inputLabel,
235+
value,
236+
onChange,
237+
placeholder,
238+
summary,
239+
hint,
240+
onDone,
241+
multiline = false,
242+
}: {
243+
label: string;
244+
/** The input's accessible name. */
245+
inputLabel: string;
246+
value: string;
247+
onChange: (value: string) => void;
248+
placeholder: string;
249+
/** The value as the chip shows it; defaults to the trimmed value. */
250+
summary?: string;
251+
/** What the setting does: the chip's tooltip and the input's description. */
252+
hint: string;
253+
/** Editing ended from the keyboard; the form puts focus back in the prompt. */
254+
onDone: () => void;
255+
/** Keep pasted lines (a one-row textarea; Shift+Enter adds a line). */
256+
multiline?: boolean;
257+
}) {
258+
const [editing, setEditing] = useState(false);
259+
const hintId = useId();
260+
const fieldProps = {
261+
className: "chip-field__input",
262+
"aria-label": inputLabel,
263+
"aria-describedby": hintId,
264+
placeholder,
265+
spellCheck: false,
266+
value,
267+
onChange: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => onChange(e.target.value),
268+
onBlur: () => setEditing(false),
269+
onKeyDown: (e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
270+
const finish = (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) || e.key === "Escape";
271+
if (!finish) return;
272+
// Done with the field, not the form: Enter does not launch, Escape does not close a dialog.
273+
e.preventDefault();
274+
e.stopPropagation();
275+
setEditing(false);
276+
onDone();
277+
},
278+
};
279+
if (editing) {
280+
return (
281+
<span className="chip-field">
282+
<span className="chip-field__label" aria-hidden="true">
283+
{label}
284+
</span>
285+
{multiline ? (
286+
<textarea autoFocus rows={1} {...fieldProps} />
287+
) : (
288+
<input autoFocus type="text" {...fieldProps} />
289+
)}
290+
<span id={hintId} className="visually-hidden">
291+
{hint}
292+
</span>
293+
</span>
294+
);
295+
}
296+
const shown = summary ?? value.trim();
297+
return (
298+
<button
299+
type="button"
300+
className={`composer__tool${shown ? " composer__tool--on" : ""}`}
301+
title={hint}
302+
onClick={() => setEditing(true)}
303+
>
304+
{label}
305+
{shown && <span className="chip-field__value">{shown}</span>}
306+
</button>
307+
);
308+
}

‎src/components/workbench/icons.tsx‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,6 @@ export const ToolIcon = () => (
6464
</Icon>
6565
);
6666

67-
/** Options: two sliders. */
68-
export const SlidersIcon = () => (
69-
<Icon size={14}>
70-
<path d="M2.75 5h6.5M12.25 5h1M2.75 11h1M6.75 11h6.5" />
71-
<circle cx="10.75" cy="5" r="1.5" />
72-
<circle cx="5.25" cy="11" r="1.5" />
73-
</Icon>
74-
);
75-
7667
/** More actions: three dots. */
7768
export const MoreIcon = () => (
7869
<Icon>

‎src/styles.css‎

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2149,57 +2149,60 @@ body.is-resizing-horizontal .sash--horizontal::before {
21492149
color: rgba(17, 17, 19, 0.55);
21502150
}
21512151

2152-
.new-agent__options {
2153-
display: grid;
2154-
grid-template-columns: 1fr 1fr;
2155-
gap: 8px 12px;
2156-
margin: 4px 0 6px;
2157-
padding: 10px 0 0;
2158-
border-top: 1px solid var(--line);
2152+
/* Title and Owns: quiet chips in the composer bar that edit in place. */
2153+
.chip-field {
2154+
display: inline-flex;
2155+
flex: 1 1 200px;
2156+
align-items: center;
2157+
gap: 6px;
2158+
min-width: 0;
2159+
max-width: 340px;
2160+
height: 26px;
2161+
padding: 0 8px;
2162+
border: 1px solid var(--line-strong);
2163+
border-radius: var(--r);
2164+
background: var(--bg-app);
21592165
}
21602166

2161-
.new-agent__field {
2162-
display: flex;
2163-
flex-direction: column;
2164-
gap: 4px;
2165-
min-width: 0;
2167+
.chip-field:focus-within {
2168+
border-color: var(--line-focus);
21662169
}
21672170

2168-
.new-agent__field > span {
2171+
.chip-field__label {
2172+
flex: none;
21692173
color: var(--text-3);
2170-
font-size: var(--text-xs);
2171-
font-weight: 500;
2174+
font-size: var(--text-sm);
21722175
}
21732176

2174-
.new-agent__name,
2175-
textarea.new-agent__claims {
2176-
width: 100%;
2177-
height: 30px;
2178-
padding: 5px 8px;
2179-
border: 1px solid var(--line-strong);
2180-
border-radius: var(--r);
2181-
background: var(--bg-app);
2177+
.chip-field__input {
2178+
flex: 1;
2179+
min-width: 0;
2180+
height: 22px;
2181+
padding: 0;
2182+
overflow: hidden;
2183+
border: 0;
2184+
background: transparent;
21822185
color: var(--text-1);
21832186
font-size: var(--text-sm);
2187+
line-height: 22px;
2188+
white-space: nowrap;
21842189
resize: none;
21852190
}
21862191

2187-
.new-agent__name:focus,
2188-
textarea.new-agent__claims:focus {
2192+
.chip-field__input:focus {
21892193
outline: none;
2190-
border-color: var(--accent);
21912194
}
21922195

2193-
.new-agent__name::placeholder,
2194-
textarea.new-agent__claims::placeholder {
2196+
.chip-field__input::placeholder {
21952197
color: var(--text-3);
21962198
}
21972199

2198-
.new-agent__hint {
2199-
grid-column: 1 / -1;
2200-
margin: 0;
2201-
color: var(--text-3);
2202-
font-size: var(--text-xs);
2200+
.chip-field__value {
2201+
max-width: 160px;
2202+
overflow: hidden;
2203+
color: var(--text-1);
2204+
text-overflow: ellipsis;
2205+
white-space: nowrap;
22032206
}
22042207

22052208
.new-agent .claim-chips {

0 commit comments

Comments
 (0)