feat: add chat and website updates
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import type { SingleResult } from "../../output/index.js";
|
import type { SingleResult } from "../../output/index.js";
|
||||||
import {
|
import {
|
||||||
|
attachAgentNamesToMessages,
|
||||||
connectChatClient,
|
connectChatClient,
|
||||||
|
resolveChatAuthorAgentId,
|
||||||
toChatCommandError,
|
toChatCommandError,
|
||||||
type ChatCommandOptions,
|
type ChatCommandOptions,
|
||||||
} from "./shared.js";
|
} from "./shared.js";
|
||||||
@@ -22,11 +24,13 @@ export async function runPostCommand(
|
|||||||
const payload = await client.postChatMessage({
|
const payload = await client.postChatMessage({
|
||||||
room,
|
room,
|
||||||
body,
|
body,
|
||||||
|
authorAgentId: resolveChatAuthorAgentId(),
|
||||||
replyToMessageId: options.replyTo,
|
replyToMessageId: options.replyTo,
|
||||||
});
|
});
|
||||||
|
const [message] = await attachAgentNamesToMessages(client, [toChatMessageRow(payload.message!)]);
|
||||||
return {
|
return {
|
||||||
type: "single",
|
type: "single",
|
||||||
data: toChatMessageRow(payload.message!),
|
data: message!,
|
||||||
schema: chatMessageSchema,
|
schema: chatMessageSchema,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import type { ListResult } from "../../output/index.js";
|
import type { ListResult } from "../../output/index.js";
|
||||||
import {
|
import {
|
||||||
|
attachAgentNamesToMessages,
|
||||||
connectChatClient,
|
connectChatClient,
|
||||||
parseSinceValue,
|
parseSinceValue,
|
||||||
toChatCommandError,
|
toChatCommandError,
|
||||||
@@ -42,9 +43,13 @@ export async function runReadCommand(
|
|||||||
since: parseSinceValue(options.since),
|
since: parseSinceValue(options.since),
|
||||||
authorAgentId: options.agent,
|
authorAgentId: options.agent,
|
||||||
});
|
});
|
||||||
|
const messages = await attachAgentNamesToMessages(
|
||||||
|
client,
|
||||||
|
payload.messages.map(toChatMessageRow),
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
type: "list",
|
type: "list",
|
||||||
data: payload.messages.map(toChatMessageRow),
|
data: messages,
|
||||||
schema: chatMessageSchema,
|
schema: chatMessageSchema,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { OutputSchema } from "../../output/index.js";
|
import type { OutputSchema } from "../../output/index.js";
|
||||||
|
import type { AnyCommandResult, OutputOptions } from "../../output/index.js";
|
||||||
|
|
||||||
export interface ChatRoomRow {
|
export interface ChatRoomRow {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -11,9 +12,11 @@ export interface ChatRoomRow {
|
|||||||
export interface ChatMessageRow {
|
export interface ChatMessageRow {
|
||||||
id: string;
|
id: string;
|
||||||
author: string;
|
author: string;
|
||||||
|
authorName: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
replyTo: string;
|
replyTo: string;
|
||||||
mentions: string;
|
mentionAgentIds: string[];
|
||||||
|
mentionLabels: string[];
|
||||||
body: string;
|
body: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,13 +36,59 @@ export const chatMessageSchema: OutputSchema<ChatMessageRow> = {
|
|||||||
columns: [
|
columns: [
|
||||||
{ header: "ID", field: "id", width: 36 },
|
{ header: "ID", field: "id", width: 36 },
|
||||||
{ header: "AUTHOR", field: "author", width: 16 },
|
{ header: "AUTHOR", field: "author", width: 16 },
|
||||||
|
{ header: "AUTHOR NAME", field: (row) => row.authorName ?? "-", width: 20 },
|
||||||
{ header: "CREATED", field: "createdAt", width: 24 },
|
{ header: "CREATED", field: "createdAt", width: 24 },
|
||||||
{ header: "REPLY TO", field: "replyTo", width: 36 },
|
{ header: "REPLY TO", field: "replyTo", width: 36 },
|
||||||
{ header: "MENTIONS", field: "mentions", width: 24 },
|
{ header: "MENTIONS", field: (row) => row.mentionLabels.join(", ") || "-", width: 24 },
|
||||||
{ header: "MESSAGE", field: "body", width: 60 },
|
{ header: "MESSAGE", field: "body", width: 60 },
|
||||||
],
|
],
|
||||||
|
renderHuman: renderChatTranscript,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function renderChatTranscript(
|
||||||
|
result: AnyCommandResult<ChatMessageRow>,
|
||||||
|
_options: OutputOptions,
|
||||||
|
): string {
|
||||||
|
const data = result.type === "list" ? result.data : [result.data];
|
||||||
|
if (data.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return data.map(renderChatMessageBlock).join("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChatMessageBlock(message: ChatMessageRow): string {
|
||||||
|
const authorLabel = message.authorName
|
||||||
|
? `${message.authorName} (${message.author})`
|
||||||
|
: message.author;
|
||||||
|
const lines = [`┌─ ${authorLabel} ── ${formatTimestamp(message.createdAt)} ── [msg ${message.id}]`];
|
||||||
|
|
||||||
|
if (message.replyTo !== "-") {
|
||||||
|
lines.push(`│ reply-to: msg ${message.replyTo}`);
|
||||||
|
}
|
||||||
|
if (message.mentionLabels.length > 0) {
|
||||||
|
lines.push(`│ mentions: ${message.mentionLabels.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("│");
|
||||||
|
|
||||||
|
const bodyLines = message.body.split(/\r?\n/);
|
||||||
|
for (const line of bodyLines) {
|
||||||
|
lines.push(`│ ${line}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("│");
|
||||||
|
lines.push("└─");
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(input: string): string {
|
||||||
|
const date = new Date(input);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
return date.toISOString().replace("T", " ").replace(".000Z", "Z");
|
||||||
|
}
|
||||||
|
|
||||||
export function toChatRoomRow(room: {
|
export function toChatRoomRow(room: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -67,9 +116,11 @@ export function toChatMessageRow(message: {
|
|||||||
return {
|
return {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
author: message.authorAgentId,
|
author: message.authorAgentId,
|
||||||
|
authorName: null,
|
||||||
createdAt: message.createdAt,
|
createdAt: message.createdAt,
|
||||||
replyTo: message.replyToMessageId ?? "-",
|
replyTo: message.replyToMessageId ?? "-",
|
||||||
mentions: message.mentionAgentIds.length > 0 ? message.mentionAgentIds.join(",") : "-",
|
mentionAgentIds: message.mentionAgentIds,
|
||||||
|
mentionLabels: message.mentionAgentIds,
|
||||||
body: message.body,
|
body: message.body,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||||
import { parseDuration } from "../../utils/duration.js";
|
import { parseDuration } from "../../utils/duration.js";
|
||||||
import type { CommandError, CommandOptions } from "../../output/index.js";
|
import type { CommandError, CommandOptions } from "../../output/index.js";
|
||||||
|
import type { ChatMessageRow } from "./schema.js";
|
||||||
|
|
||||||
export interface ChatCommandOptions extends CommandOptions {
|
export interface ChatCommandOptions extends CommandOptions {
|
||||||
host?: string;
|
host?: string;
|
||||||
@@ -22,6 +23,43 @@ export async function connectChatClient(host?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function attachAgentNamesToMessages(
|
||||||
|
client: Awaited<ReturnType<typeof connectToDaemon>>,
|
||||||
|
messages: ChatMessageRow[],
|
||||||
|
): Promise<ChatMessageRow[]> {
|
||||||
|
const agentIds = new Set<string>();
|
||||||
|
for (const message of messages) {
|
||||||
|
agentIds.add(message.author);
|
||||||
|
for (const mentionId of message.mentionAgentIds) {
|
||||||
|
agentIds.add(mentionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agentIds.size === 0) {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await client.fetchAgents({
|
||||||
|
filter: { includeArchived: true },
|
||||||
|
});
|
||||||
|
const agentNames = new Map<string, string>();
|
||||||
|
for (const entry of payload.entries) {
|
||||||
|
const title = entry.agent.title?.trim();
|
||||||
|
if (title) {
|
||||||
|
agentNames.set(entry.agent.id, title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages.map((message) => ({
|
||||||
|
...message,
|
||||||
|
authorName: agentNames.get(message.author) ?? null,
|
||||||
|
mentionLabels: message.mentionAgentIds.map((agentId) => {
|
||||||
|
const name = agentNames.get(agentId);
|
||||||
|
return name ? `${name} (${agentId})` : agentId;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export function toChatCommandError(code: string, action: string, err: unknown): CommandError {
|
export function toChatCommandError(code: string, action: string, err: unknown): CommandError {
|
||||||
if (err && typeof err === "object" && "code" in err && "message" in err) {
|
if (err && typeof err === "object" && "code" in err && "message" in err) {
|
||||||
return err as CommandError;
|
return err as CommandError;
|
||||||
@@ -82,3 +120,8 @@ export function parseTimeoutMs(input?: string): number | undefined {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveChatAuthorAgentId(): string {
|
||||||
|
const agentId = process.env.PASEO_AGENT_ID?.trim();
|
||||||
|
return agentId && agentId.length > 0 ? agentId : "manual";
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import type { ListResult } from "../../output/index.js";
|
import type { ListResult } from "../../output/index.js";
|
||||||
import {
|
import {
|
||||||
|
attachAgentNamesToMessages,
|
||||||
connectChatClient,
|
connectChatClient,
|
||||||
parseTimeoutMs,
|
parseTimeoutMs,
|
||||||
toChatCommandError,
|
toChatCommandError,
|
||||||
@@ -29,9 +30,13 @@ export async function runWaitCommand(
|
|||||||
afterMessageId,
|
afterMessageId,
|
||||||
timeoutMs: parseTimeoutMs(options.timeout),
|
timeoutMs: parseTimeoutMs(options.timeout),
|
||||||
});
|
});
|
||||||
|
const messages = await attachAgentNamesToMessages(
|
||||||
|
client,
|
||||||
|
payload.messages.map(toChatMessageRow),
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
type: "list",
|
type: "list",
|
||||||
data: payload.messages.map(toChatMessageRow),
|
data: messages,
|
||||||
schema: chatMessageSchema,
|
schema: chatMessageSchema,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ export function render<T>(
|
|||||||
return renderYaml(result, opts);
|
return renderYaml(result, opts);
|
||||||
case "table":
|
case "table":
|
||||||
default:
|
default:
|
||||||
|
if (result.schema.renderHuman) {
|
||||||
|
return result.schema.renderHuman(result, opts);
|
||||||
|
}
|
||||||
return renderTable(result, opts);
|
return renderTable(result, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export interface OutputSchema<T> {
|
|||||||
idField: keyof T | ((item: T) => string);
|
idField: keyof T | ((item: T) => string);
|
||||||
/** Column definitions for table output */
|
/** Column definitions for table output */
|
||||||
columns: ColumnDef<T>[];
|
columns: ColumnDef<T>[];
|
||||||
|
/** Optional: custom renderer for human/table output */
|
||||||
|
renderHuman?: (result: AnyCommandResult<T>, options: OutputOptions) => string;
|
||||||
/** Optional: transform data before JSON/YAML output */
|
/** Optional: transform data before JSON/YAML output */
|
||||||
serialize?: (data: T) => unknown;
|
serialize?: (data: T) => unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,17 @@ interface Agent {
|
|||||||
provider: string;
|
provider: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
id: string;
|
||||||
|
author: string;
|
||||||
|
authorName: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
replyTo: string;
|
||||||
|
mentionAgentIds: string[];
|
||||||
|
mentionLabels: string[];
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Schema for agents
|
// Schema for agents
|
||||||
const agentSchema: OutputSchema<Agent> = {
|
const agentSchema: OutputSchema<Agent> = {
|
||||||
idField: "id",
|
idField: "id",
|
||||||
@@ -211,6 +222,36 @@ test("render uses table format by default", () => {
|
|||||||
assert.ok(output.includes("ID"), "Should use table format with headers");
|
assert.ok(output.includes("ID"), "Should use table format with headers");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("render uses custom human renderer when provided", () => {
|
||||||
|
const chatSchema: OutputSchema<ChatMessage> = {
|
||||||
|
idField: "id",
|
||||||
|
columns: [{ header: "ID", field: "id" }],
|
||||||
|
renderHuman: (result) => {
|
||||||
|
const data = result.type === "list" ? result.data : [result.data];
|
||||||
|
return data.map((message) => `msg ${message.id}: ${message.body}`).join("\n");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const chatResult: ListResult<ChatMessage> = {
|
||||||
|
type: "list",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: "m1",
|
||||||
|
author: "agent-1",
|
||||||
|
authorName: "Planner",
|
||||||
|
createdAt: "2026-03-29T10:00:00Z",
|
||||||
|
replyTo: "-",
|
||||||
|
mentionAgentIds: [],
|
||||||
|
mentionLabels: [],
|
||||||
|
body: "hello",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema: chatSchema,
|
||||||
|
};
|
||||||
|
|
||||||
|
const output = render(chatResult, { noColor: true });
|
||||||
|
assert.strictEqual(output, "msg m1: hello", "Should use custom human renderer");
|
||||||
|
});
|
||||||
|
|
||||||
test("render uses json format when specified", () => {
|
test("render uses json format when specified", () => {
|
||||||
const output = render(listResult, { format: "json" });
|
const output = render(listResult, { format: "json" });
|
||||||
const parsed = JSON.parse(output);
|
const parsed = JSON.parse(output);
|
||||||
|
|||||||
@@ -32,13 +32,22 @@ try {
|
|||||||
"post",
|
"post",
|
||||||
"coord-room",
|
"coord-room",
|
||||||
"first message for @agent-1",
|
"first message for @agent-1",
|
||||||
]);
|
], {
|
||||||
|
env: { PASEO_AGENT_ID: "00000000-0000-4000-8000-000000000111" },
|
||||||
|
});
|
||||||
assert.strictEqual(posted.exitCode, 0, posted.stderr);
|
assert.strictEqual(posted.exitCode, 0, posted.stderr);
|
||||||
assert(posted.stdout.includes("first message"), posted.stdout);
|
assert(posted.stdout.includes("first message"), posted.stdout);
|
||||||
|
assert(posted.stdout.includes("00000000-0000-4000-8000-000000000111"), posted.stdout);
|
||||||
|
|
||||||
const read = await ctx.paseo(["chat", "read", "coord-room", "--limit", "10"]);
|
const read = await ctx.paseo(["chat", "read", "coord-room", "--limit", "10"]);
|
||||||
assert.strictEqual(read.exitCode, 0, read.stderr);
|
assert.strictEqual(read.exitCode, 0, read.stderr);
|
||||||
assert(read.stdout.includes("first message"), read.stdout);
|
assert(read.stdout.includes("first message"), read.stdout);
|
||||||
|
assert(read.stdout.includes("00000000-0000-4000-8000-000000000111"), read.stdout);
|
||||||
|
|
||||||
|
const readJson = await ctx.paseo(["chat", "read", "coord-room", "--limit", "10", "--json"]);
|
||||||
|
assert.strictEqual(readJson.exitCode, 0, readJson.stderr);
|
||||||
|
const readPayload = JSON.parse(readJson.stdout);
|
||||||
|
assert.strictEqual(readPayload[0]?.author, "00000000-0000-4000-8000-000000000111");
|
||||||
|
|
||||||
const waitPromise = ctx.paseo(["chat", "wait", "coord-room", "--timeout", "5s"]);
|
const waitPromise = ctx.paseo(["chat", "wait", "coord-room", "--timeout", "5s"]);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ export async function runPaseoCli(
|
|||||||
options?: {
|
options?: {
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
},
|
},
|
||||||
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
||||||
const timeout = options?.timeout ?? 60000;
|
const timeout = options?.timeout ?? 60000;
|
||||||
@@ -348,6 +349,7 @@ export async function runPaseoCli(
|
|||||||
...TEST_DAEMON_ENV_DEFAULTS,
|
...TEST_DAEMON_ENV_DEFAULTS,
|
||||||
PASEO_HOST: `${TEST_DAEMON_HOST}:${ctx.port}`,
|
PASEO_HOST: `${TEST_DAEMON_HOST}:${ctx.port}`,
|
||||||
PASEO_HOME: ctx.paseoHome,
|
PASEO_HOME: ctx.paseoHome,
|
||||||
|
...options?.env,
|
||||||
},
|
},
|
||||||
cwd,
|
cwd,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
@@ -399,7 +401,7 @@ export async function createE2ETestContext(options?: { timeout?: number }): Prom
|
|||||||
/** Run a paseo CLI command against this daemon */
|
/** Run a paseo CLI command against this daemon */
|
||||||
paseo: (
|
paseo: (
|
||||||
args: string[],
|
args: string[],
|
||||||
opts?: { timeout?: number; cwd?: string },
|
opts?: { timeout?: number; cwd?: string; env?: NodeJS.ProcessEnv },
|
||||||
) => Promise<{
|
) => Promise<{
|
||||||
exitCode: number;
|
exitCode: number;
|
||||||
stdout: string;
|
stdout: string;
|
||||||
@@ -409,7 +411,7 @@ export async function createE2ETestContext(options?: { timeout?: number }): Prom
|
|||||||
> {
|
> {
|
||||||
const ctx = await startTestDaemon({ timeout: options?.timeout });
|
const ctx = await startTestDaemon({ timeout: options?.timeout });
|
||||||
|
|
||||||
const paseo = (args: string[], opts?: { timeout?: number; cwd?: string }) =>
|
const paseo = (args: string[], opts?: { timeout?: number; cwd?: string; env?: NodeJS.ProcessEnv }) =>
|
||||||
runPaseoCli(ctx, args, opts);
|
runPaseoCli(ctx, args, opts);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ export type DeleteChatRoomOptions = {
|
|||||||
export type PostChatMessageOptions = {
|
export type PostChatMessageOptions = {
|
||||||
room: string;
|
room: string;
|
||||||
body: string;
|
body: string;
|
||||||
|
authorAgentId?: string;
|
||||||
replyToMessageId?: string | null;
|
replyToMessageId?: string | null;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
};
|
};
|
||||||
@@ -2911,6 +2912,7 @@ export class DaemonClient {
|
|||||||
type: "chat/post",
|
type: "chat/post",
|
||||||
room: options.room,
|
room: options.room,
|
||||||
body: options.body,
|
body: options.body,
|
||||||
|
...(options.authorAgentId ? { authorAgentId: options.authorAgentId } : {}),
|
||||||
...(options.replyToMessageId ? { replyToMessageId: options.replyToMessageId } : {}),
|
...(options.replyToMessageId ? { replyToMessageId: options.replyToMessageId } : {}),
|
||||||
},
|
},
|
||||||
responseType: "chat/post/response",
|
responseType: "chat/post/response",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const ChatPostRequestSchema = z.object({
|
|||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
room: z.string(),
|
room: z.string(),
|
||||||
body: z.string(),
|
body: z.string(),
|
||||||
|
authorAgentId: z.string().optional(),
|
||||||
replyToMessageId: z.string().optional(),
|
replyToMessageId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7726,9 +7726,10 @@ export class Session {
|
|||||||
request: Extract<SessionInboundMessage, { type: "chat/post" }>,
|
request: Extract<SessionInboundMessage, { type: "chat/post" }>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const authorAgentId = request.authorAgentId?.trim() || this.clientId;
|
||||||
const message = await this.chatService.postMessage({
|
const message = await this.chatService.postMessage({
|
||||||
room: request.room,
|
room: request.room,
|
||||||
authorAgentId: this.clientId,
|
authorAgentId,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
replyToMessageId: request.replyToMessageId,
|
replyToMessageId: request.replyToMessageId,
|
||||||
});
|
});
|
||||||
@@ -7742,7 +7743,7 @@ export class Session {
|
|||||||
});
|
});
|
||||||
void notifyChatMentions({
|
void notifyChatMentions({
|
||||||
room: request.room,
|
room: request.room,
|
||||||
authorAgentId: this.clientId,
|
authorAgentId,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
mentionAgentIds: message.mentionAgentIds,
|
mentionAgentIds: message.mentionAgentIds,
|
||||||
logger: this.sessionLogger,
|
logger: this.sessionLogger,
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: "Draft Example"
|
||||||
|
description: "A draft post for testing the draft system."
|
||||||
|
date: "2026-03-29"
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a draft post. It only appears when `?drafts` is in the URL.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: "Hello World"
|
||||||
|
description: "The first post on the Paseo blog."
|
||||||
|
date: "2026-03-29"
|
||||||
|
---
|
||||||
|
|
||||||
|
Welcome to the Paseo blog.
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { Mic } from "lucide-react";
|
import { Mic } from "lucide-react";
|
||||||
import { HeroMockup } from "~/components/hero-mockup";
|
import { HeroMockup } from "~/components/hero-mockup";
|
||||||
import { ClaudeIcon } from "~/components/mockup";
|
import { ClaudeIcon } from "~/components/mockup";
|
||||||
|
import { SiteHeader } from "~/components/site-header";
|
||||||
import "~/styles.css";
|
import "~/styles.css";
|
||||||
|
|
||||||
interface LandingPageProps {
|
interface LandingPageProps {
|
||||||
@@ -70,6 +71,12 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-white/60 font-medium">Product</p>
|
<p className="text-white/60 font-medium">Product</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
<a
|
||||||
|
href="/blog"
|
||||||
|
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||||
|
>
|
||||||
|
Blog
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/docs"
|
href="/docs"
|
||||||
className="block text-white/40 hover:text-white/60 transition-colors"
|
className="block text-white/40 hover:text-white/60 transition-colors"
|
||||||
@@ -186,68 +193,8 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
|
|||||||
|
|
||||||
function Nav() {
|
function Nav() {
|
||||||
return (
|
return (
|
||||||
<nav className="flex flex-col sm:flex-row items-center sm:justify-between gap-4 mb-16">
|
<nav className="mb-16">
|
||||||
<div className="flex items-center gap-3">
|
<SiteHeader />
|
||||||
<a href="/" className="flex items-center gap-3">
|
|
||||||
<img src="/logo.svg" alt="Paseo" className="w-7 h-7" />
|
|
||||||
<span className="text-lg font-medium">Paseo</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<a
|
|
||||||
href="/docs"
|
|
||||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
>
|
|
||||||
Docs
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/changelog"
|
|
||||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
>
|
|
||||||
Changelog
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/download"
|
|
||||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://discord.gg/jz8T2uahpH"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="Discord"
|
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
role="img"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://github.com/getpaseo/paseo"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="GitHub"
|
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path d="M12 0C5.37 0 0 5.484 0 12.252c0 5.418 3.438 10.013 8.205 11.637.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.738-4.042-1.61-4.042-1.61-.546-1.403-1.333-1.776-1.333-1.776-1.089-.756.084-.741.084-.741 1.205.087 1.838 1.262 1.838 1.262 1.07 1.87 2.809 1.33 3.495 1.017.108-.79.417-1.33.76-1.636-2.665-.31-5.467-1.35-5.467-6.005 0-1.327.465-2.413 1.235-3.262-.124-.31-.535-1.556.117-3.243 0 0 1.008-.33 3.3 1.248a11.2 11.2 0 0 1 3.003-.404c1.02.005 2.045.138 3.003.404 2.29-1.578 3.297-1.248 3.297-1.248.653 1.687.242 2.933.118 3.243.77.85 1.233 1.935 1.233 3.262 0 4.667-2.807 5.692-5.48 5.995.43.38.823 1.133.823 2.285 0 1.65-.015 2.98-.015 3.386 0 .315.218.694.825.576C20.565 22.26 24 17.667 24 12.252 24 5.484 18.627 0 12 0z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import "~/styles.css";
|
||||||
|
|
||||||
|
export function SiteHeader() {
|
||||||
|
return (
|
||||||
|
<header className="flex items-center justify-between gap-4">
|
||||||
|
<a href="/" className="flex items-center gap-3">
|
||||||
|
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
|
||||||
|
<span className="text-lg font-medium">Paseo</span>
|
||||||
|
</a>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<a
|
||||||
|
href="/blog"
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Blog
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/docs"
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Docs
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/changelog"
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Changelog
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/download"
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://discord.gg/jz8T2uahpH"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Discord"
|
||||||
|
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://github.com/getpaseo/paseo"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="GitHub"
|
||||||
|
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path d="M12 0C5.37 0 0 5.484 0 12.252c0 5.418 3.438 10.013 8.205 11.637.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.738-4.042-1.61-4.042-1.61-.546-1.403-1.333-1.776-1.333-1.776-1.089-.756.084-.741.084-.741 1.205.087 1.838 1.262 1.838 1.262 1.07 1.87 2.809 1.33 3.495 1.017.108-.79.417-1.33.76-1.636-2.665-.31-5.467-1.35-5.467-6.005 0-1.327.465-2.413 1.235-3.262-.124-.31-.535-1.556.117-3.243 0 0 1.008-.33 3.3 1.248a11.2 11.2 0 0 1 3.003-.404c1.02.005 2.045.138 3.003.404 2.29-1.578 3.297-1.248 3.297-1.248.653 1.687.242 2.933.118 3.243.77.85 1.233 1.935 1.233 3.262 0 4.667-2.807 5.692-5.48 5.995.43.38.823 1.133.823 2.285 0 1.65-.015 2.98-.015 3.386 0 .315.218.694.825.576C20.565 22.26 24 17.667 24 12.252 24 5.484 18.627 0 12 0z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
interface PostFrontmatter {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
date: string;
|
||||||
|
draft: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Post {
|
||||||
|
slug: string;
|
||||||
|
frontmatter: PostFrontmatter;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFrontmatter(raw: string): { data: Record<string, string>; content: string } {
|
||||||
|
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||||||
|
if (!match) return { data: {}, content: raw };
|
||||||
|
|
||||||
|
const data: Record<string, string> = {};
|
||||||
|
for (const line of match[1].split("\n")) {
|
||||||
|
const colonIdx = line.indexOf(":");
|
||||||
|
if (colonIdx === -1) continue;
|
||||||
|
const key = line.slice(0, colonIdx).trim();
|
||||||
|
const value = line
|
||||||
|
.slice(colonIdx + 1)
|
||||||
|
.trim()
|
||||||
|
.replace(/^["']|["']$/g, "");
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data, content: match[2] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const postModules = import.meta.glob("../posts/**/*.md", {
|
||||||
|
eager: true,
|
||||||
|
query: "?raw",
|
||||||
|
import: "default",
|
||||||
|
}) as Record<string, string>;
|
||||||
|
|
||||||
|
function loadPosts(includeDrafts: boolean): Post[] {
|
||||||
|
const posts: Post[] = [];
|
||||||
|
|
||||||
|
for (const [path, raw] of Object.entries(postModules)) {
|
||||||
|
const isDraft = path.includes("/drafts/");
|
||||||
|
if (isDraft && !includeDrafts) continue;
|
||||||
|
|
||||||
|
const { data, content } = parseFrontmatter(raw);
|
||||||
|
const fileName = path.split("/").pop()!.replace(".md", "");
|
||||||
|
const slug = isDraft ? `drafts/${fileName}` : fileName;
|
||||||
|
|
||||||
|
posts.push({
|
||||||
|
slug,
|
||||||
|
frontmatter: {
|
||||||
|
title: data.title ?? "",
|
||||||
|
description: data.description ?? "",
|
||||||
|
date: data.date ?? "",
|
||||||
|
draft: isDraft,
|
||||||
|
},
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
posts.sort(
|
||||||
|
(a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPosts(includeDrafts = false): Post[] {
|
||||||
|
return loadPosts(includeDrafts);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPost(slug: string): Post | undefined {
|
||||||
|
return loadPosts(true).find((p) => p.slug === slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(date: Date): string {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const day = date.getDate().toString().padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
@@ -16,8 +16,10 @@ import { Route as DocsRouteImport } from './routes/docs'
|
|||||||
import { Route as CodexRouteImport } from './routes/codex'
|
import { Route as CodexRouteImport } from './routes/codex'
|
||||||
import { Route as ClaudeCodeRouteImport } from './routes/claude-code'
|
import { Route as ClaudeCodeRouteImport } from './routes/claude-code'
|
||||||
import { Route as ChangelogRouteImport } from './routes/changelog'
|
import { Route as ChangelogRouteImport } from './routes/changelog'
|
||||||
|
import { Route as BlogRouteImport } from './routes/blog'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as DocsIndexRouteImport } from './routes/docs/index'
|
import { Route as DocsIndexRouteImport } from './routes/docs/index'
|
||||||
|
import { Route as BlogIndexRouteImport } from './routes/blog/index'
|
||||||
import { Route as DocsWorktreesRouteImport } from './routes/docs/worktrees'
|
import { Route as DocsWorktreesRouteImport } from './routes/docs/worktrees'
|
||||||
import { Route as DocsVoiceRouteImport } from './routes/docs/voice'
|
import { Route as DocsVoiceRouteImport } from './routes/docs/voice'
|
||||||
import { Route as DocsUpdatesRouteImport } from './routes/docs/updates'
|
import { Route as DocsUpdatesRouteImport } from './routes/docs/updates'
|
||||||
@@ -25,6 +27,7 @@ import { Route as DocsSecurityRouteImport } from './routes/docs/security'
|
|||||||
import { Route as DocsConfigurationRouteImport } from './routes/docs/configuration'
|
import { Route as DocsConfigurationRouteImport } from './routes/docs/configuration'
|
||||||
import { Route as DocsCliRouteImport } from './routes/docs/cli'
|
import { Route as DocsCliRouteImport } from './routes/docs/cli'
|
||||||
import { Route as DocsBestPracticesRouteImport } from './routes/docs/best-practices'
|
import { Route as DocsBestPracticesRouteImport } from './routes/docs/best-practices'
|
||||||
|
import { Route as BlogSplatRouteImport } from './routes/blog/$'
|
||||||
|
|
||||||
const PrivacyRoute = PrivacyRouteImport.update({
|
const PrivacyRoute = PrivacyRouteImport.update({
|
||||||
id: '/privacy',
|
id: '/privacy',
|
||||||
@@ -61,6 +64,11 @@ const ChangelogRoute = ChangelogRouteImport.update({
|
|||||||
path: '/changelog',
|
path: '/changelog',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const BlogRoute = BlogRouteImport.update({
|
||||||
|
id: '/blog',
|
||||||
|
path: '/blog',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const IndexRoute = IndexRouteImport.update({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -71,6 +79,11 @@ const DocsIndexRoute = DocsIndexRouteImport.update({
|
|||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => DocsRoute,
|
getParentRoute: () => DocsRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const BlogIndexRoute = BlogIndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => BlogRoute,
|
||||||
|
} as any)
|
||||||
const DocsWorktreesRoute = DocsWorktreesRouteImport.update({
|
const DocsWorktreesRoute = DocsWorktreesRouteImport.update({
|
||||||
id: '/worktrees',
|
id: '/worktrees',
|
||||||
path: '/worktrees',
|
path: '/worktrees',
|
||||||
@@ -106,9 +119,15 @@ const DocsBestPracticesRoute = DocsBestPracticesRouteImport.update({
|
|||||||
path: '/best-practices',
|
path: '/best-practices',
|
||||||
getParentRoute: () => DocsRoute,
|
getParentRoute: () => DocsRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const BlogSplatRoute = BlogSplatRouteImport.update({
|
||||||
|
id: '/$',
|
||||||
|
path: '/$',
|
||||||
|
getParentRoute: () => BlogRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/blog': typeof BlogRouteWithChildren
|
||||||
'/changelog': typeof ChangelogRoute
|
'/changelog': typeof ChangelogRoute
|
||||||
'/claude-code': typeof ClaudeCodeRoute
|
'/claude-code': typeof ClaudeCodeRoute
|
||||||
'/codex': typeof CodexRoute
|
'/codex': typeof CodexRoute
|
||||||
@@ -116,6 +135,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/download': typeof DownloadRoute
|
'/download': typeof DownloadRoute
|
||||||
'/opencode': typeof OpencodeRoute
|
'/opencode': typeof OpencodeRoute
|
||||||
'/privacy': typeof PrivacyRoute
|
'/privacy': typeof PrivacyRoute
|
||||||
|
'/blog/$': typeof BlogSplatRoute
|
||||||
'/docs/best-practices': typeof DocsBestPracticesRoute
|
'/docs/best-practices': typeof DocsBestPracticesRoute
|
||||||
'/docs/cli': typeof DocsCliRoute
|
'/docs/cli': typeof DocsCliRoute
|
||||||
'/docs/configuration': typeof DocsConfigurationRoute
|
'/docs/configuration': typeof DocsConfigurationRoute
|
||||||
@@ -123,6 +143,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/docs/updates': typeof DocsUpdatesRoute
|
'/docs/updates': typeof DocsUpdatesRoute
|
||||||
'/docs/voice': typeof DocsVoiceRoute
|
'/docs/voice': typeof DocsVoiceRoute
|
||||||
'/docs/worktrees': typeof DocsWorktreesRoute
|
'/docs/worktrees': typeof DocsWorktreesRoute
|
||||||
|
'/blog/': typeof BlogIndexRoute
|
||||||
'/docs/': typeof DocsIndexRoute
|
'/docs/': typeof DocsIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
@@ -133,6 +154,7 @@ export interface FileRoutesByTo {
|
|||||||
'/download': typeof DownloadRoute
|
'/download': typeof DownloadRoute
|
||||||
'/opencode': typeof OpencodeRoute
|
'/opencode': typeof OpencodeRoute
|
||||||
'/privacy': typeof PrivacyRoute
|
'/privacy': typeof PrivacyRoute
|
||||||
|
'/blog/$': typeof BlogSplatRoute
|
||||||
'/docs/best-practices': typeof DocsBestPracticesRoute
|
'/docs/best-practices': typeof DocsBestPracticesRoute
|
||||||
'/docs/cli': typeof DocsCliRoute
|
'/docs/cli': typeof DocsCliRoute
|
||||||
'/docs/configuration': typeof DocsConfigurationRoute
|
'/docs/configuration': typeof DocsConfigurationRoute
|
||||||
@@ -140,11 +162,13 @@ export interface FileRoutesByTo {
|
|||||||
'/docs/updates': typeof DocsUpdatesRoute
|
'/docs/updates': typeof DocsUpdatesRoute
|
||||||
'/docs/voice': typeof DocsVoiceRoute
|
'/docs/voice': typeof DocsVoiceRoute
|
||||||
'/docs/worktrees': typeof DocsWorktreesRoute
|
'/docs/worktrees': typeof DocsWorktreesRoute
|
||||||
|
'/blog': typeof BlogIndexRoute
|
||||||
'/docs': typeof DocsIndexRoute
|
'/docs': typeof DocsIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/blog': typeof BlogRouteWithChildren
|
||||||
'/changelog': typeof ChangelogRoute
|
'/changelog': typeof ChangelogRoute
|
||||||
'/claude-code': typeof ClaudeCodeRoute
|
'/claude-code': typeof ClaudeCodeRoute
|
||||||
'/codex': typeof CodexRoute
|
'/codex': typeof CodexRoute
|
||||||
@@ -152,6 +176,7 @@ export interface FileRoutesById {
|
|||||||
'/download': typeof DownloadRoute
|
'/download': typeof DownloadRoute
|
||||||
'/opencode': typeof OpencodeRoute
|
'/opencode': typeof OpencodeRoute
|
||||||
'/privacy': typeof PrivacyRoute
|
'/privacy': typeof PrivacyRoute
|
||||||
|
'/blog/$': typeof BlogSplatRoute
|
||||||
'/docs/best-practices': typeof DocsBestPracticesRoute
|
'/docs/best-practices': typeof DocsBestPracticesRoute
|
||||||
'/docs/cli': typeof DocsCliRoute
|
'/docs/cli': typeof DocsCliRoute
|
||||||
'/docs/configuration': typeof DocsConfigurationRoute
|
'/docs/configuration': typeof DocsConfigurationRoute
|
||||||
@@ -159,12 +184,14 @@ export interface FileRoutesById {
|
|||||||
'/docs/updates': typeof DocsUpdatesRoute
|
'/docs/updates': typeof DocsUpdatesRoute
|
||||||
'/docs/voice': typeof DocsVoiceRoute
|
'/docs/voice': typeof DocsVoiceRoute
|
||||||
'/docs/worktrees': typeof DocsWorktreesRoute
|
'/docs/worktrees': typeof DocsWorktreesRoute
|
||||||
|
'/blog/': typeof BlogIndexRoute
|
||||||
'/docs/': typeof DocsIndexRoute
|
'/docs/': typeof DocsIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/blog'
|
||||||
| '/changelog'
|
| '/changelog'
|
||||||
| '/claude-code'
|
| '/claude-code'
|
||||||
| '/codex'
|
| '/codex'
|
||||||
@@ -172,6 +199,7 @@ export interface FileRouteTypes {
|
|||||||
| '/download'
|
| '/download'
|
||||||
| '/opencode'
|
| '/opencode'
|
||||||
| '/privacy'
|
| '/privacy'
|
||||||
|
| '/blog/$'
|
||||||
| '/docs/best-practices'
|
| '/docs/best-practices'
|
||||||
| '/docs/cli'
|
| '/docs/cli'
|
||||||
| '/docs/configuration'
|
| '/docs/configuration'
|
||||||
@@ -179,6 +207,7 @@ export interface FileRouteTypes {
|
|||||||
| '/docs/updates'
|
| '/docs/updates'
|
||||||
| '/docs/voice'
|
| '/docs/voice'
|
||||||
| '/docs/worktrees'
|
| '/docs/worktrees'
|
||||||
|
| '/blog/'
|
||||||
| '/docs/'
|
| '/docs/'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
@@ -189,6 +218,7 @@ export interface FileRouteTypes {
|
|||||||
| '/download'
|
| '/download'
|
||||||
| '/opencode'
|
| '/opencode'
|
||||||
| '/privacy'
|
| '/privacy'
|
||||||
|
| '/blog/$'
|
||||||
| '/docs/best-practices'
|
| '/docs/best-practices'
|
||||||
| '/docs/cli'
|
| '/docs/cli'
|
||||||
| '/docs/configuration'
|
| '/docs/configuration'
|
||||||
@@ -196,10 +226,12 @@ export interface FileRouteTypes {
|
|||||||
| '/docs/updates'
|
| '/docs/updates'
|
||||||
| '/docs/voice'
|
| '/docs/voice'
|
||||||
| '/docs/worktrees'
|
| '/docs/worktrees'
|
||||||
|
| '/blog'
|
||||||
| '/docs'
|
| '/docs'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/blog'
|
||||||
| '/changelog'
|
| '/changelog'
|
||||||
| '/claude-code'
|
| '/claude-code'
|
||||||
| '/codex'
|
| '/codex'
|
||||||
@@ -207,6 +239,7 @@ export interface FileRouteTypes {
|
|||||||
| '/download'
|
| '/download'
|
||||||
| '/opencode'
|
| '/opencode'
|
||||||
| '/privacy'
|
| '/privacy'
|
||||||
|
| '/blog/$'
|
||||||
| '/docs/best-practices'
|
| '/docs/best-practices'
|
||||||
| '/docs/cli'
|
| '/docs/cli'
|
||||||
| '/docs/configuration'
|
| '/docs/configuration'
|
||||||
@@ -214,11 +247,13 @@ export interface FileRouteTypes {
|
|||||||
| '/docs/updates'
|
| '/docs/updates'
|
||||||
| '/docs/voice'
|
| '/docs/voice'
|
||||||
| '/docs/worktrees'
|
| '/docs/worktrees'
|
||||||
|
| '/blog/'
|
||||||
| '/docs/'
|
| '/docs/'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
|
BlogRoute: typeof BlogRouteWithChildren
|
||||||
ChangelogRoute: typeof ChangelogRoute
|
ChangelogRoute: typeof ChangelogRoute
|
||||||
ClaudeCodeRoute: typeof ClaudeCodeRoute
|
ClaudeCodeRoute: typeof ClaudeCodeRoute
|
||||||
CodexRoute: typeof CodexRoute
|
CodexRoute: typeof CodexRoute
|
||||||
@@ -279,6 +314,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ChangelogRouteImport
|
preLoaderRoute: typeof ChangelogRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/blog': {
|
||||||
|
id: '/blog'
|
||||||
|
path: '/blog'
|
||||||
|
fullPath: '/blog'
|
||||||
|
preLoaderRoute: typeof BlogRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/': {
|
'/': {
|
||||||
id: '/'
|
id: '/'
|
||||||
path: '/'
|
path: '/'
|
||||||
@@ -293,6 +335,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof DocsIndexRouteImport
|
preLoaderRoute: typeof DocsIndexRouteImport
|
||||||
parentRoute: typeof DocsRoute
|
parentRoute: typeof DocsRoute
|
||||||
}
|
}
|
||||||
|
'/blog/': {
|
||||||
|
id: '/blog/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/blog/'
|
||||||
|
preLoaderRoute: typeof BlogIndexRouteImport
|
||||||
|
parentRoute: typeof BlogRoute
|
||||||
|
}
|
||||||
'/docs/worktrees': {
|
'/docs/worktrees': {
|
||||||
id: '/docs/worktrees'
|
id: '/docs/worktrees'
|
||||||
path: '/worktrees'
|
path: '/worktrees'
|
||||||
@@ -342,9 +391,28 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof DocsBestPracticesRouteImport
|
preLoaderRoute: typeof DocsBestPracticesRouteImport
|
||||||
parentRoute: typeof DocsRoute
|
parentRoute: typeof DocsRoute
|
||||||
}
|
}
|
||||||
|
'/blog/$': {
|
||||||
|
id: '/blog/$'
|
||||||
|
path: '/$'
|
||||||
|
fullPath: '/blog/$'
|
||||||
|
preLoaderRoute: typeof BlogSplatRouteImport
|
||||||
|
parentRoute: typeof BlogRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BlogRouteChildren {
|
||||||
|
BlogSplatRoute: typeof BlogSplatRoute
|
||||||
|
BlogIndexRoute: typeof BlogIndexRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlogRouteChildren: BlogRouteChildren = {
|
||||||
|
BlogSplatRoute: BlogSplatRoute,
|
||||||
|
BlogIndexRoute: BlogIndexRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlogRouteWithChildren = BlogRoute._addFileChildren(BlogRouteChildren)
|
||||||
|
|
||||||
interface DocsRouteChildren {
|
interface DocsRouteChildren {
|
||||||
DocsBestPracticesRoute: typeof DocsBestPracticesRoute
|
DocsBestPracticesRoute: typeof DocsBestPracticesRoute
|
||||||
DocsCliRoute: typeof DocsCliRoute
|
DocsCliRoute: typeof DocsCliRoute
|
||||||
@@ -371,6 +439,7 @@ const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren)
|
|||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
|
BlogRoute: BlogRouteWithChildren,
|
||||||
ChangelogRoute: ChangelogRoute,
|
ChangelogRoute: ChangelogRoute,
|
||||||
ClaudeCodeRoute: ClaudeCodeRoute,
|
ClaudeCodeRoute: ClaudeCodeRoute,
|
||||||
CodexRoute: CodexRoute,
|
CodexRoute: CodexRoute,
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
||||||
|
import { SiteHeader } from "~/components/site-header";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/blog")({
|
||||||
|
component: BlogLayout,
|
||||||
|
});
|
||||||
|
|
||||||
|
function BlogLayout() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<div className="max-w-prose mx-auto p-6 md:p-12">
|
||||||
|
<div className="mb-8">
|
||||||
|
<SiteHeader />
|
||||||
|
</div>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { getPost, formatDate } from "~/posts";
|
||||||
|
import { pageMeta } from "~/meta";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/blog/$")({
|
||||||
|
head: ({ params }) => {
|
||||||
|
const slug = params._splat ?? "";
|
||||||
|
const post = getPost(slug);
|
||||||
|
if (!post) return { meta: pageMeta("Not Found - Paseo", "Post not found.") };
|
||||||
|
return {
|
||||||
|
meta: pageMeta(`${post.frontmatter.title} - Paseo`, post.frontmatter.description),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
component: BlogPost,
|
||||||
|
});
|
||||||
|
|
||||||
|
function BlogPost() {
|
||||||
|
const { _splat } = Route.useParams();
|
||||||
|
const slug = _splat ?? "";
|
||||||
|
const post = getPost(slug);
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
return <p className="text-muted-foreground">Post not found.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-4xl font-bold mb-2">{post.frontmatter.title}</h1>
|
||||||
|
<span className="text-lg text-muted-foreground">
|
||||||
|
{formatDate(new Date(post.frontmatter.date))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="blog-prose">
|
||||||
|
<ReactMarkdown>{post.content}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
|
import { getPosts, formatDate } from "~/posts";
|
||||||
|
import { pageMeta } from "~/meta";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/blog/")({
|
||||||
|
validateSearch: (search: Record<string, unknown>) => ({
|
||||||
|
drafts: search.drafts !== undefined,
|
||||||
|
}),
|
||||||
|
head: () => ({
|
||||||
|
meta: pageMeta("Blog - Paseo", "Updates, thoughts, and announcements from the Paseo team."),
|
||||||
|
}),
|
||||||
|
component: BlogIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
function BlogIndex() {
|
||||||
|
const { drafts } = Route.useSearch();
|
||||||
|
const posts = getPosts(drafts);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{drafts && (
|
||||||
|
<div className="mb-6 p-4 bg-primary/10 rounded border-l-4 border-primary">
|
||||||
|
<p className="text-sm text-foreground/80">Showing draft posts</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{posts.map(({ slug, frontmatter }) => (
|
||||||
|
<div
|
||||||
|
key={slug}
|
||||||
|
className="flex flex-col-reverse items-start md:flex-row md:items-center gap-x-4"
|
||||||
|
>
|
||||||
|
<span className="text-lg text-muted-foreground tabular-nums">
|
||||||
|
{formatDate(new Date(frontmatter.date))}
|
||||||
|
</span>
|
||||||
|
<Link
|
||||||
|
to="/blog/$"
|
||||||
|
params={{ _splat: slug }}
|
||||||
|
className="text-lg text-foreground hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
{frontmatter.title}
|
||||||
|
{frontmatter.draft && (
|
||||||
|
<span className="ml-2 text-xs px-2 py-1 bg-primary/20 text-primary rounded">
|
||||||
|
DRAFT
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{posts.length === 0 && <p className="text-muted-foreground">No posts yet.</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import changelogMarkdown from "../../../../CHANGELOG.md?raw";
|
import changelogMarkdown from "../../../../CHANGELOG.md?raw";
|
||||||
import { pageMeta } from "~/meta";
|
import { pageMeta } from "~/meta";
|
||||||
import "~/styles.css";
|
import { SiteHeader } from "~/components/site-header";
|
||||||
|
|
||||||
export const Route = createFileRoute("/changelog")({
|
export const Route = createFileRoute("/changelog")({
|
||||||
head: () => ({
|
head: () => ({
|
||||||
@@ -18,56 +18,9 @@ function Changelog() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<div className="max-w-4xl mx-auto p-6 md:p-12">
|
<div className="max-w-4xl mx-auto p-6 md:p-12">
|
||||||
<header className="flex items-center justify-between gap-4 mb-8">
|
<div className="mb-8">
|
||||||
<Link to="/" className="flex items-center gap-3">
|
<SiteHeader />
|
||||||
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
|
</div>
|
||||||
<span className="text-lg font-medium">Paseo</span>
|
|
||||||
</Link>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Link
|
|
||||||
to="/docs"
|
|
||||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
>
|
|
||||||
Docs
|
|
||||||
</Link>
|
|
||||||
<a
|
|
||||||
href="https://discord.gg/jz8T2uahpH"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="Discord"
|
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
role="img"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://github.com/getpaseo/paseo"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="GitHub"
|
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path d="M12 0C5.37 0 0 5.484 0 12.252c0 5.418 3.438 10.013 8.205 11.637.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.738-4.042-1.61-4.042-1.61-.546-1.403-1.333-1.776-1.333-1.776-1.089-.756.084-.741.084-.741 1.205.087 1.838 1.262 1.838 1.262 1.07 1.87 2.809 1.33 3.495 1.017.108-.79.417-1.33.76-1.636-2.665-.31-5.467-1.35-5.467-6.005 0-1.327.465-2.413 1.235-3.262-.124-.31-.535-1.556.117-3.243 0 0 1.008-.33 3.3 1.248a11.2 11.2 0 0 1 3.003-.404c1.02.005 2.045.138 3.003.404 2.29-1.578 3.297-1.248 3.297-1.248.653 1.687.242 2.933.118 3.243.77.85 1.233 1.935 1.233 3.262 0 4.667-2.807 5.692-5.48 5.995.43.38.823 1.133.823 2.285 0 1.65-.015 2.98-.015 3.386 0 .315.218.694.825.576C20.565 22.26 24 17.667 24 12.252 24 5.484 18.627 0 12 0z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<article className="changelog-markdown rounded-xl border border-border bg-card/40 p-6 md:p-8">
|
<article className="changelog-markdown rounded-xl border border-border bg-card/40 p-6 md:p-8">
|
||||||
<ReactMarkdown>{changelogMarkdown}</ReactMarkdown>
|
<ReactMarkdown>{changelogMarkdown}</ReactMarkdown>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -80,6 +80,112 @@
|
|||||||
color: rgba(250, 250, 250, 0.8);
|
color: rgba(250, 250, 250, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Blog post prose styles — adapted from moboudra.com */
|
||||||
|
.blog-prose {
|
||||||
|
color: rgba(250, 250, 250, 0.82);
|
||||||
|
line-height: 1.8;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose h1,
|
||||||
|
.blog-prose h2,
|
||||||
|
.blog-prose h3 {
|
||||||
|
color: var(--color-foreground);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: color-mix(in srgb, var(--color-primary) 50%, transparent);
|
||||||
|
text-decoration-thickness: 2px;
|
||||||
|
text-underline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose h1,
|
||||||
|
.blog-prose h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose p {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose a {
|
||||||
|
color: var(--color-foreground);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose a:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose ul,
|
||||||
|
.blog-prose ol {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose ul {
|
||||||
|
list-style: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose ol {
|
||||||
|
list-style: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose li {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose blockquote {
|
||||||
|
border-left: 3px solid var(--color-border);
|
||||||
|
padding-left: 1rem;
|
||||||
|
color: rgba(250, 250, 250, 0.6);
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose pre {
|
||||||
|
background-color: var(--color-card);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose :not(pre) > code {
|
||||||
|
background-color: var(--color-muted);
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose img {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-prose hr {
|
||||||
|
border-color: var(--color-border);
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-background: #0a0a0a;
|
--color-background: #0a0a0a;
|
||||||
--color-foreground: #fafafa;
|
--color-foreground: #fafafa;
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -7,6 +7,26 @@ import react from "@vitejs/plugin-react";
|
|||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
const repoRoot = path.resolve(__dirname, "../..");
|
const repoRoot = path.resolve(__dirname, "../..");
|
||||||
|
const siteHost = "https://paseo.sh";
|
||||||
|
const sitemapPages = [
|
||||||
|
"/",
|
||||||
|
"/changelog",
|
||||||
|
"/claude-code",
|
||||||
|
"/codex",
|
||||||
|
"/docs",
|
||||||
|
"/download",
|
||||||
|
"/opencode",
|
||||||
|
"/privacy",
|
||||||
|
"/docs/best-practices",
|
||||||
|
"/docs/cli",
|
||||||
|
"/docs/configuration",
|
||||||
|
"/docs/security",
|
||||||
|
"/docs/updates",
|
||||||
|
"/docs/voice",
|
||||||
|
"/docs/worktrees",
|
||||||
|
].map((routePath) => ({
|
||||||
|
path: routePath,
|
||||||
|
}));
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
@@ -18,7 +38,12 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
cloudflare({ viteEnvironment: { name: "ssr" } }),
|
cloudflare({ viteEnvironment: { name: "ssr" } }),
|
||||||
tsConfigPaths(),
|
tsConfigPaths(),
|
||||||
tanstackStart(),
|
tanstackStart({
|
||||||
|
pages: sitemapPages,
|
||||||
|
sitemap: {
|
||||||
|
host: siteHost,
|
||||||
|
},
|
||||||
|
}),
|
||||||
react(),
|
react(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user