Merge origin/main into show-image-previews-in-optimistic-messages

This commit is contained in:
Mohamed Boudra
2026-02-18 10:02:46 +07:00
159 changed files with 10110 additions and 1480 deletions
@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import { shouldClearAgentAttentionOnView } from "./agent-attention";
describe("shouldClearAgentAttentionOnView", () => {
it("returns true only when the viewed agent is focused, connected, and requires attention", () => {
expect(
shouldClearAgentAttentionOnView({
agentId: "agent-1",
focusedAgentId: "agent-1",
isConnected: true,
requiresAttention: true,
})
).toBe(true);
});
it("returns false when the app is disconnected", () => {
expect(
shouldClearAgentAttentionOnView({
agentId: "agent-1",
focusedAgentId: "agent-1",
isConnected: false,
requiresAttention: true,
})
).toBe(false);
});
it("returns false when the agent is not focused", () => {
expect(
shouldClearAgentAttentionOnView({
agentId: "agent-1",
focusedAgentId: "agent-2",
isConnected: true,
requiresAttention: true,
})
).toBe(false);
});
it("returns false when attention is already clear", () => {
expect(
shouldClearAgentAttentionOnView({
agentId: "agent-1",
focusedAgentId: "agent-1",
isConnected: true,
requiresAttention: false,
})
).toBe(false);
});
it("returns false for empty agent ids", () => {
expect(
shouldClearAgentAttentionOnView({
agentId: "",
focusedAgentId: "agent-1",
isConnected: true,
requiresAttention: true,
})
).toBe(false);
});
});
+22
View File
@@ -0,0 +1,22 @@
interface ShouldClearAgentAttentionOnViewInput {
agentId: string | null | undefined;
focusedAgentId: string | null | undefined;
isConnected: boolean;
requiresAttention: boolean | null | undefined;
}
export function shouldClearAgentAttentionOnView(
input: ShouldClearAgentAttentionOnViewInput
): boolean {
const agentId = input.agentId?.trim();
if (!agentId) {
return false;
}
if (!input.isConnected) {
return false;
}
if (!input.requiresAttention) {
return false;
}
return input.focusedAgentId === agentId;
}
@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import { collectAgentWorkingDirectorySuggestions } from "@/utils/agent-working-directory-suggestions";
describe("collectAgentWorkingDirectorySuggestions", () => {
it("deduplicates by cwd and sorts by most recent timestamp", () => {
const results = collectAgentWorkingDirectorySuggestions([
{
cwd: "/Users/me/project-alpha",
createdAt: new Date("2026-02-10T10:00:00.000Z"),
},
{
cwd: "/Users/me/project-beta",
createdAt: new Date("2026-02-11T10:00:00.000Z"),
},
{
cwd: "/Users/me/project-alpha",
lastActivityAt: new Date("2026-02-12T10:00:00.000Z"),
},
]);
expect(results).toEqual([
"/Users/me/project-alpha",
"/Users/me/project-beta",
]);
});
it("excludes Paseo-owned worktree paths", () => {
const results = collectAgentWorkingDirectorySuggestions([
{
cwd: "/Users/me/repo/.paseo/worktrees/feature-a",
createdAt: new Date("2026-02-12T10:00:00.000Z"),
},
{
cwd: "/Users/me/repo",
createdAt: new Date("2026-02-10T10:00:00.000Z"),
},
{
cwd: "C:\\Users\\me\\repo\\.paseo\\worktrees\\feature-b",
createdAt: new Date("2026-02-11T10:00:00.000Z"),
},
]);
expect(results).toEqual(["/Users/me/repo"]);
});
it("ignores empty cwd values", () => {
const results = collectAgentWorkingDirectorySuggestions([
{ cwd: " ", createdAt: new Date("2026-02-10T10:00:00.000Z") },
{ cwd: null, createdAt: new Date("2026-02-11T10:00:00.000Z") },
{ cwd: undefined, lastActivityAt: new Date("2026-02-12T10:00:00.000Z") },
{
cwd: "/Users/me/project",
createdAt: new Date("2026-02-09T10:00:00.000Z"),
},
]);
expect(results).toEqual(["/Users/me/project"]);
});
});
@@ -0,0 +1,51 @@
export interface AgentWorkingDirectorySource {
cwd?: string | null;
createdAt?: Date | null;
lastActivityAt?: Date | null;
}
const PASEO_WORKTREE_PATH_PATTERN = /(^|\/)\.paseo\/worktrees(\/|$)/;
export function collectAgentWorkingDirectorySuggestions(
sources: Iterable<AgentWorkingDirectorySource>
): string[] {
const lastSeenByPath = new Map<string, number>();
for (const source of sources) {
const cwd = source.cwd?.trim();
if (!cwd) {
continue;
}
if (isPaseoOwnedWorktreePath(cwd)) {
continue;
}
const timestamp = toEpochMs(source.lastActivityAt ?? source.createdAt);
const previous = lastSeenByPath.get(cwd);
if (previous === undefined || timestamp > previous) {
lastSeenByPath.set(cwd, timestamp);
}
}
return Array.from(lastSeenByPath.entries())
.sort((left, right) => {
const timeDiff = right[1] - left[1];
if (timeDiff !== 0) {
return timeDiff;
}
return left[0].localeCompare(right[0]);
})
.map(([cwd]) => cwd);
}
function isPaseoOwnedWorktreePath(cwd: string): boolean {
return PASEO_WORKTREE_PATH_PATTERN.test(cwd.replace(/\\/g, "/"));
}
function toEpochMs(date: Date | null | undefined): number {
if (!(date instanceof Date)) {
return 0;
}
const value = date.getTime();
return Number.isFinite(value) ? value : 0;
}
@@ -0,0 +1,116 @@
import { afterEach, describe, expect, it, vi } from "vitest";
type MockPlatform = "web" | "ios" | "android";
type AlertButton = {
onPress?: () => void;
};
async function loadModuleForPlatform(platform: MockPlatform): Promise<{
confirmDialog: typeof import("./confirm-dialog").confirmDialog;
alertMock: ReturnType<typeof vi.fn>;
}> {
vi.resetModules();
const alertMock = vi.fn();
vi.doMock("react-native", () => ({
Alert: {
alert: alertMock,
},
Platform: { OS: platform },
}));
const module = await import("./confirm-dialog");
return { confirmDialog: module.confirmDialog, alertMock };
}
function clearDialogGlobals(): void {
delete (globalThis as { __TAURI__?: unknown }).__TAURI__;
delete (globalThis as { confirm?: unknown }).confirm;
}
describe("confirmDialog", () => {
afterEach(() => {
vi.doUnmock("react-native");
vi.restoreAllMocks();
vi.resetModules();
clearDialogGlobals();
});
it("uses Tauri dialog.ask on web when available", async () => {
const askMock = vi.fn(async () => true);
(globalThis as { __TAURI__?: unknown }).__TAURI__ = {
dialog: { ask: askMock },
};
const { confirmDialog, alertMock } = await loadModuleForPlatform("web");
const confirmed = await confirmDialog({
title: "Restart host",
message: "This will restart the daemon.",
confirmLabel: "Restart",
cancelLabel: "Cancel",
destructive: true,
});
expect(confirmed).toBe(true);
expect(alertMock).not.toHaveBeenCalled();
expect(askMock).toHaveBeenCalledWith("This will restart the daemon.", {
title: "Restart host",
okLabel: "Restart",
cancelLabel: "Cancel",
kind: "warning",
});
});
it("falls back to browser confirm on web when Tauri APIs are unavailable", async () => {
const browserConfirm = vi.fn(() => true);
(globalThis as { confirm?: unknown }).confirm = browserConfirm;
const { confirmDialog } = await loadModuleForPlatform("web");
const confirmed = await confirmDialog({
title: "Restart host",
message: "This will restart the daemon.",
});
expect(confirmed).toBe(true);
expect(browserConfirm).toHaveBeenCalledWith(
"Restart host\n\nThis will restart the daemon."
);
});
it("throws on web when no confirm backend exists", async () => {
const { confirmDialog } = await loadModuleForPlatform("web");
await expect(
confirmDialog({
title: "Restart host",
message: "This will restart the daemon.",
})
).rejects.toThrow("[ConfirmDialog] No web confirmation backend is available.");
});
it("uses native Alert on iOS/Android", async () => {
const { confirmDialog, alertMock } = await loadModuleForPlatform("ios");
alertMock.mockImplementation(
(
_title: string,
_message: string,
buttons?: AlertButton[]
) => {
const confirmButton = buttons?.[1];
confirmButton?.onPress?.();
}
);
const confirmed = await confirmDialog({
title: "Restart host",
message: "This will restart the daemon.",
confirmLabel: "Restart",
cancelLabel: "Cancel",
destructive: true,
});
expect(confirmed).toBe(true);
expect(alertMock).toHaveBeenCalled();
});
});
+144
View File
@@ -0,0 +1,144 @@
import { Alert, Platform } from "react-native";
export interface ConfirmDialogInput {
title: string;
message: string;
confirmLabel?: string;
cancelLabel?: string;
destructive?: boolean;
}
interface TauriDialogAskOptions {
title?: string;
okLabel?: string;
cancelLabel?: string;
kind?: "info" | "warning" | "error";
}
interface TauriDialogApi {
ask?: (message: string, options?: TauriDialogAskOptions) => Promise<boolean>;
}
interface TauriCoreApi {
invoke?: (command: string, args?: Record<string, unknown>) => Promise<unknown>;
}
interface TauriGlobalApi {
dialog?: TauriDialogApi;
core?: TauriCoreApi;
}
interface ConfirmButtonConfig {
confirmLabel: string;
cancelLabel: string;
}
function resolveButtonLabels(input: ConfirmDialogInput): ConfirmButtonConfig {
return {
confirmLabel: input.confirmLabel ?? "Confirm",
cancelLabel: input.cancelLabel ?? "Cancel",
};
}
async function showNativeConfirmDialog(input: ConfirmDialogInput): Promise<boolean> {
const labels = resolveButtonLabels(input);
return new Promise<boolean>((resolve) => {
Alert.alert(
input.title,
input.message,
[
{
text: labels.cancelLabel,
style: "cancel",
onPress: () => resolve(false),
},
{
text: labels.confirmLabel,
style: input.destructive ? "destructive" : "default",
onPress: () => resolve(true),
},
],
{
cancelable: true,
onDismiss: () => resolve(false),
}
);
});
}
function getTauriApi(): TauriGlobalApi | null {
if (Platform.OS !== "web") {
return null;
}
const tauriApi = (globalThis as { __TAURI__?: TauriGlobalApi }).__TAURI__;
return tauriApi ?? null;
}
function buildTauriAskOptions(input: ConfirmDialogInput): TauriDialogAskOptions {
const labels = resolveButtonLabels(input);
return {
title: input.title,
okLabel: labels.confirmLabel,
cancelLabel: labels.cancelLabel,
kind: input.destructive ? "warning" : "info",
};
}
async function showTauriConfirmDialog(input: ConfirmDialogInput): Promise<boolean | null> {
const tauriApi = getTauriApi();
if (!tauriApi) {
return null;
}
const options = buildTauriAskOptions(input);
const tauriAsk = tauriApi.dialog?.ask;
if (typeof tauriAsk === "function") {
try {
return Boolean(await tauriAsk(input.message, options));
} catch (error) {
console.warn("[ConfirmDialog] Tauri dialog.ask failed", error);
}
}
const tauriInvoke = tauriApi.core?.invoke;
if (typeof tauriInvoke === "function") {
try {
const result = await tauriInvoke("plugin:dialog|ask", {
message: input.message,
...options,
});
return result === true;
} catch (error) {
console.warn("[ConfirmDialog] Tauri plugin:dialog|ask failed", error);
}
}
return null;
}
function showWebConfirmDialog(input: ConfirmDialogInput): boolean {
const browserConfirm = (globalThis as { confirm?: (message?: string) => boolean }).confirm;
if (typeof browserConfirm !== "function") {
throw new Error("[ConfirmDialog] No web confirmation backend is available.");
}
const promptMessage = `${input.title}\n\n${input.message}`;
return browserConfirm(promptMessage);
}
export async function confirmDialog(input: ConfirmDialogInput): Promise<boolean> {
if (Platform.OS !== "web") {
return showNativeConfirmDialog(input);
}
const tauriResult = await showTauriConfirmDialog(input);
if (tauriResult !== null) {
return tauriResult;
}
return showWebConfirmDialog(input);
}
@@ -0,0 +1,28 @@
import * as Linking from "expo-linking";
import { Platform } from "react-native";
import { getIsTauri } from "@/constants/layout";
interface TauriWindowWithOpener {
__TAURI__?: {
opener?: {
openUrl?: (url: string) => Promise<void>;
};
};
}
export async function openExternalUrl(url: string): Promise<void> {
if (Platform.OS === "web") {
if (typeof window !== "undefined" && getIsTauri()) {
const opener = (window as TauriWindowWithOpener).__TAURI__?.opener?.openUrl;
if (typeof opener === "function") {
await opener(url);
return;
}
}
window.open(url, "_blank", "noopener,noreferrer");
return;
}
await Linking.openURL(url);
}
@@ -5,6 +5,7 @@ import { parser as cssParser } from "@lezer/css";
import { parser as htmlParser } from "@lezer/html";
import { parser as pythonParser } from "@lezer/python";
import { parser as markdownParser } from "@lezer/markdown";
import { parser as elixirParser } from "lezer-elixir";
import type { Parser } from "@lezer/common";
// Map file extensions to parsers
@@ -26,6 +27,9 @@ const parsersByExtension: Record<string, Parser> = {
htm: htmlParser,
// Python
py: pythonParser,
// Elixir
ex: elixirParser,
exs: elixirParser,
// Markdown
md: markdownParser,
mdx: markdownParser,
+27 -4
View File
@@ -109,7 +109,21 @@ export function useTrafficLightPadding(): { left: number; top: number } {
useEffect(() => {
if (Platform.OS !== "web" || !getIsTauriMac()) return;
let unlisten: (() => void) | undefined;
let disposed = false;
let cleanup: (() => void) | undefined;
let didCleanup = false;
function runCleanup() {
if (!cleanup || didCleanup) return;
didCleanup = true;
try {
void Promise.resolve(cleanup()).catch((error) => {
console.warn("[TauriWindow] Failed to remove resize listener", error);
});
} catch (error) {
console.warn("[TauriWindow] Failed to remove resize listener", error);
}
}
async function setup() {
const win = await getTauriWindow();
@@ -117,19 +131,28 @@ export function useTrafficLightPadding(): { left: number; top: number } {
// Check initial fullscreen state
const fullscreen = await win.isFullscreen();
if (disposed) return;
setIsFullscreen(fullscreen);
// Listen for resize events which include fullscreen changes
unlisten = await win.onResized(async () => {
const unlisten = await win.onResized(async () => {
if (disposed) return;
const fs = await win.isFullscreen();
if (disposed) return;
setIsFullscreen(fs);
});
cleanup = unlisten;
if (disposed) {
runCleanup();
}
}
setup();
void setup();
return () => {
unlisten?.();
disposed = true;
runCleanup();
};
}, []);
@@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import { upsertTerminalListEntry } from "./terminal-list";
describe("terminal-list", () => {
it("adds a created terminal when the list is empty", () => {
const result = upsertTerminalListEntry({
terminals: [],
terminal: {
id: "term-1",
name: "Terminal 1",
cwd: "/tmp/project",
},
});
expect(result).toEqual([{ id: "term-1", name: "Terminal 1" }]);
});
it("appends a created terminal when the id does not already exist", () => {
const result = upsertTerminalListEntry({
terminals: [{ id: "term-1", name: "Terminal 1" }],
terminal: {
id: "term-2",
name: "Terminal 2",
cwd: "/tmp/project",
},
});
expect(result).toEqual([
{ id: "term-1", name: "Terminal 1" },
{ id: "term-2", name: "Terminal 2" },
]);
});
it("replaces the existing terminal entry when ids match", () => {
const result = upsertTerminalListEntry({
terminals: [
{ id: "term-1", name: "Terminal 1" },
{ id: "term-2", name: "Old Name" },
],
terminal: {
id: "term-2",
name: "Renamed Terminal",
cwd: "/tmp/project",
},
});
expect(result).toEqual([
{ id: "term-1", name: "Terminal 1" },
{ id: "term-2", name: "Renamed Terminal" },
]);
});
});
+32
View File
@@ -0,0 +1,32 @@
import type {
CreateTerminalResponse,
ListTerminalsResponse,
} from "@server/shared/messages";
type TerminalListEntry = ListTerminalsResponse["payload"]["terminals"][number];
type CreatedTerminal = NonNullable<CreateTerminalResponse["payload"]["terminal"]>;
function toTerminalListEntry(input: { terminal: CreatedTerminal }): TerminalListEntry {
return {
id: input.terminal.id,
name: input.terminal.name,
};
}
export function upsertTerminalListEntry(input: {
terminals: TerminalListEntry[];
terminal: CreatedTerminal;
}): TerminalListEntry[] {
const createdTerminal = toTerminalListEntry({ terminal: input.terminal });
const existingIndex = input.terminals.findIndex(
(terminal) => terminal.id === createdTerminal.id
);
if (existingIndex < 0) {
return [...input.terminals, createdTerminal];
}
const nextTerminals = [...input.terminals];
nextTerminals[existingIndex] = createdTerminal;
return nextTerminals;
}
@@ -76,6 +76,34 @@ describe("tool-call-display", () => {
});
});
it("builds display model from worktree setup detail", () => {
const display = buildToolCallDisplayModel({
name: "paseo_worktree_setup",
status: "running",
error: null,
detail: {
type: "worktree_setup",
worktreePath: "/tmp/repo/.paseo/worktrees/repo/branch",
branchName: "feature-branch",
log: "==> [1/1] Running: npm install\n",
commands: [
{
index: 1,
command: "npm install",
cwd: "/tmp/repo/.paseo/worktrees/repo/branch",
status: "running",
exitCode: null,
},
],
},
});
expect(display).toEqual({
displayName: "Worktree Setup",
summary: "feature-branch",
});
});
it("does not derive command summary from unknown raw detail", () => {
const display = buildToolCallDisplayModel({
name: "exec_command",
+1
View File
@@ -19,6 +19,7 @@ const TOOL_DETAIL_ICONS: Record<ToolCallDetail["type"], ToolCallIconComponent> =
edit: Pencil,
write: Pencil,
search: Search,
worktree_setup: SquareTerminal,
unknown: Wrench,
};
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import { buildWorkingDirectorySuggestions } from "./working-directory-suggestions";
describe("buildWorkingDirectorySuggestions", () => {
it("returns de-duplicated recommendations when query is empty", () => {
const results = buildWorkingDirectorySuggestions({
recommendedPaths: ["/Users/me/projects/paseo", "/Users/me/projects/paseo"],
serverPaths: ["/Users/me/projects/playground"],
query: "",
});
expect(results).toEqual(["/Users/me/projects/paseo"]);
});
it("prioritizes matching recommended directories before server matches", () => {
const results = buildWorkingDirectorySuggestions({
recommendedPaths: ["/Users/me/projects/paseo", "/Users/me/documents"],
serverPaths: [
"/Users/me/projects/playground",
"/Users/me/projects/paseo",
"/Users/me/projects/planbook",
],
query: "pla",
});
expect(results).toEqual([
"/Users/me/projects/playground",
"/Users/me/projects/planbook",
]);
});
it("puts matching recommended items first when they also match query", () => {
const results = buildWorkingDirectorySuggestions({
recommendedPaths: ["/Users/me/projects/playground", "/Users/me/projects/paseo"],
serverPaths: [
"/Users/me/projects/planbook",
"/Users/me/projects/playground",
],
query: "pla",
});
expect(results).toEqual([
"/Users/me/projects/playground",
"/Users/me/projects/planbook",
]);
});
it("treats '~' as an active query and includes server suggestions", () => {
const results = buildWorkingDirectorySuggestions({
recommendedPaths: ["/Users/me/projects/paseo"],
serverPaths: [
"/Users/me/documents",
"/Users/me/projects",
],
query: "~",
});
expect(results).toEqual([
"/Users/me/projects/paseo",
"/Users/me/documents",
"/Users/me/projects",
]);
});
});
@@ -0,0 +1,72 @@
export interface BuildWorkingDirectorySuggestionsInput {
recommendedPaths: string[];
serverPaths: string[];
query: string;
}
export function buildWorkingDirectorySuggestions(
input: BuildWorkingDirectorySuggestionsInput
): string[] {
const rawQuery = input.query.trim();
const recommended = uniquePaths(input.recommendedPaths);
if (!rawQuery) {
return recommended;
}
const normalizedQuery = normalizeQuery(rawQuery);
const shouldFilterByQuery = normalizedQuery.length > 0;
const recommendedMatches = shouldFilterByQuery
? recommended.filter((entry) => pathMatchesQuery(entry, normalizedQuery))
: recommended;
const seen = new Set(recommendedMatches);
const ordered = [...recommendedMatches];
for (const entry of uniquePaths(input.serverPaths)) {
if (shouldFilterByQuery && !pathMatchesQuery(entry, normalizedQuery)) {
continue;
}
if (seen.has(entry)) {
continue;
}
ordered.push(entry);
seen.add(entry);
}
return ordered;
}
function uniquePaths(paths: string[]): string[] {
const seen = new Set<string>();
const ordered: string[] = [];
for (const pathEntry of paths) {
const trimmed = pathEntry.trim();
if (!trimmed || seen.has(trimmed)) {
continue;
}
seen.add(trimmed);
ordered.push(trimmed);
}
return ordered;
}
function normalizeQuery(query: string): string {
let normalized = query.trim();
if (!normalized) {
return "";
}
if (normalized.startsWith("~")) {
normalized = normalized.slice(1);
}
normalized = normalized.replace(/^\/+/, "").toLowerCase();
return normalized;
}
function pathMatchesQuery(candidatePath: string, query: string): boolean {
const lowerPath = candidatePath.toLowerCase();
if (lowerPath.includes(query)) {
return true;
}
const segments = lowerPath.split("/");
return (segments[segments.length - 1] ?? "").includes(query);
}