diff --git a/package.json b/package.json
index 51418cf0e..c0058238c 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"packages/website"
],
"scripts": {
- "dev": "npm run dev --workspace=@paseo/server",
+ "dev": "NODE_ENV=development tsx packages/server/scripts/dev-runner.ts",
"dev:app": "npm run start --workspace=@paseo/app",
"dev:website": "npm run dev --workspace=@paseo/website",
"build": "npm run build --workspaces --if-present",
diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx
index f94405100..46270edd9 100644
--- a/packages/app/src/components/agent-list.tsx
+++ b/packages/app/src/components/agent-list.tsx
@@ -1,11 +1,22 @@
-import { View, Text, Pressable, Modal, RefreshControl, FlatList, type ListRenderItem } from "react-native";
+import {
+ View,
+ Text,
+ Pressable,
+ Modal,
+ RefreshControl,
+ FlatList,
+ type ListRenderItem,
+} from "react-native";
import { useCallback, useState, type ReactElement } from "react";
import { router, usePathname } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { formatTimeAgo } from "@/utils/time";
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
import { useSessionStore } from "@/stores/session-store";
-import { buildAgentNavigationKey, startNavigationTiming } from "@/utils/navigation-timing";
+import {
+ buildAgentNavigationKey,
+ startNavigationTiming,
+} from "@/utils/navigation-timing";
interface AgentListProps {
agents: AggregatedAgent[];
@@ -30,7 +41,9 @@ export function AgentList({
// Get the methods for the specific server
const methods = useSessionStore((state) =>
- actionAgent?.serverId ? state.sessions[actionAgent.serverId]?.methods : undefined
+ actionAgent?.serverId
+ ? state.sessions[actionAgent.serverId]?.methods
+ : undefined
);
const deleteAgent = methods?.deleteAgent;
@@ -94,7 +107,11 @@ export function AgentList({
const isRunning = agent.status === "running";
const agentKey = `${agent.serverId}:${agent.id}`;
const isSelected = selectedAgentId === agentKey;
- const statusColor = isRunning ? theme.colors.palette.blue[500] : agent.requiresAttention ? theme.colors.success : null;
+ const statusColor = isRunning
+ ? theme.colors.palette.blue[500]
+ : agent.requiresAttention
+ ? theme.colors.success
+ : null;
return (
handleAgentLongPress(agent)}
>
{({ hovered }) => (
-
-
- {statusColor && (
-
- )}
-
- {agent.title || "New Agent"}
+
+
+ {statusColor && (
+
+ )}
+
+ {agent.title || "New Agent"}
+
+
+
+
+ {agent.cwd.replace(/^\/(?:Users|home)\/[^/]+/, "~")} · {timeAgo}
-
-
- {agent.cwd.replace(/^\/(?:Users|home)\/[^/]+/, "~")} · {timeAgo}
-
-
)}
);
@@ -176,7 +195,10 @@ export function AgentList({
onRequestClose={handleCloseActionSheet}
>
-
+
@@ -195,7 +217,8 @@ export function AgentList({
{isActionDaemonUnavailable ? "Host offline" : "Delete agent"}
@@ -246,7 +269,7 @@ const styles = StyleSheet.create((theme) => ({
row: {
flexDirection: "row",
alignItems: "center",
- gap: theme.spacing[2],
+ gap: theme.spacing[0],
},
agentTitle: {
flex: 1,
@@ -255,10 +278,12 @@ const styles = StyleSheet.create((theme) => ({
xs: "400",
md: "300",
},
- color: theme.colors.foregroundMuted,
+ color: theme.colors.foreground,
+ opacity: 0.8,
},
agentTitleHighlighted: {
color: theme.colors.foreground,
+ opacity: 1,
},
secondaryRow: {
fontSize: theme.fontSize.sm,
diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx
index 55544264b..17b68c2f7 100644
--- a/packages/app/src/components/agent-stream-view.tsx
+++ b/packages/app/src/components/agent-stream-view.tsx
@@ -373,8 +373,6 @@ export function AgentStreamView({
return null;
}
- const gap = getGapAbove(item, index);
-
// Check if this is the end of a turn (before a user message or end of stream when not running)
// In inverted list: index-1 is the next item (newer in time)
const nextItem = flatListData[index - 1];
@@ -386,18 +384,19 @@ export function AgentStreamView({
const getContent = () => collectTurnContent(index);
return (
-
+
{content}
{isEndOfTurn ? : null}
);
},
- [getGapAbove, renderStreamItemContent, flatListData, agent.status, collectTurnContent]
+ [
+ getGapAbove,
+ renderStreamItemContent,
+ flatListData,
+ agent.status,
+ collectTurnContent,
+ ]
);
const pendingPermissionItems = useMemo(
@@ -419,7 +418,10 @@ export function AgentStreamView({
}
const delta = Math.abs(totalCount - prevCount);
streamItemCountRef.current = totalCount;
- if (totalCount < STREAM_ITEM_LOG_MIN_COUNT && delta < STREAM_ITEM_LOG_DELTA_THRESHOLD) {
+ if (
+ totalCount < STREAM_ITEM_LOG_MIN_COUNT &&
+ delta < STREAM_ITEM_LOG_DELTA_THRESHOLD
+ ) {
return;
}
let userCount = 0;
@@ -452,7 +454,10 @@ export function AgentStreamView({
break;
}
}
- const metrics = totalCount >= STREAM_ITEM_LOG_MIN_COUNT ? measurePayload(streamItems) : null;
+ const metrics =
+ totalCount >= STREAM_ITEM_LOG_MIN_COUNT
+ ? measurePayload(streamItems)
+ : null;
perfLog(AGENT_STREAM_LOG_TAG, {
event: "stream_items",
agentId,
@@ -945,11 +950,10 @@ function PermissionRequestCard({
const handleResponse = useCallback(
(response: AgentPermissionResponse) => {
respondToPermission({
- agentId: permission.agentId,
- requestId: permission.request.id,
- response,
- })
- .catch((error) => {
+ agentId: permission.agentId,
+ requestId: permission.request.id,
+ response,
+ }).catch((error) => {
console.error(
"[PermissionRequestCard] Failed to respond to permission:",
error
diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx
index 635439e12..d0c974fc7 100644
--- a/packages/app/src/components/message.tsx
+++ b/packages/app/src/components/message.tsx
@@ -1,4 +1,11 @@
-import { View, Text, Pressable, Animated } from "react-native";
+import {
+ View,
+ Text,
+ Pressable,
+ Animated,
+ StyleProp,
+ ViewStyle,
+} from "react-native";
import {
useState,
useEffect,
@@ -31,6 +38,7 @@ import {
Search,
Brain,
Copy,
+ TriangleAlertIcon,
} from "lucide-react-native";
import {
StyleSheet,
@@ -173,14 +181,11 @@ export const UserMessage = memo(function UserMessage({
({
label: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
- fontWeight: theme.fontWeight.medium,
+ fontWeight: theme.fontWeight.normal,
flexShrink: 0,
},
secondaryLabel: {
@@ -804,8 +809,7 @@ export const ActivityLog = memo(function ActivityLog({
disabled={!isInteractive}
style={[
activityLogStylesheet.pressable,
- !resolvedDisableOuterSpacing &&
- activityLogStylesheet.pressableSpacing,
+ !resolvedDisableOuterSpacing && activityLogStylesheet.pressableSpacing,
config.bg,
isInteractive && activityLogStylesheet.pressableActive,
]}
@@ -985,8 +989,7 @@ export const TodoListCard = memo(function TodoListCard({
@@ -1056,6 +1059,7 @@ interface ExpandableBadgeProps {
secondaryLabel?: string;
icon?: ComponentType<{ size?: number; color?: string }>;
isExpanded: boolean;
+ style?: StyleProp;
onToggle?: () => void;
renderDetails?: () => ReactNode;
isLoading?: boolean;
@@ -1066,6 +1070,7 @@ interface ExpandableBadgeProps {
const ExpandableBadge = memo(function ExpandableBadge({
label,
+ style,
secondaryLabel,
icon,
isExpanded,
@@ -1118,7 +1123,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
const IconComponent = icon;
const iconColor = isError
? theme.colors.destructive
- : theme.colors.foreground;
+ : theme.colors.mutedForeground;
let iconNode: ReactNode = null;
if (isLoading) {
@@ -1128,22 +1133,13 @@ const ExpandableBadge = memo(function ExpandableBadge({
);
} else if (isError) {
- iconNode = ;
+ iconNode = ;
} else if (IconComponent) {
iconNode = ;
}
return (
-
+
null
- : undefined
+ ? () => null
+ : undefined
}
isLoading={status === "executing"}
isError={status === "failed"}
isLastInSequence={isLastInSequence}
+ style={isLastInSequence ? undefined : { marginBottom: theme.spacing[1] }}
disableOuterSpacing={disableOuterSpacing}
/>
);
diff --git a/packages/app/src/hooks/use-favicon-status.ts b/packages/app/src/hooks/use-favicon-status.ts
index a1dec97d2..cb7497fe7 100644
--- a/packages/app/src/hooks/use-favicon-status.ts
+++ b/packages/app/src/hooks/use-favicon-status.ts
@@ -67,7 +67,9 @@ function updateFavicon(status: FaviconStatus, colorScheme: ColorScheme) {
}
function getSystemColorScheme(): ColorScheme {
- if (typeof window === "undefined") return "dark";
+ if (Platform.OS !== "web" || typeof window === "undefined" || typeof window.matchMedia !== "function") {
+ return "dark";
+ }
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
diff --git a/packages/app/src/styles/unistyles.ts b/packages/app/src/styles/unistyles.ts
index 648fe73d6..c8595ad2d 100644
--- a/packages/app/src/styles/unistyles.ts
+++ b/packages/app/src/styles/unistyles.ts
@@ -18,7 +18,7 @@ StyleSheet.configure({
xl: 1200,
},
settings: {
- initialTheme: "dark",
+ adaptiveThemes: true,
},
});
diff --git a/packages/server/scripts/dev-runner.ts b/packages/server/scripts/dev-runner.ts
index b2a4f1464..4ac81edf3 100644
--- a/packages/server/scripts/dev-runner.ts
+++ b/packages/server/scripts/dev-runner.ts
@@ -10,7 +10,7 @@ let child: ChildProcess | null = null;
let restarting = false;
function spawnServer() {
- child = fork(serverEntry, {
+ child = fork(serverEntry, process.argv.slice(2), {
stdio: "inherit",
env: process.env,
execArgv: ["--import", "tsx"],
diff --git a/packages/server/src/client/daemon-client-v2.ts b/packages/server/src/client/daemon-client-v2.ts
index 4e6f05246..bfaaa87dc 100644
--- a/packages/server/src/client/daemon-client-v2.ts
+++ b/packages/server/src/client/daemon-client-v2.ts
@@ -1408,6 +1408,8 @@ export class DaemonClientV2 {
const parsed = WSOutboundMessageSchema.safeParse(parsedJson);
if (!parsed.success) {
+ const msgType = (parsedJson as { message?: { type?: string } })?.message?.type ?? "unknown";
+ console.warn(`[DaemonClientV2] Message validation failed for "${msgType}":`, parsed.error.message);
return;
}
diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts
index 9c546ba9f..cd4f7c0f7 100644
--- a/packages/server/src/server/agent/agent-manager.ts
+++ b/packages/server/src/server/agent/agent-manager.ts
@@ -879,7 +879,10 @@ export class AgentManager {
event: AgentStreamEvent,
options?: { fromHistory?: boolean }
): void {
- agent.updatedAt = new Date();
+ // Only update timestamp for live events, not history replay
+ if (!options?.fromHistory) {
+ agent.updatedAt = new Date();
+ }
switch (event.type) {
case "thread_started":
diff --git a/packages/server/src/server/index.ts b/packages/server/src/server/index.ts
index 0c2522c89..3f35d34a2 100644
--- a/packages/server/src/server/index.ts
+++ b/packages/server/src/server/index.ts
@@ -1,4 +1,9 @@
-import "dotenv/config";
+import dotenv from "dotenv";
+import { fileURLToPath } from "node:url";
+import path from "node:path";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+dotenv.config({ path: path.resolve(__dirname, "../../.env") });
import { createPaseoDaemon } from "./bootstrap.js";
import { loadConfig } from "./config.js";
import { resolvePaseoHome } from "./paseo-home.js";
diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts
index 386df5374..f8b67721b 100644
--- a/packages/server/src/shared/messages.ts
+++ b/packages/server/src/shared/messages.ts
@@ -430,14 +430,10 @@ export const GitDiffRequestSchema = z.object({
});
// Highlighted diff token schema
+// Note: style can be a compound class name (e.g., "heading meta") from the syntax highlighter
const HighlightTokenSchema = z.object({
text: z.string(),
- style: z.enum([
- "keyword", "comment", "string", "number", "literal",
- "function", "definition", "class", "type", "tag",
- "attribute", "property", "variable", "operator",
- "punctuation", "regexp", "escape", "meta", "heading", "link",
- ]).nullable(),
+ style: z.string().nullable(),
});
const DiffLineSchema = z.object({
diff --git a/packages/server/src/tasks/cli.ts b/packages/server/src/tasks/cli.ts
index 2de34dbcf..a1e8feb62 100644
--- a/packages/server/src/tasks/cli.ts
+++ b/packages/server/src/tasks/cli.ts
@@ -468,6 +468,8 @@ function runAgentWithModel(
"exec",
"--dangerously-bypass-approvals-and-sandbox",
"--skip-git-repo-check",
+ "-c",
+ "model_reasoning_effort=\"medium\"",
];
if (config.model) {
args.push("--model", config.model);
@@ -540,11 +542,28 @@ function makePlannerPrompt(task: Task, context: string, iteration: number): stri
---
-You are a PLANNER agent. Your job is to analyze the task tree and organize work to achieve the top-level goal.
+You are a PLANNER agent. You ORCHESTRATE work - you do NOT do the work yourself.
+
+Your job is to organize tasks so that WORKER agents can accomplish the top-level acceptance criteria.
Current iteration: ${iteration}
-You have FULL ACCESS to the task CLI and can reorganize the ENTIRE task tree under the scope:
+## Planning vs Working
+
+As a planner you MAY:
+- Look at file structure and test patterns to write good acceptance criteria
+- Read the task tree to understand what's been done and what needs reorganizing
+- Check naming conventions to make specific, verifiable criteria
+
+As a planner you must NOT:
+- Investigate WHY bugs happen (workers will do this)
+- Debug or trace through code (workers will do this)
+- Try to understand root causes (workers will do this)
+- Fix or implement anything (workers will do this)
+
+Take the task descriptions AS GIVEN and create well-organized subtasks for workers to investigate and implement.
+
+## Task CLI Commands
- \`task show ${task.id}\` - view task details with parent context
- \`task list\` - see all tasks
- \`task tree ${task.id}\` - see full task hierarchy
@@ -694,18 +713,17 @@ The ONLY question: Does the acceptance criterion pass when verified? Yes or No.
If an agent left notes explaining why something couldn't be done, IGNORE THE EXPLANATION. Just check: is the criterion met?
-## Output Format
+## Output Format - CRITICAL
-After verification, output EXACTLY one of these XML tags:
+You MUST output one of these XML tags at the END of your response. This is REQUIRED for the system to parse your verdict:
-If ALL criteria pass verification:
+If ALL criteria pass:
DONE
-If ANY criterion fails verification:
+If ANY criterion fails:
NOT_DONE
-
-- criterion that failed: what you checked and what you found
-
+
+If you do not include this exact XML tag, your verdict will not be recorded and the task will retry.
Then add a note to the task with your findings:
\`task note ${task.id} "Judge verdict: [DONE/NOT_DONE]. Details: ..."\`
@@ -785,10 +803,42 @@ program
};
const runTaskLoop = async (): Promise => {
+ // First check for in_progress tasks (resuming from crash)
+ const allTasks = await store.list();
+ let candidates = scopeId
+ ? [await store.get(scopeId), ...(await store.getDescendants(scopeId))].filter(Boolean) as Task[]
+ : allTasks;
+
+ const inProgress = candidates.filter((t) => t.status === "in_progress");
+ if (inProgress.length > 0) {
+ log(logFile, `Found ${inProgress.length} in_progress task(s) from previous run, resuming...`);
+ for (const t of inProgress) {
+ await store.update(t.id, { status: "open" });
+ log(logFile, `Reset ${t.id} to open`);
+ }
+ }
+
+ // Step 1: Initial planner run on scope root (if enabled)
+ if (enablePlanner && scopeId) {
+ const scopeTask = await store.get(scopeId);
+ if (scopeTask) {
+ log(logFile, `[PLANNER] Initial planning on scope root...`);
+ await runPlanner(scopeTask, "initial planning");
+ log(logFile, `[DEBUG] Planner finished, continuing to worker loop`);
+ }
+ }
+
+ log(logFile, `[DEBUG] Entering worker loop`);
+
+ // Step 2: Worker/Judge loop on ready tasks
while (true) {
- // Find ready tasks
+ log(logFile, `[DEBUG] Checking for ready tasks...`);
const ready = await store.getReady(scopeId);
- if (ready.length === 0) break;
+ log(logFile, `[DEBUG] Found ${ready.length} ready tasks`);
+ if (ready.length === 0) {
+ log(logFile, `[DEBUG] No ready tasks, exiting loop`);
+ break;
+ }
const task = ready[0];
const workerModel = (task.assignee === "codex" ? "gpt-5.2" : "sonnet") as ModelName;
@@ -796,20 +846,7 @@ program
log(logFile, `\n=== Starting task: ${task.id} - ${task.title} ===`);
await store.start(task.id);
- // Step 1: Initial planner run (if enabled)
- if (enablePlanner) {
- const shouldRestart = await runPlanner(task, "initial planning");
- if (shouldRestart) {
- // Reset task to open so we process children first
- const currentTask = await store.get(task.id);
- if (currentTask && currentTask.status === "in_progress") {
- await store.update(task.id, { status: "open" });
- }
- continue;
- }
- }
-
- // Step 2: Worker/Judge loop
+ // Worker/Judge loop
let iteration = 1;
let taskDone = false;
@@ -852,6 +889,7 @@ program
const scopeTask = scopeId ? await store.get(scopeId) : nextReady[0];
if (scopeTask) {
await runPlanner(scopeTask, "periodic reassessment");
+ log(logFile, `[DEBUG] Periodic replan finished, continuing loop`);
}
}
}