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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@

### Added

- Added Codex app-server auth token support for websocket and UDS
transports via `backendConfig.codex.authTokenEnv` and
`AGENT_RUNNER_CODEX_AUTH_TOKEN_ENV`, with token values resolved at
connection time instead of persisted.
([#167](https://github.com/kcosr/agent-runner/pull/167))
- Added `--no-inherit-run-group` for fresh `run` and `init` commands so
child runs can preserve `parentRunId` lineage while starting in their own
run group. ([#165](https://github.com/kcosr/agent-runner/pull/165))
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ The rest are focused topic pages:
| `AGENT_RUNNER_CWD` | Active backend attempt cwd provided to backend wrapper processes |
| `AGENT_RUNNER_CLAUDE_BIN` | Claude CLI binary |
| `AGENT_RUNNER_CODEX_BIN` | Codex stdio binary |
| `AGENT_RUNNER_CODEX_AUTH_TOKEN_ENV` | Default env var name containing the Codex app-server bearer token for fresh Codex WS/UDS runs |
| `AGENT_RUNNER_CODEX_UDS_PATH` | Default WebSocket-over-UDS transport socket path for fresh Codex runs when no explicit `backendConfig.codex.transport` was authored |
| `AGENT_RUNNER_CODEX_WS_URL` | Default websocket transport for fresh Codex runs when no explicit `backendConfig.codex.transport` was authored |
| `AGENT_RUNNER_CURSOR_BIN` | Cursor CLI binary |
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/daemon/request-parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ export function parseRunInputSurfaceQuery(search: string): RunInputSurfaceParams
const entries = queryEntries(search);
return {
agent: requiredNonEmptyString(entries.get("agent"), "agent"),
assignment: requiredNonEmptyString(entries.get("assignment"), "assignment"),
assignment: optionalNonEmptyString(entries.get("assignment"), "assignment"),
cwd: optionalNonEmptyString(entries.get("cwd"), "cwd"),
};
}
212 changes: 150 additions & 62 deletions apps/web/src/app.test.tsx

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions apps/web/src/components/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ function MermaidDiagram({ code }: { code: string }) {
aria-label="Mermaid diagram loading"
className="markdown-mermaid markdown-mermaid--loading"
>
Rendering Mermaid diagram...
Rendering Mermaid diagram
</div>
);
}
Expand Down Expand Up @@ -371,8 +371,13 @@ function FrontmatterTable({ rows }: { rows: FrontmatterTableRow[] }) {
}

const components: Components = {
a({ node: _node, href, ...props }) {
return <a {...props} href={href} target="_blank" rel="noopener noreferrer" />;
a({ children, node: _node, href, ...props }) {
const ariaLabel = props["aria-label"] ?? (children ? undefined : (href ?? "Open link"));
return (
<a {...props} aria-label={ariaLabel} href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
);
},
pre({ node: _node, children, ...props }) {
const mermaidCode = readMermaidBlock(children);
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/run-column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@ export function RunColumn({
className="column"
data-collapsed={collapsed ? "true" : "false"}
data-status={column.key}
onClick={handleColumnClick}
onKeyDown={handleColumnKeyDown}
onClick={collapsed ? handleColumnClick : undefined}
onKeyDown={collapsed ? handleColumnKeyDown : undefined}
ref={columnRef}
role={collapsed ? "button" : undefined}
tabIndex={collapsed ? 0 : undefined}
>
<header className="col-head">
Expand Down
24 changes: 17 additions & 7 deletions apps/web/src/components/run-detail-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ export function RunDetailDrawer({
}) {
const drawerRef = useRef<HTMLElement | null>(null);
const drawerBodyRef = useRef<HTMLDivElement | null>(null);
const sectionTabsRef = useRef<HTMLElement | null>(null);
const sectionTabsRef = useRef<HTMLDivElement | null>(null);
const timelineContentScrollRef = useRef<HTMLDivElement | null>(null);
const timelineResponseAtBottomRef = useRef(false);
const latestAttemptRef = useRef<number | null>(null);
Expand Down Expand Up @@ -846,10 +846,10 @@ export function RunDetailDrawer({
}, new Map<number, typeof timelineAttempts>()),
([sessionIndex, attempts]) => ({
sessionIndex,
attempts: [...attempts].sort((a, b) => a.attemptNumber - b.attemptNumber),
attempts: attempts.toSorted((a, b) => a.attemptNumber - b.attemptNumber),
summary: run.sessions.find((session) => session.sessionIndex === sessionIndex) ?? null,
}),
).sort((a, b) => a.sessionIndex - b.sessionIndex);
).toSorted((a, b) => a.sessionIndex - b.sessionIndex);
const pendingAttemptAvailable =
(run.status === "initialized" || run.status === "ready") &&
run.totalAttemptCount === 0 &&
Expand Down Expand Up @@ -2293,11 +2293,17 @@ export function RunDetailDrawer({
</div>
) : null}

<nav aria-label="Run sections" className="tabs tabs--scrollable" ref={sectionTabsRef}>
<div
aria-label="Run sections"
className="tabs tabs--scrollable"
ref={sectionTabsRef}
role="tablist"
>
<button
aria-selected={activeSection === "attachments"}
className={activeSection === "attachments" ? "tab active" : "tab"}
onClick={() => onSelectSection("attachments")}
role="tab"
type="button"
>
Attachments
Expand All @@ -2310,6 +2316,7 @@ export function RunDetailDrawer({
aria-selected={activeSection === "events"}
className={activeSection === "events" ? "tab active" : "tab"}
onClick={() => onSelectSection("events")}
role="tab"
type="button"
>
Attempts
Expand All @@ -2319,6 +2326,7 @@ export function RunDetailDrawer({
aria-selected={activeSection === "audit"}
className={activeSection === "audit" ? "tab active" : "tab"}
onClick={() => onSelectSection("audit")}
role="tab"
type="button"
>
Audit
Expand All @@ -2330,6 +2338,7 @@ export function RunDetailDrawer({
aria-selected={activeSection === "data"}
className={activeSection === "data" ? "tab active" : "tab"}
onClick={() => onSelectSection("data")}
role="tab"
type="button"
>
Data
Expand All @@ -2338,6 +2347,7 @@ export function RunDetailDrawer({
aria-selected={activeSection === "dependencies"}
className={activeSection === "dependencies" ? "tab active" : "tab"}
onClick={() => onSelectSection("dependencies")}
role="tab"
type="button"
>
Dependencies
Expand All @@ -2348,7 +2358,7 @@ export function RunDetailDrawer({
</span>
) : null}
</button>
</nav>
</div>

{activeSection === "attachments" ? (
<section aria-label="Attachments" className="drawer-panel drawer-panel--attachments">
Expand Down Expand Up @@ -3209,7 +3219,7 @@ export function RunDetailDrawer({
)
) : timelineTab === "response" ? (
selectedPendingAttempt ? (
<p className="task-empty">No response yet this run has not started.</p>
<p className="task-empty">No response yet; this run has not started.</p>
) : selectedAttemptResponse ? (
<section aria-label="Attempt response">
<MarkdownContent
Expand All @@ -3225,7 +3235,7 @@ export function RunDetailDrawer({
</p>
)
) : selectedPendingAttempt ? (
<p className="task-empty">No diagnostics yet this run has not started.</p>
<p className="task-empty">No diagnostics yet; this run has not started.</p>
) : selectedAttemptDiagnostics ? (
<section aria-label="Attempt diagnostics">
<MarkdownContent
Expand Down
138 changes: 73 additions & 65 deletions apps/web/src/components/run-diffs-surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ export function RunDiffsSurface({
const [dialogReference, setDialogReference] = useState<TaskReference | null>(null);
const rootRef = useRef<HTMLElement | null>(null);
const codeViewRef = useRef<CodeViewHandle<undefined> | null>(null);
const activeDiffVersionRef = useRef(0);
const mobileCollapsedPathRef = useRef<string | null>(null);
const searchRequestVersionRef = useRef(searchRequestVersion);
const persistedSidebarWidth = viewState.diffsSidebarWidth;
Expand Down Expand Up @@ -415,26 +416,48 @@ export function RunDiffsSurface({
});
const selectedTreePaths = useFileTreeSelection(tree.model);
const treeSearch = useFileTreeSearch(tree.model);
const filePathsRef = useRef(filePaths);
const treeModelRef = useRef(tree.model);
const treeSearchRef = useRef(treeSearch);
filePathsRef.current = filePaths;
treeModelRef.current = tree.model;
treeSearchRef.current = treeSearch;
const parsedDiffItems = useMemo(() => (diff ? parseDiffItems(diff) : []), [diff]);
const codeViewItems = useMemo(
() => parsedDiffItems.map((entry) => entry.item),
[parsedDiffItems],
);
const codeViewItemIds = useMemo(() => codeViewItems.map((item) => item.id), [codeViewItems]);
const codeViewItemIdSet = useMemo(() => new Set(codeViewItemIds), [codeViewItemIds]);
const allDiffItemsCollapsed =
codeViewItemIds.length > 0 && codeViewItemIds.every((id) => collapsedDiffItemIds.has(id));
const codeViewContentVersionKey = diff ? `${diff.displayRange}\0${diff.patch}` : "";
const codeViewContentVersion = useMemo(
() => hashCodeViewVersion(codeViewContentVersionKey),
[codeViewContentVersionKey],
);
useEffect(() => {
const validIds = new Set(codeViewItemIds);
setCollapsedDiffItemIds((current) => {
const next = new Set([...current].filter((id) => validIds.has(id)));
return next.size === current.size ? current : next;
});
}, [codeViewItemIds]);

if (collapsedDiffItemIds.size > 0) {
const validCollapsedIds = new Set(
Array.from(collapsedDiffItemIds).filter((id) => codeViewItemIdSet.has(id)),
);
if (validCollapsedIds.size !== collapsedDiffItemIds.size) {
setCollapsedDiffItemIds(validCollapsedIds);
}
}

if (activeDiffVersionRef.current !== codeViewContentVersion) {
activeDiffVersionRef.current = codeViewContentVersion;
setSelectedPath(
diff
? selectedPath && filePathSet.has(selectedPath)
? selectedPath
: firstSelectablePath(files)
: null,
);
setSelectedLines(null);
}

const displayedCodeViewItems = useMemo(
() =>
codeViewItems.map(
Expand Down Expand Up @@ -493,18 +516,6 @@ export function RunDiffsSurface({
};
}, [diff, files, parsedDiffItemById, selectedLines]);

useEffect(() => {
if (!diff) {
setSelectedPath(null);
setSelectedLines(null);
return;
}
setSelectedPath((current) =>
current && filePathSet.has(current) ? current : firstSelectablePath(files),
);
setSelectedLines(null);
}, [diff, files, filePathSet]);

useEffect(() => {
tree.model.resetPaths(filePaths);
tree.model.setGitStatus(gitStatus);
Expand Down Expand Up @@ -552,59 +563,51 @@ export function RunDiffsSurface({
focusTreeSearchInput();
}, [searchRequestVersion]);

useEffect(() => {
const root = rootRef.current;
if (!root) {
function treeSearchInputFromEvent(
event: ReactKeyboardEvent<HTMLElement>,
): HTMLInputElement | null {
const target = event.target;
return target instanceof HTMLInputElement && target.matches("[data-file-tree-search-input]")
? target
: null;
}

function handleTreeSearchKeyDown(event: ReactKeyboardEvent<HTMLElement>) {
if (event.defaultPrevented) {
return;
}

function treeSearchInputFromEvent(event: KeyboardEvent): HTMLInputElement | null {
const target = event
.composedPath()
.find(
(element) =>
element instanceof HTMLInputElement && element.matches("[data-file-tree-search-input]"),
);
return target instanceof HTMLInputElement ? target : null;
const input = treeSearchInputFromEvent(event);
if (!input || (event.key !== "ArrowDown" && event.key !== "ArrowUp")) {
return;
}

function handleTreeSearchKeyDown(event: KeyboardEvent) {
if (event.defaultPrevented) {
return;
}
const input = treeSearchInputFromEvent(event);
if (!input || (event.key !== "ArrowDown" && event.key !== "ArrowUp")) {
return;
}

event.preventDefault();
event.stopPropagation();
const hasSearch = treeSearch.value.trim().length > 0;
if (hasSearch && treeSearch.matchingPaths.length > 0) {
if (event.key === "ArrowDown") {
treeSearch.focusNextMatch();
} else {
treeSearch.focusPreviousMatch();
}
return;
}

event.preventDefault();
event.stopPropagation();
const currentTreeSearch = treeSearchRef.current;
const hasSearch = currentTreeSearch.value.trim().length > 0;
if (hasSearch && currentTreeSearch.matchingPaths.length > 0) {
if (event.key === "ArrowDown") {
const firstPath = filePaths.at(0);
if (firstPath) {
tree.model.focusPath(firstPath);
}
currentTreeSearch.focusNextMatch();
} else {
const lastPath = filePaths.at(-1);
if (lastPath) {
tree.model.focusPath(lastPath);
}
currentTreeSearch.focusPreviousMatch();
}
return;
}

root.addEventListener("keydown", handleTreeSearchKeyDown, { capture: true });
return () => root.removeEventListener("keydown", handleTreeSearchKeyDown, { capture: true });
}, [filePaths, tree.model, treeSearch]);
const currentFilePaths = filePathsRef.current;
const currentTreeModel = treeModelRef.current;
if (event.key === "ArrowDown") {
const firstPath = currentFilePaths.at(0);
if (firstPath) {
currentTreeModel.focusPath(firstPath);
}
} else {
const lastPath = currentFilePaths.at(-1);
if (lastPath) {
currentTreeModel.focusPath(lastPath);
}
}
}

function applyRange(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
Expand Down Expand Up @@ -793,7 +796,12 @@ export function RunDiffsSurface({
);

return (
<section aria-label="Diffs" className="drawer-panel drawer-panel--diffs" ref={rootRef}>
<section
aria-label="Diffs"
className="drawer-panel drawer-panel--diffs"
onKeyDownCapture={handleTreeSearchKeyDown}
ref={rootRef}
>
<div className="diffs-range-controls">
<div className="task-tabs" role="tablist" aria-label="Diff source">
<button
Expand Down Expand Up @@ -854,7 +862,7 @@ export function RunDiffsSurface({
<p className="diffs-notice">Patch output was truncated at {formatBytes(diff.maxBytes)}.</p>
) : null}
{diffQuery.isError ? <p className="files-error">{diffQuery.error.message}</p> : null}
{loading ? <p className="task-empty">Loading diff...</p> : null}
{loading ? <p className="task-empty">Loading diff</p> : null}
{empty ? <p className="task-empty">No changes in this comparison.</p> : null}

<div
Expand Down
Loading