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
34 changes: 26 additions & 8 deletions api/src/chat/chat.prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,21 @@ export function buildChatPrompt(p: {
.join('\n\n');

return [
`You are Hermes, a capable autonomous assistant in an interactive chat. Help with research, questions, analysis, and general engineering work.`,
workspaceNote,
`Use your full toolset — web access, file reading, and shell — as needed to answer well. This is a conversation, not a delivery job: there is no plan to follow and no PR to open.`,
`--- CONVERSATION SO FAR ---\n${transcript}\n--- END CONVERSATION ---`,
`Respond to the latest user message above. Output only your reply in GitHub-flavored Markdown — no preamble, no sign-off.`,
`# Role

You are Hermes, a capable autonomous assistant in an interactive chat. Help with research, questions, analysis, and general engineering work.

${workspaceNote}

Use your full toolset — web access, file reading, and shell — as needed to answer well. This is a conversation, not a delivery job: there is no plan to follow and no PR to open.`,
`# Context

--- CONVERSATION SO FAR ---
${transcript}
--- END CONVERSATION ---`,
`# Responding

Respond to the latest user message above. Output only your reply in GitHub-flavored Markdown — no preamble, no sign-off.`,
].join('\n\n');
}

Expand All @@ -32,8 +42,16 @@ export function buildChatPrompt(p: {
*/
export function buildTitlePrompt(firstMessage: string): string {
return [
`Generate a short, descriptive title for a chat conversation that opens with the message below.`,
`Rules:\n- Output ONLY the title — no quotes, no markdown, no trailing punctuation, no preamble.\n- 4 to 8 words, Title Case.\n- Do NOT use any tools; just reply with the title.`,
`--- FIRST MESSAGE ---\n${firstMessage}\n--- END ---`,
`# Task

Generate a short, descriptive title for a chat conversation that opens with the message below.`,
`--- FIRST MESSAGE ---
${firstMessage}
--- END FIRST MESSAGE ---`,
`# Rules

- Output ONLY the title — no quotes, no markdown, no trailing punctuation, no preamble.
- 4 to 8 words, Title Case.
- Do NOT use any tools; just reply with the title.`,
].join('\n\n');
}
24 changes: 7 additions & 17 deletions app/src/components/AgentRunRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,9 @@ interface Props {
jobId: string;
}

const PHASE_COLOURS: Record<string, string> = {
PLAN: 'bg-blue-900/60 text-blue-300',
IMPLEMENT: 'bg-sky-900/60 text-sky-300',
REVIEW: 'bg-violet-900/60 text-violet-300',
REVISE: 'bg-sky-900/60 text-sky-300',
VERIFY: 'bg-teal-900/60 text-teal-300',
SUMMARY: 'bg-zinc-800 text-zinc-300',
JUDGE: 'bg-pink-900/60 text-pink-300',
};

function phasePill(phase: string): string {
return PHASE_COLOURS[phase] ?? 'bg-zinc-800 text-zinc-300';
}
// Phase badges are neutral labels — the phase name carries the meaning, so they all share one zinc
// style rather than a per-phase rainbow.
const PHASE_PILL = 'bg-zinc-800 text-zinc-300';

function formatDuration(ms: number | null): string {
if (ms === null) return '';
Expand All @@ -42,20 +32,20 @@ export default function AgentRunRow({ run, jobId }: Props) {
return (
<div
class={`rounded-lg border p-3 transition-colors ${
isRunning ? 'border-hermes-300/30 bg-zinc-900/50' : 'border-zinc-800 bg-zinc-900/50'
isRunning ? 'border-green-500/30 bg-zinc-900/50' : 'border-zinc-800 bg-zinc-900/50'
}`}
>
<div class="flex items-center gap-3">
{/* Phase badge */}
<span class={`font-mono text-xs px-2.5 py-1 rounded shrink-0 ${phasePill(run.phase)}`}>
<span class={`font-mono text-xs px-2.5 py-1 rounded shrink-0 ${PHASE_PILL}`}>
{run.phase}
</span>

{/* Status, with model + duration on a meta line beneath */}
<div class="flex-1 min-w-0 leading-tight">
{isRunning ? (
<span class="flex items-center gap-1.5 text-xs text-hermes-400 font-medium">
<span class="w-1.5 h-1.5 rounded-full bg-hermes-400 animate-pulse" />
<span class="flex items-center gap-1.5 text-xs text-green-400 font-medium">
<span class="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />
Running
</span>
) : isJudge && run.status === 'SUCCEEDED' ? (
Expand Down
8 changes: 4 additions & 4 deletions app/src/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export default function Chat() {
<div class="flex flex-col items-center justify-center h-full text-zinc-500 gap-3">
<p class="text-base">Chat not found</p>
<button
class="text-sm text-indigo-400 hover:text-indigo-300"
class="text-sm text-cyan-400 hover:text-cyan-300"
onClick={() => navigate('/chats')}
>
Back to chats
Expand Down Expand Up @@ -211,7 +211,7 @@ export default function Chat() {
<div class="max-w-3xl mx-auto px-4 py-5 space-y-4">
{session === null ? (
<div class="flex justify-center py-10">
<div class="w-5 h-5 border-2 border-zinc-700 border-t-indigo-500 rounded-full animate-spin" />
<div class="w-5 h-5 border-2 border-zinc-700 border-t-cyan-500 rounded-full animate-spin" />
</div>
) : session.messages.length === 0 && !activeRunId ? (
<p class="text-center text-sm text-zinc-600 py-10">
Expand Down Expand Up @@ -269,7 +269,7 @@ export default function Chat() {
<select
value={modelKey}
onChange={(e) => setModelKey((e.target as HTMLSelectElement).value)}
class="rounded-lg bg-zinc-900 border border-zinc-800 px-2 py-1 text-xs text-zinc-300 focus:outline-none focus:border-indigo-600"
class="rounded-lg bg-zinc-900 border border-zinc-800 px-2 py-1 text-xs text-zinc-300 focus:outline-none focus:border-cyan-600"
>
{models.map((m) => (
<option key={m.key} value={m.key}>
Expand All @@ -292,7 +292,7 @@ export default function Chat() {
placeholder={activeRunId ? 'Hermes is responding…' : 'Message Hermes…'}
rows={1}
disabled={!!activeRunId}
class="flex-1 rounded-xl bg-zinc-900 border border-zinc-800 px-3 py-2.5 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-indigo-600 resize-none disabled:opacity-60"
class="flex-1 rounded-xl bg-zinc-900 border border-zinc-800 px-3 py-2.5 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-cyan-600 resize-none disabled:opacity-60"
/>
<button
type="submit"
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/ChatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export default function ChatList() {
<div class="flex-1 overflow-y-auto">
{sessions === null ? (
<div class="flex justify-center py-16">
<div class="w-5 h-5 border-2 border-zinc-700 border-t-indigo-500 rounded-full animate-spin" />
<div class="w-5 h-5 border-2 border-zinc-700 border-t-cyan-500 rounded-full animate-spin" />
</div>
) : sessions.length === 0 ? (
<p class="text-center text-sm text-zinc-600 py-16 px-4">
Expand Down
8 changes: 4 additions & 4 deletions app/src/components/CreateJob.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { navigate } from '../utils/navigate.ts';
const SSH_REMOTE_REGEX = /^(?:[^@\s]+@[^:\s]+:.+|ssh:\/\/.+)$/;

const PROSE =
'prose prose-sm prose-invert max-w-none prose-headings:text-zinc-100 prose-p:text-zinc-300 prose-a:text-blue-400 prose-strong:text-zinc-200 prose-code:text-amber-300 prose-code:bg-zinc-900 prose-code:px-1 prose-code:rounded prose-code:before:content-none prose-code:after:content-none prose-pre:bg-zinc-900 prose-pre:border prose-pre:border-zinc-800 prose-li:text-zinc-300';
'prose prose-sm prose-invert max-w-none prose-headings:text-zinc-100 prose-p:text-zinc-300 prose-a:text-cyan-400 prose-strong:text-zinc-200 prose-code:text-amber-300 prose-code:bg-zinc-900 prose-code:px-1 prose-code:rounded prose-code:before:content-none prose-code:after:content-none prose-pre:bg-zinc-900 prose-pre:text-zinc-300 [&_pre_code]:text-zinc-300 prose-pre:border prose-pre:border-zinc-800 prose-li:text-zinc-300';

export default function CreateJob() {
const [title, setTitle] = useState('');
Expand Down Expand Up @@ -76,7 +76,7 @@ export default function CreateJob() {
value={title}
onInput={(e) => setTitle((e.target as HTMLInputElement).value)}
placeholder="Short summary of the work"
class="w-full rounded-lg bg-zinc-900 border border-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-indigo-600"
class="w-full rounded-lg bg-zinc-900 border border-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-cyan-600"
/>
</div>

Expand All @@ -90,7 +90,7 @@ export default function CreateJob() {
onInput={(e) => setRepoUrl((e.target as HTMLInputElement).value)}
placeholder="git@github.com:owner/repo.git — leave empty for a scratch workspace"
class={`w-full rounded-lg bg-zinc-900 border px-3 py-2 text-sm font-mono text-zinc-100 placeholder-zinc-600 focus:outline-none ${
repoInvalid ? 'border-red-700 focus:border-red-600' : 'border-zinc-800 focus:border-indigo-600'
repoInvalid ? 'border-red-700 focus:border-red-600' : 'border-zinc-800 focus:border-cyan-600'
}`}
/>
{repoInvalid && (
Expand Down Expand Up @@ -125,7 +125,7 @@ export default function CreateJob() {
onInput={(e) => setRequirements((e.target as HTMLTextAreaElement).value)}
placeholder="Describe the work like a GitHub issue — context, goals, acceptance criteria…"
rows={14}
class="w-full rounded-lg bg-zinc-900 border border-zinc-800 px-3 py-2 text-sm font-mono text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-indigo-600 resize-y"
class="w-full rounded-lg bg-zinc-900 border border-zinc-800 px-3 py-2 text-sm font-mono text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-cyan-600 resize-y"
/>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/JobCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default function JobCard({ job }: Props) {
{job.prNumber ? (
<a
href={job.prUrl ?? '#'}
class={`text-xs font-mono hover:underline ${job.prIsDraft ? 'text-zinc-500' : 'text-sky-400'}`}
class={`text-xs font-mono hover:underline ${job.prIsDraft ? 'text-zinc-500' : 'text-cyan-400'}`}
onClick={(e) => e.stopPropagation()}
target="_blank"
rel="noopener noreferrer"
Expand Down
16 changes: 8 additions & 8 deletions app/src/components/JobDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export default function JobDetail() {
return (
<div class="flex flex-col items-center justify-center h-full text-zinc-500 gap-3">
<p class="text-base">Job not found</p>
<button class="text-sm text-indigo-400 hover:text-indigo-300" onClick={() => navigate('/')}>
<button class="text-sm text-cyan-400 hover:text-cyan-300" onClick={() => navigate('/')}>
Back to all jobs
</button>
</div>
Expand All @@ -207,7 +207,7 @@ export default function JobDetail() {
if (!job) {
return (
<div class="flex items-center justify-center h-full">
<div class="w-6 h-6 border-2 border-zinc-700 border-t-indigo-500 rounded-full animate-spin" />
<div class="w-6 h-6 border-2 border-zinc-700 border-t-cyan-500 rounded-full animate-spin" />
</div>
);
}
Expand Down Expand Up @@ -250,16 +250,16 @@ export default function JobDetail() {
{(activeRun || !TERMINAL_STATES.has(job.state) || job.state === 'FAILED') && (
<div class="flex items-stretch gap-2">
{activeRun && (
<div class="flex-1 min-w-0 rounded-lg border border-hermes-300/30 bg-hermes-400/10 px-3 py-2.5 flex items-center gap-2.5">
<span class="w-1.5 h-1.5 rounded-full bg-hermes-400 animate-pulse shrink-0" />
<span class="text-xs text-hermes-400 font-mono">{activeRun.phase}</span>
<div class="flex-1 min-w-0 rounded-lg border border-green-500/30 bg-green-500/10 px-3 py-2.5 flex items-center gap-2.5">
<span class="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse shrink-0" />
<span class="text-xs text-green-400 font-mono">{activeRun.phase}</span>
{activeRun.model && (
<span class="text-xs text-zinc-500 truncate hidden sm:block">
{activeRun.model}
</span>
)}
<button
class="ml-auto shrink-0 text-xs font-medium text-hermes-400 hover:text-hermes-300 transition-colors"
class="ml-auto shrink-0 text-xs font-medium text-green-400 hover:text-green-300 transition-colors"
onClick={() => navigate(`/jobs/${id}/runs/${activeRun.id}`)}
>
Watch live →
Expand Down Expand Up @@ -315,7 +315,7 @@ export default function JobDetail() {
{job.prNumber && (
<a
href={job.prUrl ?? '#'}
class={`shrink-0 hover:underline ${job.prIsDraft ? 'text-zinc-500' : 'text-sky-400'}`}
class={`shrink-0 hover:underline ${job.prIsDraft ? 'text-zinc-500' : 'text-cyan-400'}`}
target="_blank"
rel="noopener noreferrer"
>
Expand Down Expand Up @@ -375,7 +375,7 @@ export default function JobDetail() {
onInput={(e) => setChangesNote((e.target as HTMLTextAreaElement).value)}
placeholder="Request changes (Markdown) — Hermes will revise and push an update…"
rows={3}
class="w-full rounded-lg bg-zinc-900 border border-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-indigo-600 resize-y"
class="w-full rounded-lg bg-zinc-900 border border-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-cyan-600 resize-y"
/>
<button
disabled={actionPending || changesNote.trim().length === 0}
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/JobList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function MobileJobCard({ job }: { job: JobSummaryDto }) {
</span>
)}
{job.prNumber && (
<span class={job.prIsDraft ? 'text-zinc-600' : 'text-sky-400'}>PR #{job.prNumber}</span>
<span class={job.prIsDraft ? 'text-zinc-600' : 'text-cyan-400'}>PR #{job.prNumber}</span>
)}
{job.confidence !== null && (
<span
Expand Down
8 changes: 4 additions & 4 deletions app/src/components/JudgeOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function JudgeOutput() {
<div class="flex flex-col items-center justify-center h-full text-zinc-500 gap-3">
<p class="text-base">Judgement not found</p>
<button
class="text-sm text-indigo-400 hover:text-indigo-300"
class="text-sm text-cyan-400 hover:text-cyan-300"
onClick={() => navigate(`/jobs/${jobId}`)}
>
Back to job
Expand All @@ -48,7 +48,7 @@ export default function JudgeOutput() {
if (!judgement) {
return (
<div class="flex items-center justify-center h-full">
<div class="w-6 h-6 border-2 border-zinc-700 border-t-indigo-500 rounded-full animate-spin" />
<div class="w-6 h-6 border-2 border-zinc-700 border-t-cyan-500 rounded-full animate-spin" />
</div>
);
}
Expand All @@ -66,7 +66,7 @@ export default function JudgeOutput() {
← Back
</button>
<span class="text-zinc-700 text-sm">/</span>
<span class="font-mono text-xs px-2 py-0.5 rounded shrink-0 bg-pink-900/60 text-pink-300">
<span class="font-mono text-xs px-2 py-0.5 rounded shrink-0 bg-zinc-800 text-zinc-300">
JUDGE
</span>
<span
Expand All @@ -80,7 +80,7 @@ export default function JudgeOutput() {
<div class="max-w-4xl mx-auto px-4 py-5 space-y-4">
{html ? (
<div
class="prose prose-sm prose-invert max-w-none prose-headings:text-zinc-100 prose-p:text-zinc-300 prose-li:text-zinc-300 prose-strong:text-zinc-200 prose-code:text-amber-300 prose-code:bg-zinc-900 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none prose-pre:bg-zinc-900 prose-pre:border prose-pre:border-zinc-800"
class="prose prose-sm prose-invert max-w-none prose-headings:text-zinc-100 prose-p:text-zinc-300 prose-li:text-zinc-300 prose-strong:text-zinc-200 prose-code:text-amber-300 prose-code:bg-zinc-900 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none prose-pre:bg-zinc-900 prose-pre:text-zinc-300 [&_pre_code]:text-zinc-300 prose-pre:border prose-pre:border-zinc-800"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: html }}
/>
Expand Down
4 changes: 2 additions & 2 deletions app/src/components/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ const PROSE =
'prose prose-sm prose-invert max-w-none ' +
'prose-headings:font-semibold prose-headings:text-zinc-100 ' +
'prose-p:text-zinc-300 prose-p:leading-relaxed ' +
'prose-a:text-blue-400 hover:prose-a:text-blue-300 ' +
'prose-a:text-cyan-400 hover:prose-a:text-cyan-300 ' +
'prose-strong:text-zinc-200 ' +
'prose-code:text-amber-300 prose-code:bg-zinc-900 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-[0.8em] prose-code:before:content-none prose-code:after:content-none ' +
'prose-pre:bg-zinc-900 prose-pre:border prose-pre:border-zinc-800 ' +
'prose-pre:bg-zinc-900 prose-pre:text-zinc-300 [&_pre_code]:text-zinc-300 prose-pre:border prose-pre:border-zinc-800 ' +
'prose-blockquote:border-zinc-700 prose-blockquote:text-zinc-400 ' +
'prose-hr:border-zinc-800 prose-li:text-zinc-300';

Expand Down
4 changes: 2 additions & 2 deletions app/src/components/PlanThread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default function PlanThread({
? 'bg-green-900/50 text-green-400'
: item.plan.status === 'SUPERSEDED'
? 'bg-zinc-800 text-zinc-500'
: 'bg-indigo-900/40 text-indigo-300'
: 'bg-cyan-900/40 text-cyan-300'
}`}
>
{item.plan.status}
Expand Down Expand Up @@ -89,7 +89,7 @@ export default function PlanThread({
onInput={(e) => setFeedback((e.target as HTMLTextAreaElement).value)}
placeholder="Request changes to the plan (Markdown) — Hermes will revise and re-propose…"
rows={4}
class="w-full rounded-lg bg-zinc-900 border border-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-indigo-600 resize-y"
class="w-full rounded-lg bg-zinc-900 border border-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-cyan-600 resize-y"
/>
<button
disabled={pending || feedback.trim().length === 0}
Expand Down
4 changes: 2 additions & 2 deletions app/src/components/RepoControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default function RepoControl({
setValue(repoUrl ?? '');
setEditing(true);
}}
class="text-indigo-400 hover:text-indigo-300 transition-colors"
class="text-cyan-400 hover:text-cyan-300 transition-colors"
>
{repoUrl ? 'change' : 'set'}
</button>
Expand All @@ -81,7 +81,7 @@ export default function RepoControl({
onInput={(e) => setValue((e.target as HTMLInputElement).value)}
placeholder="git@github.com:owner/repo.git (empty = scratch)"
class={`flex-1 rounded-lg bg-zinc-900 border px-2.5 py-1.5 text-xs font-mono text-zinc-100 placeholder-zinc-600 focus:outline-none ${
invalid ? 'border-red-700' : 'border-zinc-800 focus:border-indigo-600'
invalid ? 'border-red-700' : 'border-zinc-800 focus:border-cyan-600'
}`}
/>
<button
Expand Down
4 changes: 2 additions & 2 deletions app/src/components/ReviewPassCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ interface Props {
const SEVERITY_STYLES = {
low: 'bg-zinc-700 text-zinc-300',
medium: 'bg-amber-900 text-amber-300',
high: 'bg-orange-900 text-orange-300',
critical: 'bg-red-900 text-red-300',
high: 'bg-red-950 text-red-300',
critical: 'bg-red-900 text-red-200',
};

export default function ReviewPassCard({ pass }: Props) {
Expand Down
Loading
Loading