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
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"test:server": "vitest run --project server",
"test:client": "vitest run --project client",
"automation:icons": "node ./scripts/generate-automation-icons.mjs",
"task-robot:icons": "node ./scripts/generate-task-robot-icons.mjs",
"docs:assets": "node ./scripts/sync-docs-assets.mjs",
"dev:prepare": "rimraf .next && pnpm docs:assets && pnpm automation:icons",
"dev": "pnpm docs:assets && pnpm automation:icons && dotenvx run -f ../../.env.local -- next dev --turbopack",
Expand Down
Binary file added apps/web/public/task-robots/robot-001.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-002.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-003.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-004.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-005.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-006.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-007.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-008.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-009.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-010.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-011.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-012.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-013.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-014.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-015.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-016.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-017.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-018.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-019.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-020.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-021.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-022.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-023.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/task-robots/robot-025.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
161 changes: 161 additions & 0 deletions apps/web/scripts/generate-task-robot-icons.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { mkdir, rm } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

import sharp from 'sharp';

const SCRIPT_DIRECTORY = dirname(fileURLToPath(import.meta.url));
const SOURCE_PATH = join(SCRIPT_DIRECTORY, 'task-robot-icons.source.png');
const OUTPUT_DIRECTORY = join(SCRIPT_DIRECTORY, '..', 'public', 'task-robots');
const SOURCE_GRID_SIZE = 5;
const ICON_COUNT = 25;
const OUTPUT_SIZE = 96;
const DRAWING_SIZE = 70;
const DRAWING_PADDING = 2;
const BACKGROUND = { r: 213, g: 237, b: 65 };
const FOREGROUND_DISTANCE_SQUARED = 50 ** 2;

async function findDrawingMetrics(image, name) {
const { data: pixels, info } = await sharp(image)
.removeAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
let left = info.width;
let top = info.height;
let right = -1;
let bottom = -1;
let horizontalMass = 0;
let verticalMass = 0;
let foregroundPixels = 0;

for (let y = 0; y < info.height; y += 1) {
for (let x = 0; x < info.width; x += 1) {
const offset = (y * info.width + x) * info.channels;
const red = pixels[offset] - BACKGROUND.r;
const green = pixels[offset + 1] - BACKGROUND.g;
const blue = pixels[offset + 2] - BACKGROUND.b;
if (
red * red + green * green + blue * blue <=
FOREGROUND_DISTANCE_SQUARED
) {
continue;
}

left = Math.min(left, x);
top = Math.min(top, y);
right = Math.max(right, x);
bottom = Math.max(bottom, y);
horizontalMass += x;
verticalMass += y;
foregroundPixels += 1;
}
}

if (right < 0) throw new Error(`${name} contains no drawing.`);
return {
left,
top,
right,
bottom,
imageWidth: info.width,
imageHeight: info.height,
centerX: horizontalMass / foregroundPixels,
centerY: verticalMass / foregroundPixels,
};
}

function paddedRectangle(bounds, padding) {
const left = Math.max(0, bounds.left - padding);
const top = Math.max(0, bounds.top - padding);
const right = Math.min(bounds.imageWidth - 1, bounds.right + padding);
const bottom = Math.min(bounds.imageHeight - 1, bounds.bottom + padding);
return { left, top, width: right - left + 1, height: bottom - top + 1 };
}

const source = sharp(SOURCE_PATH);
const metadata = await source.metadata();
if (!metadata.width || !metadata.height || metadata.width !== metadata.height) {
throw new Error('Task robot source must be a square image.');
}

await rm(OUTPUT_DIRECTORY, { recursive: true, force: true });
await mkdir(OUTPUT_DIRECTORY, { recursive: true });

for (let index = 0; index < ICON_COUNT; index += 1) {
const row = Math.floor(index / SOURCE_GRID_SIZE);
const column = index % SOURCE_GRID_SIZE;
const left = Math.round((column * metadata.width) / SOURCE_GRID_SIZE);
const top = Math.round((row * metadata.height) / SOURCE_GRID_SIZE);
const right = Math.round(((column + 1) * metadata.width) / SOURCE_GRID_SIZE);
const bottom = Math.round(((row + 1) * metadata.height) / SOURCE_GRID_SIZE);
const name = `robot-${String(index + 1).padStart(3, '0')}.png`;
const cell = await sharp(SOURCE_PATH)
.extract({ left, top, width: right - left, height: bottom - top })
.png()
.toBuffer();
const sourceBounds = await findDrawingMetrics(cell, name);

const { data: drawing, info } = await sharp(cell)
.extract(paddedRectangle(sourceBounds, DRAWING_PADDING))
.resize(DRAWING_SIZE, DRAWING_SIZE, { fit: 'inside' })
.png({ compressionLevel: 9 })
.toBuffer({ resolveWithObject: true });
const outputLeft = Math.floor((OUTPUT_SIZE - info.width) / 2);
const outputTop = Math.floor((OUTPUT_SIZE - info.height) / 2);

const initialIcon = await sharp({
create: {
width: OUTPUT_SIZE,
height: OUTPUT_SIZE,
channels: 3,
background: BACKGROUND,
},
})
.composite([{ input: drawing, left: outputLeft, top: outputTop }])
.png({ compressionLevel: 9 })
.toBuffer();
const finalBounds = await findDrawingMetrics(initialIcon, name);
const finalRectangle = paddedRectangle(finalBounds, DRAWING_PADDING);
const finalDrawing = await sharp(initialIcon)
.extract(finalRectangle)
.png()
.toBuffer();

await sharp({
create: {
width: OUTPUT_SIZE,
height: OUTPUT_SIZE,
channels: 3,
background: BACKGROUND,
},
})
.composite([
{
input: finalDrawing,
left: Math.max(
0,
Math.min(
OUTPUT_SIZE - finalRectangle.width,
Math.round(
(OUTPUT_SIZE - 1) / 2 -
(finalBounds.centerX - finalRectangle.left),
),
),
),
top: Math.max(
0,
Math.min(
OUTPUT_SIZE - finalRectangle.height,
Math.round(
(OUTPUT_SIZE - 1) / 2 -
(finalBounds.centerY - finalRectangle.top),
),
),
),
},
])
.png({ compressionLevel: 9, palette: true, colours: 256, dither: 0 })
.toFile(join(OUTPUT_DIRECTORY, name));
}

console.log(`Generated ${ICON_COUNT} task robot icons in ${OUTPUT_DIRECTORY}`);
Binary file added apps/web/scripts/task-robot-icons.source.png
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
getTextFromContentBlocks,
inferAcpMessageKind,
parsePrReviewActionOffer,
getTaskModelDisplayName,
type AcpMessage,
type PrReviewActionChoice,
type AcpEventType,
Expand Down Expand Up @@ -68,6 +69,7 @@ import {
AcpTranscriptBlockList,
useAcpTranscriptBlocks,
} from '../../task/[taskId]/messages/acp';
import { ModelBadge } from '@/components/sandbox';
import {
AcpProtocolService,
toAcpUiMessage,
Expand Down Expand Up @@ -308,6 +310,7 @@ export function FastSessionTranscript({
const taskStateRevision = useSessionTaskStateRevision();
const { enabled: narrationModeEnabled } = useNarrationMode();
const displayMode = narrationModeEnabled ? 'narration' : 'default';
const effectiveSessionModel = sessionModel ?? defaultModelId;
const slackMentionScope = useMemo<SlackMentionScope>(
() => ({ kind: 'session', sessionId }),
[sessionId],
Expand Down Expand Up @@ -750,13 +753,25 @@ export function FastSessionTranscript({
>
<SlackMentionProvider scope={slackMentionScope}>
<WorkspaceHeader
className="py-4.25"
contentClassName={SESSION_HEADER_CONTENT_CLASS_NAME}
className="py-3.25"
contentClassName={`${SESSION_HEADER_CONTENT_CLASS_NAME} !flex-col !items-stretch !gap-1`}
>
<h1 className={`ph-no-capture ${SESSION_HEADER_TITLE_CLASS_NAME}`}>
{title ?? fallbackTitle}
</h1>
{headerExtras}
{(effectiveSessionModel || headerExtras) && (
<div className="flex min-w-0 flex-wrap items-center gap-x-4 gap-y-2 text-xs text-muted-foreground">
{effectiveSessionModel ? (
<ModelBadge
model={effectiveSessionModel}
displayName={getTaskModelDisplayName(effectiveSessionModel)}
showIcon={false}
iconClassName="text-muted-foreground"
/>
) : null}
{headerExtras}
</div>
)}
</WorkspaceHeader>
<Conversation className="min-h-0 flex-1" initial="instant">
<ConversationContent className="ph-no-capture mx-auto w-full max-w-4xl p-4 pt-0">
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ import {
ExternalLink,
Skeleton,
} from '@/components/system';
import { WorkspaceBadge } from '@/components/sandbox';
import { FramedSurface } from '@/components/layout';
import {
TaskHeaderContent,
TaskHeaderMetadata,
TaskTitle,
} from '../../task/[taskId]/TaskHeader';

import { ArtifactLinkProvider } from '../../task/[taskId]/hooks/ArtifactLinkProvider';
import { HistoricalSandboxProvider } from '../../task/[taskId]/hooks/HistoricalSandboxProvider';
Expand Down Expand Up @@ -287,6 +291,10 @@ export function NestedTaskSidePanel({
const title = session.task?.title?.trim() || 'Task';
const environmentId = session.taskRun?.payload?.environmentId;
const repo = session.taskRun?.payload?.repo;
const model = session.task?.model ?? null;
const pullRequests = session.taskRun?.pullRequests ?? [];
const prRepo = session.taskRun?.prRepo;
const prNumber = session.taskRun?.prNumber;

return (
<FramedSurface
Expand All @@ -298,14 +306,6 @@ export function NestedTaskSidePanel({
onClose={onClose}
actions={
<>
{environmentId || repo ? (
<WorkspaceBadge
environmentId={environmentId}
repo={repo}
className="max-w-32 text-xs text-muted-foreground"
iconClassName="text-muted-foreground"
/>
) : null}
<BasicTooltip content="Go to task">
<Button asChild variant="ghost" size="icon" className="size-8">
<Link href={`/task/${taskId}`} aria-label="Go to task">
Expand All @@ -316,43 +316,59 @@ export function NestedTaskSidePanel({
</>
}
titleAdornment={
tasks.length > 1 ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="relative -left-2 flex h-7 w-full min-w-0 justify-start gap-1.5 px-2 text-sm hover:text-accent-foreground"
>
<span className="shrink-0 font-semibold">Task:</span>
<span className="min-w-0 flex-1 truncate text-left font-medium">
{title}
</span>
<ChevronDown className="size-3.5 shrink-0" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="max-h-80">
<DropdownMenuLabel>Tasks in this session</DropdownMenuLabel>
{tasks.map((task) => (
<DropdownMenuItem
key={task.taskId}
className="cursor-pointer text-xs"
onClick={() => onSelectTask?.(task.taskId)}
<TaskHeaderContent taskId={taskId} showIcon={false}>
{tasks.length > 1 ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="relative -left-2 flex h-7 w-full min-w-0 justify-start gap-1.5 px-2 text-sm hover:text-accent-foreground"
>
<span className="max-w-72 truncate">{task.title}</span>
{task.taskId === taskId ? (
<span className="ml-auto text-muted-foreground">
&bull;
</span>
) : null}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : (
<h2 className="truncate text-sm font-medium whitespace-nowrap">
<span className="font-semibold">Task:</span> {title}
</h2>
)
<TaskTitle
taskId={taskId}
title={title}
className="flex-1 text-left font-medium"
/>
<ChevronDown className="size-3.5 shrink-0" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="max-h-80">
<DropdownMenuLabel>Tasks in this session</DropdownMenuLabel>
{tasks.map((task) => (
<DropdownMenuItem
key={task.taskId}
className="cursor-pointer text-xs"
onClick={() => onSelectTask?.(task.taskId)}
>
<TaskTitle
taskId={task.taskId}
title={task.title}
className="max-w-72"
/>
{task.taskId === taskId ? (
<span className="ml-auto text-muted-foreground">
&bull;
</span>
) : null}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : (
<h2 className="truncate text-sm font-medium whitespace-nowrap">
<TaskTitle taskId={taskId} title={title} prefix="Task:" />
</h2>
)}
<TaskHeaderMetadata
model={model}
environmentId={environmentId}
repo={repo}
pullRequests={pullRequests}
prRepo={prRepo}
prNumber={prNumber}
className="pl-7"
/>
</TaskHeaderContent>
}
/>
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
Expand Down
Loading
Loading