Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
d07437f
A typed project name and Enter pick that project
nitedani-agent Nov 10, 2026
3ea8574
The person's picture at the sidebar's foot is a visible circle
nitedani-agent Nov 10, 2026
d976ef3
The Needs you tile counts each kind that waits
nitedani-agent Nov 10, 2026
2c44a11
The Skills card says it counts each project's own skills
nitedani-agent Nov 10, 2026
43ee4ef
The first steps' greeting keeps one line on a phone
nitedani-agent Nov 10, 2026
585630e
The tickets step of the first steps reads as words, its folder as code
nitedani-agent Nov 10, 2026
3c392a4
The Overview's header comment says empty cards say so
nitedani-agent Nov 10, 2026
dea7c0b
An Overview card fades only once its fresh read has been out for a se…
nitedani-agent Nov 10, 2026
a7eff18
In Customize, a card's bar sits above the card, covering none of it
nitedani-agent Nov 10, 2026
a3508a9
The trust question gives focus back to the project picker when it closes
nitedani-agent Nov 10, 2026
383fba1
A repo whose remote is a folder is shown by its own folder
nitedani-agent Nov 10, 2026
4f105fc
Removing a project says it is removed and how to bring it back
nitedani-agent Nov 10, 2026
4184e89
The Queue page's count box shows the agents its button starts
nitedani-agent Nov 10, 2026
e2e66e0
Starting agents from the Queue and Tickets is called Start everywhere
nitedani-agent Nov 10, 2026
07f2f73
A ticket's page shows its text from below the title, its title and ke…
nitedani-agent Nov 10, 2026
0a36c74
A ticket's "← Tickets" returns to the list it was opened from
nitedani-agent Nov 10, 2026
92fdc02
A ticket's priority reads "Priority 8", as the Queue writes it
nitedani-agent Nov 10, 2026
5dc0d72
Shortcuts name the keyboard's own key, Ctrl off a Mac, and Enter in w…
nitedani-agent Nov 10, 2026
7daec43
The ✕ and ✓ in the dashboard's words are icons, drawn without a symbo…
nitedani-agent Nov 10, 2026
fcea8e6
The demo's project has the Logs and Automations pages
nitedani-agent Nov 10, 2026
79c1801
A page's heading says its name and its count apart
nitedani-agent Nov 10, 2026
f6813c6
The logo link is named OpenAgent, as its word says
nitedani-agent Nov 10, 2026
eadd1ba
A menu's one pick of several is a radio a screen reader says checked
nitedani-agent Nov 10, 2026
b8cdee8
A run stopped before its first turn says so, and keeps its prompt
nitedani-agent Nov 10, 2026
d92b9f1
A run that has not begun shows its prompt, on every page and after a …
nitedani-agent Nov 10, 2026
688b697
Only a working run waits for its short name, and the Overview's Agent…
nitedani-agent Nov 10, 2026
bddd3d7
A draft stays in its box across a navigation and a reload, until it i…
nitedani-agent Nov 10, 2026
0abdcf0
The project picker says when GitHub could not be reached for its repos
nitedani-agent Nov 10, 2026
a384df4
A failed start names the project's line and its file, then what the t…
nitedani-agent Nov 10, 2026
bff04f5
After a refused start the prompt has the focus again, so typing goes …
nitedani-agent Nov 10, 2026
3214b9a
A message waiting for a working agent says it is read at the agent's …
nitedani-agent Nov 10, 2026
576ec4d
The Log says Needs push or Needs PR alike on every page, the Overview…
nitedani-agent Nov 10, 2026
c35c6f6
A pushed branch with no pull request says what pull requests need, in…
nitedani-agent Nov 10, 2026
1aa151a
A work bar put away stays away after a reload, until the work moves on
nitedani-agent Nov 10, 2026
482aa5c
A session is called a session where it is deleted, gone, and in its p…
nitedani-agent Nov 10, 2026
9fe28c4
A session's details name its coding agent "Coding agent", as the Over…
nitedani-agent Nov 10, 2026
37a5737
A session that is gone shows no panes, and no guess at why it is gone
nitedani-agent Nov 10, 2026
fb4c28a
The rows around the message box fit the chat's own width, beside pane…
nitedani-agent Nov 10, 2026
f11daf5
While OpenAgent is not answering, the banner says so and nothing can …
nitedani-agent Nov 10, 2026
d0d82a4
Run on and Auto say their pick to a screen reader
nitedani-agent Nov 10, 2026
ceec7e8
The Changes heading reads with a space before whether it is merged
nitedani-agent Nov 10, 2026
85f5002
A Stop pressed while a run starts stops it once it is up
nitedani-agent Nov 10, 2026
167932e
On a phone the Agents and Permissions pages are named in the bar
nitedani-agent Nov 10, 2026
0f60ada
The Default agent's note is a line under its name, which keeps its wi…
nitedani-agent Nov 10, 2026
4463fda
The agent dialog opens with the cursor in Name
nitedani-agent Nov 10, 2026
f83502d
An agent's accounts show "All" ticked while it picks none
nitedani-agent Nov 10, 2026
fe86f90
The two login presses say what each does: log this computer in, or ad…
nitedani-agent Nov 10, 2026
ad4d27f
A coding agent's card says "Not logged in", and its copy button sits …
nitedani-agent Nov 10, 2026
8bccf64
Skills are "Save and commit" while the remote has no branch to send t…
nitedani-agent Nov 10, 2026
eaabbe5
Settings names the two agent data entries apart: Your agent data, Pro…
nitedani-agent Nov 10, 2026
85c757d
The coding agent is "Coding agent" in Settings and on the start box's…
nitedani-agent Nov 10, 2026
4bebbb0
Devices says "your other computer", whatever it runs
nitedani-agent Nov 10, 2026
0f5683a
A Linux computer with no display gives the login command to type, not…
nitedani-agent Nov 10, 2026
f78ebdc
The person menu's Browser notifications are ticked only once the brow…
nitedani-agent Nov 10, 2026
8780271
The command's footer is its version and whether it is the latest, in …
nitedani-agent Nov 10, 2026
ceef722
The install copies the dashboard whenever it comes from an installed …
nitedani-agent Nov 10, 2026
eb7ac89
Stopping the dashboard ends the logins and installs its presses started
nitedani-agent Nov 10, 2026
58d6499
The Usage card says Claude Code is not logged in, not that its accoun…
nitedani-agent Nov 10, 2026
c358895
Switching browser notifications on in Settings asks the browser, as t…
nitedani-agent Nov 10, 2026
604d255
A Logs row opens from the keyboard: its run's name is a button
nitedani-agent Nov 10, 2026
6853b17
Logs, Members and the Overview show a run's status in the dashboard's…
nitedani-agent Nov 10, 2026
d4ed814
A run waiting on a question says nothing under Took until it is over
nitedani-agent Nov 10, 2026
3e4f157
The Logs, Tickets and Queue pages open with their last list while the…
nitedani-agent Nov 10, 2026
45fbb42
The Logs page keeps its status pick in its address, across a reload a…
nitedani-agent Nov 10, 2026
4f44cb2
The Logs page says it lists the last 50 runs when a project has more
nitedani-agent Nov 10, 2026
9837fe4
An automation's pace says "Every day", not "Every 1 day"
nitedani-agent Nov 10, 2026
6089ca5
A switched-on automation says it does not start while the scheduler i…
nitedani-agent Nov 10, 2026
2bd0692
A stopped scheduler's line points at the Skills page first, and its c…
nitedani-agent Nov 10, 2026
66df0f4
Closing the New automation form, or a removal's note, hands the keybo…
nitedani-agent Nov 10, 2026
cd31fc2
The Automations page names the scheduler's model once, as its rows do…
nitedani-agent Nov 10, 2026
0761c7c
The Automations page's Run now says "Starting…" and its header "looke…
nitedani-agent Nov 10, 2026
5e9c3cd
The Automations card says a stopped scheduler once and leads to the p…
nitedani-agent Nov 10, 2026
107ad8c
The New automation form promises a start only while the scheduler runs
nitedani-agent Nov 10, 2026
97e183b
An Automations page with no rows names both ways to add one
nitedani-agent Nov 10, 2026
aea16dd
Settings › Scheduler says it is loading, and links the Automations pa…
nitedani-agent Nov 10, 2026
aa5e880
A name an automation cannot have says why under its field, which is m…
nitedani-agent Nov 10, 2026
8a554cc
The Logs page names a run as the other lists do
nitedani-agent Nov 10, 2026
bfb9fd7
The Logs, Tickets and Queue pages fade their remembered list while th…
nitedani-agent Nov 10, 2026
a58f6bb
On the Logs page a status's dot carries its color, and its word is pl…
nitedani-agent Nov 10, 2026
42e5695
Add computer asks "Don't see it?", true before any computer is listed
nitedani-agent Nov 10, 2026
9d4f606
The website's copy boxes are presses the keyboard reaches, and Enter …
nitedani-agent Nov 10, 2026
f96227c
A row's "·" wraps with the item after it, so no line of it ends in one
nitedani-agent Nov 10, 2026
5d15753
Settings › Scheduler's Spend offset field has a name, as a form field…
nitedani-agent Nov 10, 2026
7ff3ed7
The Permissions page's coding agent cards are the page's own cards, a…
nitedani-agent Nov 10, 2026
f84f942
Settings opened by its address waits for the projects, so it opens on…
nitedani-agent Nov 10, 2026
5ef9f27
The launcher's prompt takes the caret when the page opens, except on …
nitedani-agent Nov 10, 2026
a805fd6
The work bar's group is named for the session, as the session's other…
nitedani-agent Nov 10, 2026
e760309
A question's Recommended tag reads as a word of its own, and its own-…
nitedani-agent Nov 10, 2026
45df0f8
A message waiting for a working run stays in its chat across a visit …
nitedani-agent Nov 10, 2026
4db7d33
On a phone a kept draft comes back without the caret, which would rai…
nitedani-agent Nov 10, 2026
6b74f43
Hiding or showing the sidebar gives the focus to the button that take…
nitedani-agent Nov 10, 2026
ec86f76
A working run waits for its name only while its card says the name is…
nitedani-agent Nov 10, 2026
610e775
The first steps' action goes under its words until both fit side by side
nitedani-agent Nov 10, 2026
e6365db
The project picker opened downward is as tall as its rows
nitedani-agent Nov 10, 2026
a5af410
A module's page has a phone's padding on a phone
nitedani-agent Nov 11, 2026
f6de2e2
A server's login words, an agent's GitHub account and a git account's…
nitedani-agent Nov 11, 2026
1f8fa29
Settings closes back onto the page under it as that page left it, and…
nitedani-agent Nov 11, 2026
5d86933
Every press that starts a run is off while OpenAgent is not answering…
nitedani-agent Nov 11, 2026
311ca82
Closing a dialog gives the focus back to the press that opened it
nitedani-agent Nov 11, 2026
e1c9610
The add-account dialog's field has a name, as a form field should
nitedani-agent Nov 11, 2026
1378558
The person menu and Settings describe the notification switches in th…
nitedani-agent Nov 11, 2026
52ed61b
A page's buttons go under each other when its column is narrower than…
nitedani-agent Nov 11, 2026
77c5f15
The Overview's tiles and cards say "Loading…" before they have read w…
nitedani-agent Nov 11, 2026
2080acd
The Files pane's filter field has a name, as a form field should
nitedani-agent Nov 11, 2026
f47bcec
After a question is answered, the message box has the focus
nitedani-agent Nov 11, 2026
59793a6
A session left before its run says anything lets go of its live feed
nitedani-agent Nov 11, 2026
2a13200
Every form field of the dashboard and its modules has a name, the Sav…
nitedani-agent Nov 11, 2026
4532ba3
The remove dialog opens on Cancel, as the other confirmations do
nitedani-agent Nov 11, 2026
560a3c4
A question's choices are a radio group: the arrow keys move the pick,…
nitedani-agent Nov 11, 2026
79b9e97
A ticket's page has the "Show the sidebar" press, as every page has
nitedani-agent Nov 11, 2026
b1f9252
The owner-name question puts the focus in its field once the name is …
nitedani-agent Nov 11, 2026
5fe1f01
A time of day left half typed is caught when its field is left
nitedani-agent Nov 11, 2026
3e90fd7
A time of day left half typed holds Save whatever the unit or the cho…
nitedani-agent Nov 11, 2026
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
36 changes: 36 additions & 0 deletions packages/agent-runner/src/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,42 @@ test('a run asked only a slash command keeps it while it works, and is named fro
}
})

test('a run\'s card says its name is being asked while it is, not once the ask ends without one, nor for a slash command', async () => {
const repo = await testRepo()
try {
let answer: (title: string | undefined) => void = () => {}
const title = new Promise<string | undefined>(resolve => (answer = resolve))
const seen: unknown[] = []
const driver: Driver = {
id: 'claude-code',
start: async opts => {
const fake = (await new FakeDriver({ respond: () => 'Done.' }).start(opts)) as FakeDriverSession
// What the card says once the writes made so far have landed.
const naming = async () => {
await fake.log?.settled()
return (await readLiveCard(opts.cwd, opts.log!.card.id))?.caller?.['naming']
}
return wrap(fake, async () => {
seen.push(await naming())
answer(undefined)
for (let tries = 0; (await naming()) === true && tries < 100; tries++) await new Promise(resolve => setTimeout(resolve, 20))
seen.push(await naming())
})
},
}
const run = await runCommand(repo, { prompt: 'make readme.md shorter', naming: { title, stop: new AbortController() }, driver, now: () => NOW, gitHost: noGitHost })
assert.deepEqual(seen, [true, undefined])
assert.equal((await findRun(repo, run.id))?.title, undefined)

// A slash command is named from its report, at its end: nothing is asked while it works.
seen.length = 0
await runCommand(repo, { prompt: '/work-queue', driver, now: () => new Date(NOW.getTime() + 60_000), gitHost: noGitHost })
assert.deepEqual(seen, [undefined, undefined])
} finally {
await removeRepo(repo)
}
})

test('a run told how hard to think starts its agent that way and keeps it on its record', async () => {
const repo = await testRepo()
try {
Expand Down
7 changes: 6 additions & 1 deletion packages/agent-runner/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,8 +697,13 @@ function nameSession(run: SessionRun, session: DriverSession | undefined, stoppe
const titled = async (title: string | undefined): Promise<void> => {
if (title !== undefined) await session?.log?.title(title)
}
// The card says while the name is being asked, and only then: a list holds the run's place for it.
const asking = mayName && !fromReport
if (asking) void session?.log?.patch({ caller: { naming: true } })
return {
done: mayName && !fromReport ? (asked?.title ?? nameRun(run.driver, { prompt: run.prompt, signal: stopped })).then(titled, () => {}) : Promise.resolve(),
done: asking
? (asked?.title ?? nameRun(run.driver, { prompt: run.prompt, signal: stopped })).then(titled, () => {}).then(() => session?.log?.patch({ caller: { naming: undefined } }))
: Promise.resolve(),
// Named from its first report once its status is settled, before it is said ended, so the
// name's line in the diary comes before the ended one; a stop meanwhile only cuts the naming
// short. A question is no report of the work, and a few seconds is all a name may hold the
Expand Down
18 changes: 9 additions & 9 deletions packages/dashboard-changes/dashboard/ChangesPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const diffLines = (root: HTMLElement = document.body) => [...root.querySelectorA
describe('ChangesPanel', () => {
test('a run’s page lists only the files the run changed, grouped by folder, and its header says between which branches and that they are not merged yet', async () => {
render(<ChangesPanel projectId="p1" agentId="run-1" context={context} />)
await waitFor(() => expect(screen.getByTestId('pane-title').textContent).toBe(`${NOT_MERGED}· not merged yet`))
await waitFor(() => expect(screen.getByTestId('pane-title').textContent).toBe(`${NOT_MERGED} · not merged yet`))
expect(readRun).toHaveBeenCalledWith(expect.anything(), 'p1', 'run-1')
expect(readProject).not.toHaveBeenCalled()
expect(within(list()).getAllByRole('button').map(b => [b.textContent, b.title])).toEqual([
Expand Down Expand Up @@ -103,7 +103,7 @@ describe('ChangesPanel', () => {
test('a run that changed nothing, a run whose changes are gone, and a run still being read each say so in one line', async () => {
readRun.mockResolvedValue({ source: 'unchanged', changes: {} })
render(<ChangesPanel projectId="p1" agentId="run-1" context={context} />)
await waitFor(() => expect(screen.getByText('This run changed no files.')).toBeTruthy())
await waitFor(() => expect(screen.getByText('This session changed no files.')).toBeTruthy())
cleanup()
readRun.mockResolvedValue({ source: 'gone' })
render(<ChangesPanel projectId="p1" agentId="run-2" context={context} />)
Expand All @@ -112,7 +112,7 @@ describe('ChangesPanel', () => {
readRun.mockResolvedValue({ source: 'pending' })
render(<ChangesPanel projectId="p1" agentId="run-3" context={context} />)
await waitFor(() => expect(readRun).toHaveBeenCalledWith(expect.anything(), 'p1', 'run-3'))
expect(screen.getByText('Looking for this run’s changes…')).toBeTruthy()
expect(screen.getByText('Looking for this session’s changes…')).toBeTruthy()
})

test('the project’s own page lists what is changed in its folder and not committed, or says nothing is', async () => {
Expand All @@ -137,7 +137,7 @@ describe('ChangesPanel remembered', () => {
readRun.mockReturnValue(new Promise(() => {}) as never)
readCommits.mockReturnValue(new Promise(() => {}) as never)
render(<ChangesPanel projectId="p1" agentId="run-1" context={context} />)
expect(screen.queryByText('Looking for this run’s changes…')).toBeNull()
expect(screen.queryByText('Looking for this session’s changes…')).toBeNull()
expect(screen.getByText(NOT_MERGED)).toBeTruthy()
expect(readRun).toHaveBeenCalledTimes(2)
})
Expand All @@ -151,7 +151,7 @@ describe('ChangesPanel remembered', () => {
// Its commits have answered; its files have not.
await waitFor(() => expect(readCommits).toHaveBeenCalledWith(expect.anything(), 'p1', 'run-2'))
await new Promise(resolve => setTimeout(resolve, 20))
expect(screen.getByText('Looking for this run’s changes…')).toBeTruthy()
expect(screen.getByText('Looking for this session’s changes…')).toBeTruthy()
expect(queryRow(/app\.ts/)).toBeNull()
})
})
Expand Down Expand Up @@ -219,7 +219,7 @@ describe('ChangesPanel commits', () => {
render(<ChangesPanel projectId="p1" agentId="run-1" context={context} />)
await waitFor(() => expect(commitRows()).toHaveLength(3))
expect(screen.getByText('No change is left: the commits cancel out.')).toBeTruthy()
expect(screen.queryByText('This run changed no files.')).toBeNull()
expect(screen.queryByText('This session changed no files.')).toBeNull()
})

test('while the run’s files move as it ends, the commits last read stay, and so does the picked commit', async () => {
Expand All @@ -236,15 +236,15 @@ describe('ChangesPanel commits', () => {
expect(screen.getByText('bbbbbbb Change the app')).toBeTruthy()
})

test('"This run changed no files." is not said before the commits are read', async () => {
test('"This session changed no files." is not said before the commits are read', async () => {
readRun.mockResolvedValue({ source: 'checkout', changes: {}, merged: false })
let answer: (commits: unknown) => void = () => {}
readCommits.mockReturnValue(new Promise(resolve => (answer = resolve)) as never)
render(<ChangesPanel projectId="p1" agentId="run-1" context={context} />)
await waitFor(() => expect(readRun).toHaveBeenCalled())
await new Promise(resolve => setTimeout(resolve, 20))
expect(screen.queryByText('This run changed no files.')).toBeNull()
expect(screen.getByText('Looking for this run’s changes…')).toBeTruthy()
expect(screen.queryByText('This session changed no files.')).toBeNull()
expect(screen.getByText('Looking for this session’s changes…')).toBeTruthy()
answer([NEW])
await waitFor(() => expect(screen.getByText('No change is left: the commits cancel out.')).toBeTruthy())
})
Expand Down
9 changes: 5 additions & 4 deletions packages/dashboard-changes/dashboard/ChangesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ function ChangeSearch({ paths, changes, onPick }: { paths: string[]; changes: Re
</Tooltip>
<PopoverContent align="end" className="w-72 p-1">
<Input
name="search-changes"
autoFocus
type="search"
value={query}
Expand Down Expand Up @@ -268,7 +269,7 @@ function ChangesHeader({ between, merged, page, paths, changes, listShown, onPic
{between ? (
<h2 className="min-w-0 truncate pl-2 text-[13px] text-muted-foreground" title={between}>
{between}
{merged !== undefined && <span className="pl-1 text-xs text-muted-foreground">{merged ? '· merged' : '· not merged yet'}</span>}
{merged !== undefined && <span className="text-xs text-muted-foreground">{merged ? ' · merged' : ' · not merged yet'}</span>}
</h2>
) : (
<h2 className="truncate pl-2 text-[13px] text-muted-foreground">Changes</h2>
Expand Down Expand Up @@ -490,10 +491,10 @@ export function ChangesPanel({ projectId, agentId, activity, reveal }: ModulePan
</>
)
if (agentId) {
if (!runLoaded || !commitsLoaded || !run || run.source === 'pending') return waiting('Looking for this run’s changes…')
if (run.source === 'gone') return line('This run’s changes are gone from this machine: its checkout was reclaimed and it left no branch or merged pull request here.')
if (!runLoaded || !commitsLoaded || !run || run.source === 'pending') return waiting('Looking for this session’s changes…')
if (run.source === 'gone') return line('This session’s changes are gone from this machine: its checkout was reclaimed and it left no branch or merged pull request here.')
} else if (!projectLoaded) return waiting('Reading the project’s files…')
if (Object.keys(all).length === 0 && commits.length === 0) return line(agentId ? 'This run changed no files.' : 'Nothing is changed in the project’s folder.')
if (Object.keys(all).length === 0 && commits.length === 0) return line(agentId ? 'This session changed no files.' : 'Nothing is changed in the project’s folder.')

const caption = commit ? `${commit.short} ${commit.subject}` : agentId ? undefined : 'Changed in the project’s folder, not committed.'
// What stands where the diffs would be, when the list is empty.
Expand Down
2 changes: 1 addition & 1 deletion packages/dashboard-changes/dashboard/keys.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// The keys the reads are remembered under (`usePolled`'s `remember`): a pane opened again shows
// what was read last at once, while it is read again. Without them every opening of the pane
// passed through "Looking for this run’s changes…".
// passed through "Looking for this session’s changes…".

/** What is changed in the project's own folder. */
export const projectKey = (projectId: string) => `changes:project:${projectId}`
Expand Down
4 changes: 2 additions & 2 deletions packages/dashboard-devices/dashboard/AddDevice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type Adding = 'choose' | 'phone' | 'computer' | 'server' | 'code'

const KINDS: readonly { id: Exclude<Adding, 'choose'>; icon: ComponentType<{ className?: string; 'aria-hidden'?: boolean }>; title: string; words: string }[] = [
{ id: 'phone', icon: Smartphone, title: 'Phone or tablet', words: 'Scan a code with its camera.' },
{ id: 'computer', icon: Monitor, title: 'Computer', words: 'Another Mac or PC that runs OpenAgent.' },
{ id: 'computer', icon: Monitor, title: 'Computer', words: 'Another computer that runs OpenAgent.' },
{ id: 'server', icon: Server, title: 'Server', words: 'Install OpenAgent on a VPS, or on a PC that stays on.' },
{ id: 'code', icon: Ticket, title: 'I have a code', words: 'Join a server of yours, or a computer someone shared with you.' },
]
Expand Down Expand Up @@ -339,7 +339,7 @@ function AddComputer({ d, onClose }: { d: Devices; onClose: () => void }) {
))}
</ul>
)}
<p className="text-xs text-muted-foreground">Not in the list? Open OpenAgent on it, on the same Wi-Fi, with "On this Wi-Fi" on there.</p>
<p className="text-xs text-muted-foreground">Don't see it? Open OpenAgent on it, on the same Wi-Fi, with "On this Wi-Fi" on there.</p>
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-border pt-3">
<span className="text-sm">
Somewhere else?
Expand Down
2 changes: 1 addition & 1 deletion packages/dashboard-devices/dashboard/ComputerPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function ComputerPage({ d, item, onBack, onSetUp }: { d: Devices; item: C
>
<label className="min-w-60 flex-1 space-y-1 text-xs text-muted-foreground">
<span>{`On ${item.name}, open Settings › Devices › Add device › Computer › Give it this computer's link, and paste that link here:`}</span>
<Input autoFocus aria-label={`${item.name}'s link`} value={link} onChange={event => setLink(event.target.value)} />
<Input name="computer-link" autoFocus aria-label={`${item.name}'s link`} value={link} onChange={event => setLink(event.target.value)} />
</label>
<Button size="sm" type="submit" disabled={busy || !link.trim()}>
Save
Expand Down
4 changes: 2 additions & 2 deletions packages/dashboard-devices/dashboard/ConnectServer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ export function ConnectServer({ d, onClose, onInvite }: { d: Devices; onClose: (
<div className="grid grid-cols-[1fr_8rem] gap-2">
<label className="space-y-1 text-xs text-muted-foreground">
<span>{kind === 'tailscale' ? 'Its name on Tailscale' : kind === 'home' ? 'Its address on its network' : 'Its address'}</span>
<Input autoFocus aria-label="The server's address" placeholder={kind === 'tailscale' ? 'homepc.tail1234.ts.net' : kind === 'home' ? '192.168.1.40' : '203.0.113.5'} value={address} onChange={event => setAddress(event.target.value)} />
<Input name="server-address" autoFocus aria-label="The server's address" placeholder={kind === 'tailscale' ? 'homepc.tail1234.ts.net' : kind === 'home' ? '192.168.1.40' : '203.0.113.5'} value={address} onChange={event => setAddress(event.target.value)} />
</label>
<label className="space-y-1 text-xs text-muted-foreground">
<span>User</span>
<Input aria-label="User name" placeholder={pc ? 'yours on it' : 'root'} value={user} onChange={event => setUser(event.target.value)} />
<Input name="server-user" aria-label="User name" placeholder={pc ? 'yours on it' : 'root'} value={user} onChange={event => setUser(event.target.value)} />
</label>
</div>
<p className="text-xs text-muted-foreground">
Expand Down
19 changes: 18 additions & 1 deletion packages/dashboard-devices/dashboard/DevicesSettings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('Settings › Devices: this computer, and how devices reach it', () =>
expect(card.getByText("Nothing reaches it yet: only this computer's own browser opens OpenAgent.")).toBeTruthy()
expect(screen.getByText('Add your first device')).toBeTruthy()
expect(screen.getByText('Start runs and answer your agents from your phone, on the same Wi-Fi as this computer.')).toBeTruthy()
expect(screen.getByText('Run agents on your other Mac or PC, on the same Wi-Fi as this computer.')).toBeTruthy()
expect(screen.getByText('Run agents on your other computer, on the same Wi-Fi as this one.')).toBeTruthy()
expect(screen.getByRole('button', { name: 'Set up a server' })).toBeTruthy()
expect(screen.getByRole('button', { name: 'Join with a code' })).toBeTruthy()
// Only the server's card says "anywhere": nothing reaches this computer from anywhere yet.
Expand Down Expand Up @@ -705,6 +705,23 @@ describe('Settings › Devices: a computer several people use', () => {
expect(runComputerCommand.mock.calls.some(([args]) => args[0] === 'owner' || args[0] === 'add')).toBe(false)
})

test('the owner names themselves from the keyboard: their field has the focus once the name git knows is read, and again when the name is refused', async () => {
const SERVER = { on: false, listening: true, https: { address: '203.0.113.5' }, devices: [], persons: [] }
const { host } = door(args => (args[0] === 'me' ? ok({ name: 'Ada Lovelace' }) : args[0] === 'owner' ? { ok: false, error: 'that name is taken here' } : ok(SERVER)), [], () => ok({ server: true, servers: [] }))
show(host)
fireEvent.click(await screen.findByRole('button', { name: 'Add person' }))
fireEvent.change(await screen.findByRole('textbox', { name: 'Their name' }), { target: { value: 'Rom' } })
fireEvent.click(screen.getByRole('button', { name: 'Make the code' }))
const own = (await screen.findByRole('textbox', { name: 'First, your name?' })) as HTMLInputElement
await waitFor(() => expect(own.value).toBe('Ada Lovelace'))
await waitFor(() => expect(document.activeElement).toBe(own))
const go = screen.getByRole('button', { name: 'Continue' })
go.focus()
fireEvent.click(go)
expect(await screen.findByText(/that name is taken here/)).toBeTruthy()
await waitFor(() => expect(document.activeElement).toBe(own))
})

test('once named, the owner is not asked again, and the devices are listed by person; a person is removed with their devices', async () => {
const persons = [{ name: 'Ada', owner: true }, { name: 'Rom' }, { name: 'Eve' }]
const devices = [{ ...PHONE, person: 'Ada' }, { ...PHONE, id: 'fed654cba321', name: 'Pixel, Chrome', person: 'Rom' }]
Expand Down
4 changes: 2 additions & 2 deletions packages/dashboard-devices/dashboard/DevicesSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ function Start({ d, onAdd }: { d: Devices; onAdd: (adding: Adding) => void }) {
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
<Tile icon={Smartphone} title="Use it on your phone" words="Start runs and answer your agents from your phone, on the same Wi-Fi as this computer." button="Add phone" primary onClick={() => onAdd('phone')} />
<Tile icon={Monitor} title="Use another computer" words="Run agents on your other Mac or PC, on the same Wi-Fi as this computer." button="Add computer" onClick={() => onAdd('computer')} />
<Tile icon={Monitor} title="Use another computer" words="Run agents on your other computer, on the same Wi-Fi as this one." button="Add computer" onClick={() => onAdd('computer')} />
{!d.isServer && <Tile icon={Server} title="Get a server" words="Always on. Your phone and computers reach each other from anywhere, and you can share it with people." button="Set up a server" onClick={() => onAdd('server')} />}
</div>
<p className="text-xs text-muted-foreground">
Expand Down Expand Up @@ -327,7 +327,7 @@ function PhoneRow({ d, item }: { d: Devices; item: PhoneItem }) {
void act(() => d.changeDoor(() => renameDevice(host, item.door!.id, name.trim()))).then(done => done && setRenaming(false))
}}
>
<Input autoFocus aria-label="Name" value={name} maxLength={80} onChange={event => setName(event.target.value)} />
<Input name="device-name" autoFocus aria-label="Name" value={name} maxLength={80} onChange={event => setName(event.target.value)} />
<div className="flex justify-end gap-2">
<Button size="sm" type="button" variant="ghost" onClick={() => setRenaming(false)}>
Cancel
Expand Down
Loading
Loading