mirror of
https://github.com/coder/coder.git
synced 2026-11-02 21:50:08 +08:00
feat: add AI chat system with agent tools and chat UI Introduce the chatd subsystem and Agents UI for AI-powered chat within Coder workspaces. - Add chatd package with chat loop, message compaction, prompt management, and LLM provider integration (OpenAI, Anthropic) - Add agent tools: create workspace, list/read templates, read/write/ edit files, execute commands - Add chat API endpoints with streaming, message editing, and durable reconnection - Add database schema and migrations for chats, chat messages, chat providers, and chat model configs - Add RBAC policies and dbauthz enforcement for chat resources - Add Agents UI pages with conversation timeline, queued messages list, diff viewer, and model configuration panel - Add comprehensive test coverage including coderd integration tests, chatd unit tests, and Storybook stories - Gate feature behind experiments flag --------- Co-authored-by: Cian Johnston <cian@coder.com> Co-authored-by: Danielle Maywood <danielle@themaywoods.com> Co-authored-by: Jeremy Ruppel <jeremy@coder.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
183 lines
4.3 KiB
TypeScript
183 lines
4.3 KiB
TypeScript
import dayjs, { type Dayjs } from "dayjs";
|
|
import duration from "dayjs/plugin/duration";
|
|
import relativeTimePlugin from "dayjs/plugin/relativeTime";
|
|
import timezone from "dayjs/plugin/timezone";
|
|
import utc from "dayjs/plugin/utc";
|
|
import humanizeDuration from "humanize-duration";
|
|
|
|
// Load required plugins
|
|
dayjs.extend(duration);
|
|
dayjs.extend(relativeTimePlugin);
|
|
dayjs.extend(utc);
|
|
dayjs.extend(timezone);
|
|
|
|
// Time conversion constants
|
|
const TIME_CONSTANTS = {
|
|
MS_PER_SECOND: 1000,
|
|
MS_PER_MINUTE: 60 * 1000,
|
|
MS_PER_HOUR: 60 * 60 * 1000,
|
|
MS_PER_DAY: 24 * 60 * 60 * 1000,
|
|
NS_PER_MS: 1e6,
|
|
SECONDS_PER_MINUTE: 60,
|
|
MINUTES_PER_HOUR: 60,
|
|
HOURS_PER_DAY: 24,
|
|
};
|
|
|
|
export type TimeUnit = "days" | "hours";
|
|
type DateTimeInput = Date | string | number | Dayjs | null | undefined;
|
|
|
|
// Standard format strings
|
|
// https://day.js.org/docs/en/display/format
|
|
export const DATE_FORMAT = {
|
|
ISO_DATE: "YYYY-MM-DD",
|
|
ISO_DATETIME: "YYYY-MM-DD HH:mm:ss",
|
|
FULL_DATE: "MMMM D, YYYY",
|
|
MEDIUM_DATE: "MMM D, YYYY",
|
|
FULL_DATETIME: "MMMM D, YYYY h:mm A",
|
|
SHORT_DATE: "MM/DD/YYYY",
|
|
TIME_24H: "HH:mm:ss",
|
|
TIME_12H: "h:mm A",
|
|
UTC_OFFSET: "Z",
|
|
} as const;
|
|
|
|
// Format functions
|
|
export function formatDateTime(
|
|
date: DateTimeInput,
|
|
format: string = DATE_FORMAT.ISO_DATETIME,
|
|
) {
|
|
return dayjs(date).format(format);
|
|
}
|
|
|
|
const defaultDateLocaleOptions: Intl.DateTimeFormatOptions = {
|
|
second: "2-digit",
|
|
minute: "2-digit",
|
|
hour: "2-digit",
|
|
day: "numeric",
|
|
month: "short",
|
|
year: "numeric",
|
|
};
|
|
|
|
export function formatDate(
|
|
date: Date,
|
|
options?: { locale?: Intl.LocalesArgument } & Intl.DateTimeFormatOptions,
|
|
) {
|
|
return date.toLocaleDateString(options?.locale, {
|
|
...defaultDateLocaleOptions,
|
|
...options,
|
|
});
|
|
}
|
|
|
|
// Duration functions
|
|
export function humanDuration(durationInMs: number) {
|
|
return humanizeDuration(durationInMs, {
|
|
conjunction: " and ",
|
|
serialComma: false,
|
|
round: true,
|
|
units: ["y", "mo", "w", "d", "h", "m", "s", "ms"],
|
|
largest: 3,
|
|
});
|
|
}
|
|
|
|
export function durationInHours(durationMs: number): number {
|
|
return durationMs / TIME_CONSTANTS.MS_PER_HOUR;
|
|
}
|
|
|
|
export function durationInDays(durationMs: number): number {
|
|
return durationMs / TIME_CONSTANTS.MS_PER_DAY;
|
|
}
|
|
|
|
export function suggestedTimeUnit(duration: number): TimeUnit {
|
|
if (duration === 0) {
|
|
return "hours";
|
|
}
|
|
|
|
return Number.isInteger(durationInDays(duration)) ? "days" : "hours";
|
|
}
|
|
|
|
// Relative time functions
|
|
export function relativeTime(date: DateTimeInput) {
|
|
return dayjs(date).fromNow();
|
|
}
|
|
|
|
/**
|
|
* Returns a compact relative time string like "now", "5m", "2h",
|
|
* "3d", "1w", "2mo", or "1y". Useful for tight UI spaces like
|
|
* sidebar timestamps.
|
|
*/
|
|
export function shortRelativeTime(date: DateTimeInput): string {
|
|
const now = dayjs();
|
|
const then = dayjs(date);
|
|
const diffSeconds = now.diff(then, "second");
|
|
|
|
if (diffSeconds < 60) {
|
|
return "now";
|
|
}
|
|
const diffMinutes = now.diff(then, "minute");
|
|
if (diffMinutes < 60) {
|
|
return `${diffMinutes}m`;
|
|
}
|
|
const diffHours = now.diff(then, "hour");
|
|
if (diffHours < 24) {
|
|
return `${diffHours}h`;
|
|
}
|
|
const diffDays = now.diff(then, "day");
|
|
if (diffDays < 7) {
|
|
return `${diffDays}d`;
|
|
}
|
|
const diffWeeks = now.diff(then, "week");
|
|
if (diffWeeks < 5) {
|
|
return `${diffWeeks}w`;
|
|
}
|
|
const diffMonths = now.diff(then, "month");
|
|
if (diffMonths < 12) {
|
|
return `${diffMonths}mo`;
|
|
}
|
|
const diffYears = now.diff(then, "year");
|
|
return `${diffYears}y`;
|
|
}
|
|
|
|
export function relativeTimeWithoutSuffix(date: DateTimeInput) {
|
|
return dayjs(date).fromNow(true);
|
|
}
|
|
|
|
export function timeFrom(
|
|
date: DateTimeInput,
|
|
referenceDate: DateTimeInput = new Date(),
|
|
) {
|
|
return dayjs(date).from(dayjs(referenceDate));
|
|
}
|
|
|
|
// Time manipulation functions
|
|
export function addTime(
|
|
date: DateTimeInput,
|
|
amount: number,
|
|
unit: dayjs.ManipulateType,
|
|
) {
|
|
return dayjs(date).add(amount, unit).toDate();
|
|
}
|
|
|
|
export function subtractTime(
|
|
date: DateTimeInput,
|
|
amount: number,
|
|
unit: dayjs.ManipulateType,
|
|
) {
|
|
return dayjs(date).subtract(amount, unit).toDate();
|
|
}
|
|
|
|
export function startOfDay(date: DateTimeInput) {
|
|
return dayjs(date).startOf("day").toDate();
|
|
}
|
|
|
|
export function startOfHour(date: DateTimeInput) {
|
|
return dayjs(date).startOf("hour").toDate();
|
|
}
|
|
|
|
export function daysAgo(count: number) {
|
|
return dayjs().subtract(count, "day").toISOString();
|
|
}
|
|
|
|
// Date comparison functions
|
|
export function isAfter(date1: DateTimeInput, date2: DateTimeInput) {
|
|
return dayjs(date1).isAfter(dayjs(date2));
|
|
}
|