Add global live feed for task comments

This commit is contained in:
Abhimanyu Saharan
2026-02-07 05:26:15 +05:30
parent 844b521d00
commit b2109da88b
16 changed files with 1518 additions and 122 deletions

View File

@@ -20,7 +20,10 @@ import type {
import type {
HTTPValidationError,
LimitOffsetPageTypeVarCustomizedActivityEventRead,
LimitOffsetPageTypeVarCustomizedActivityTaskCommentFeedItemRead,
ListActivityApiV1ActivityGetParams,
ListTaskCommentFeedApiV1ActivityTaskCommentsGetParams,
StreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetParams,
} from ".././model";
import { customFetch } from "../../mutator";
@@ -236,3 +239,533 @@ export function useListActivityApiV1ActivityGet<
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List Task Comment Feed
*/
export type listTaskCommentFeedApiV1ActivityTaskCommentsGetResponse200 = {
data: LimitOffsetPageTypeVarCustomizedActivityTaskCommentFeedItemRead;
status: 200;
};
export type listTaskCommentFeedApiV1ActivityTaskCommentsGetResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type listTaskCommentFeedApiV1ActivityTaskCommentsGetResponseSuccess =
listTaskCommentFeedApiV1ActivityTaskCommentsGetResponse200 & {
headers: Headers;
};
export type listTaskCommentFeedApiV1ActivityTaskCommentsGetResponseError =
listTaskCommentFeedApiV1ActivityTaskCommentsGetResponse422 & {
headers: Headers;
};
export type listTaskCommentFeedApiV1ActivityTaskCommentsGetResponse =
| listTaskCommentFeedApiV1ActivityTaskCommentsGetResponseSuccess
| listTaskCommentFeedApiV1ActivityTaskCommentsGetResponseError;
export const getListTaskCommentFeedApiV1ActivityTaskCommentsGetUrl = (
params?: ListTaskCommentFeedApiV1ActivityTaskCommentsGetParams,
) => {
const normalizedParams = new URLSearchParams();
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? "null" : value.toString());
}
});
const stringifiedParams = normalizedParams.toString();
return stringifiedParams.length > 0
? `/api/v1/activity/task-comments?${stringifiedParams}`
: `/api/v1/activity/task-comments`;
};
export const listTaskCommentFeedApiV1ActivityTaskCommentsGet = async (
params?: ListTaskCommentFeedApiV1ActivityTaskCommentsGetParams,
options?: RequestInit,
): Promise<listTaskCommentFeedApiV1ActivityTaskCommentsGetResponse> => {
return customFetch<listTaskCommentFeedApiV1ActivityTaskCommentsGetResponse>(
getListTaskCommentFeedApiV1ActivityTaskCommentsGetUrl(params),
{
...options,
method: "GET",
},
);
};
export const getListTaskCommentFeedApiV1ActivityTaskCommentsGetQueryKey = (
params?: ListTaskCommentFeedApiV1ActivityTaskCommentsGetParams,
) => {
return [
`/api/v1/activity/task-comments`,
...(params ? [params] : []),
] as const;
};
export const getListTaskCommentFeedApiV1ActivityTaskCommentsGetQueryOptions = <
TData = Awaited<
ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>
>,
TError = HTTPValidationError,
>(
params?: ListTaskCommentFeedApiV1ActivityTaskCommentsGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getListTaskCommentFeedApiV1ActivityTaskCommentsGetQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>>
> = ({ signal }) =>
listTaskCommentFeedApiV1ActivityTaskCommentsGet(params, {
signal,
...requestOptions,
});
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type ListTaskCommentFeedApiV1ActivityTaskCommentsGetQueryResult =
NonNullable<
Awaited<ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>>
>;
export type ListTaskCommentFeedApiV1ActivityTaskCommentsGetQueryError =
HTTPValidationError;
export function useListTaskCommentFeedApiV1ActivityTaskCommentsGet<
TData = Awaited<
ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>
>,
TError = HTTPValidationError,
>(
params: undefined | ListTaskCommentFeedApiV1ActivityTaskCommentsGetParams,
options: {
query: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>
>,
TError,
TData
>
> &
Pick<
DefinedInitialDataOptions<
Awaited<
ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>
>,
TError,
Awaited<
ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>
>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListTaskCommentFeedApiV1ActivityTaskCommentsGet<
TData = Awaited<
ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>
>,
TError = HTTPValidationError,
>(
params?: ListTaskCommentFeedApiV1ActivityTaskCommentsGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>
>,
TError,
TData
>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<
ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>
>,
TError,
Awaited<
ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>
>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListTaskCommentFeedApiV1ActivityTaskCommentsGet<
TData = Awaited<
ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>
>,
TError = HTTPValidationError,
>(
params?: ListTaskCommentFeedApiV1ActivityTaskCommentsGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary List Task Comment Feed
*/
export function useListTaskCommentFeedApiV1ActivityTaskCommentsGet<
TData = Awaited<
ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>
>,
TError = HTTPValidationError,
>(
params?: ListTaskCommentFeedApiV1ActivityTaskCommentsGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof listTaskCommentFeedApiV1ActivityTaskCommentsGet>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions =
getListTaskCommentFeedApiV1ActivityTaskCommentsGetQueryOptions(
params,
options,
);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Stream Task Comment Feed
*/
export type streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetResponse200 =
{
data: unknown;
status: 200;
};
export type streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetResponse422 =
{
data: HTTPValidationError;
status: 422;
};
export type streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetResponseSuccess =
streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetResponse200 & {
headers: Headers;
};
export type streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetResponseError =
streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetResponse422 & {
headers: Headers;
};
export type streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetResponse =
| streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetResponseSuccess
| streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetResponseError;
export const getStreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetUrl = (
params?: StreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetParams,
) => {
const normalizedParams = new URLSearchParams();
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? "null" : value.toString());
}
});
const stringifiedParams = normalizedParams.toString();
return stringifiedParams.length > 0
? `/api/v1/activity/task-comments/stream?${stringifiedParams}`
: `/api/v1/activity/task-comments/stream`;
};
export const streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet = async (
params?: StreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetParams,
options?: RequestInit,
): Promise<streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetResponse> => {
return customFetch<streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetResponse>(
getStreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetUrl(params),
{
...options,
method: "GET",
},
);
};
export const getStreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetQueryKey =
(params?: StreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetParams) => {
return [
`/api/v1/activity/task-comments/stream`,
...(params ? [params] : []),
] as const;
};
export const getStreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetQueryOptions =
<
TData = Awaited<
ReturnType<typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet>
>,
TError = HTTPValidationError,
>(
params?: StreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<
typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet
>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getStreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetQueryKey(
params,
);
const queryFn: QueryFunction<
Awaited<
ReturnType<
typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet
>
>
> = ({ signal }) =>
streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet(params, {
signal,
...requestOptions,
});
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<
ReturnType<
typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet
>
>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type StreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetQueryResult =
NonNullable<
Awaited<
ReturnType<typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet>
>
>;
export type StreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetQueryError =
HTTPValidationError;
export function useStreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet<
TData = Awaited<
ReturnType<typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet>
>,
TError = HTTPValidationError,
>(
params:
| undefined
| StreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetParams,
options: {
query: Partial<
UseQueryOptions<
Awaited<
ReturnType<
typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet
>
>,
TError,
TData
>
> &
Pick<
DefinedInitialDataOptions<
Awaited<
ReturnType<
typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet
>
>,
TError,
Awaited<
ReturnType<
typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet
>
>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useStreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet<
TData = Awaited<
ReturnType<typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet>
>,
TError = HTTPValidationError,
>(
params?: StreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<
typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet
>
>,
TError,
TData
>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<
ReturnType<
typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet
>
>,
TError,
Awaited<
ReturnType<
typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet
>
>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useStreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet<
TData = Awaited<
ReturnType<typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet>
>,
TError = HTTPValidationError,
>(
params?: StreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<
typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet
>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary Stream Task Comment Feed
*/
export function useStreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet<
TData = Awaited<
ReturnType<typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet>
>,
TError = HTTPValidationError,
>(
params?: StreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<
typeof streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet
>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions =
getStreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetQueryOptions(
params,
options,
);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
return { ...query, queryKey: queryOptions.queryKey };
}

View File

@@ -28,6 +28,7 @@ import type {
GatewaySessionMessageRequest,
GatewaySessionResponse,
GatewaySessionsResponse,
GatewayTemplatesSyncResult,
GatewayUpdate,
GatewaysStatusApiV1GatewaysStatusGetParams,
GatewaysStatusResponse,
@@ -39,6 +40,7 @@ import type {
ListGatewaysApiV1GatewaysGetParams,
OkResponse,
SendGatewaySessionMessageApiV1GatewaysSessionsSessionIdMessagePostParams,
SyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostParams,
} from ".././model";
import { customFetch } from "../../mutator";
@@ -2229,3 +2231,192 @@ export const useUpdateGatewayApiV1GatewaysGatewayIdPatch = <
queryClient,
);
};
/**
* @summary Sync Gateway Templates
*/
export type syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostResponse200 =
{
data: GatewayTemplatesSyncResult;
status: 200;
};
export type syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostResponse422 =
{
data: HTTPValidationError;
status: 422;
};
export type syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostResponseSuccess =
syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostResponse200 & {
headers: Headers;
};
export type syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostResponseError =
syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostResponse422 & {
headers: Headers;
};
export type syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostResponse =
| syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostResponseSuccess
| syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostResponseError;
export const getSyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostUrl =
(
gatewayId: string,
params?: SyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostParams,
) => {
const normalizedParams = new URLSearchParams();
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(
key,
value === null ? "null" : value.toString(),
);
}
});
const stringifiedParams = normalizedParams.toString();
return stringifiedParams.length > 0
? `/api/v1/gateways/${gatewayId}/templates/sync?${stringifiedParams}`
: `/api/v1/gateways/${gatewayId}/templates/sync`;
};
export const syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPost =
async (
gatewayId: string,
params?: SyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostParams,
options?: RequestInit,
): Promise<syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostResponse> => {
return customFetch<syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostResponse>(
getSyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostUrl(
gatewayId,
params,
),
{
...options,
method: "POST",
},
);
};
export const getSyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostMutationOptions =
<TError = HTTPValidationError, TContext = unknown>(options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<
typeof syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPost
>
>,
TError,
{
gatewayId: string;
params?: SyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostParams;
},
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<
ReturnType<
typeof syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPost
>
>,
TError,
{
gatewayId: string;
params?: SyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostParams;
},
TContext
> => {
const mutationKey = [
"syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPost",
];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<
ReturnType<
typeof syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPost
>
>,
{
gatewayId: string;
params?: SyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostParams;
}
> = (props) => {
const { gatewayId, params } = props ?? {};
return syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPost(
gatewayId,
params,
requestOptions,
);
};
return { mutationFn, ...mutationOptions };
};
export type SyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostMutationResult =
NonNullable<
Awaited<
ReturnType<
typeof syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPost
>
>
>;
export type SyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostMutationError =
HTTPValidationError;
/**
* @summary Sync Gateway Templates
*/
export const useSyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPost = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<
typeof syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPost
>
>,
TError,
{
gatewayId: string;
params?: SyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostParams;
},
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<
ReturnType<
typeof syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPost
>
>,
TError,
{
gatewayId: string;
params?: SyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostParams;
},
TContext
> => {
return useMutation(
getSyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostMutationOptions(
options,
),
queryClient,
);
};

View File

@@ -0,0 +1,19 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export interface ActivityTaskCommentFeedItemRead {
agent_id: string | null;
agent_name?: string | null;
agent_role?: string | null;
board_id: string;
board_name: string;
created_at: string;
id: string;
message: string | null;
task_id: string;
task_title: string;
}

View File

@@ -0,0 +1,13 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export interface GatewayTemplatesSyncError {
agent_id?: string | null;
agent_name?: string | null;
board_id?: string | null;
message: string;
}

View File

@@ -0,0 +1,17 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import type { GatewayTemplatesSyncError } from "./gatewayTemplatesSyncError";
export interface GatewayTemplatesSyncResult {
agents_skipped: number;
agents_updated: number;
errors?: GatewayTemplatesSyncError[];
gateway_id: string;
include_main: boolean;
main_updated: boolean;
reset_sessions: boolean;
}

View File

@@ -6,6 +6,7 @@
*/
export * from "./activityEventRead";
export * from "./activityTaskCommentFeedItemRead";
export * from "./agentCreate";
export * from "./agentCreateHeartbeatConfig";
export * from "./agentCreateIdentityProfile";
@@ -86,6 +87,8 @@ export * from "./gatewaysStatusApiV1GatewaysStatusGetParams";
export * from "./gatewaysStatusResponse";
export * from "./gatewayStatusApiV1GatewayStatusGet200";
export * from "./gatewayStatusApiV1GatewayStatusGetParams";
export * from "./gatewayTemplatesSyncError";
export * from "./gatewayTemplatesSyncResult";
export * from "./gatewayUpdate";
export * from "./getGatewaySessionApiV1GatewaySessionsSessionIdGet200";
export * from "./getGatewaySessionApiV1GatewaySessionsSessionIdGetParams";
@@ -97,6 +100,7 @@ export * from "./healthHealthGet200";
export * from "./healthzHealthzGet200";
export * from "./hTTPValidationError";
export * from "./limitOffsetPageTypeVarCustomizedActivityEventRead";
export * from "./limitOffsetPageTypeVarCustomizedActivityTaskCommentFeedItemRead";
export * from "./limitOffsetPageTypeVarCustomizedAgentRead";
export * from "./limitOffsetPageTypeVarCustomizedApprovalRead";
export * from "./limitOffsetPageTypeVarCustomizedBoardMemoryRead";
@@ -117,6 +121,7 @@ export * from "./listGatewaysApiV1GatewaysGetParams";
export * from "./listGatewaySessionsApiV1GatewaysSessionsGetParams";
export * from "./listSessionsApiV1GatewaySessionsGet200";
export * from "./listSessionsApiV1GatewaySessionsGetParams";
export * from "./listTaskCommentFeedApiV1ActivityTaskCommentsGetParams";
export * from "./listTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams";
export * from "./listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams";
export * from "./listTasksApiV1AgentBoardsBoardIdTasksGetParams";
@@ -130,7 +135,9 @@ export * from "./sendSessionMessageApiV1GatewaySessionsSessionIdMessagePostParam
export * from "./streamAgentsApiV1AgentsStreamGetParams";
export * from "./streamApprovalsApiV1BoardsBoardIdApprovalsStreamGetParams";
export * from "./streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams";
export * from "./streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetParams";
export * from "./streamTasksApiV1BoardsBoardIdTasksStreamGetParams";
export * from "./syncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostParams";
export * from "./taskCardRead";
export * from "./taskCardReadStatus";
export * from "./taskCommentCreate";

View File

@@ -0,0 +1,17 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import type { ActivityTaskCommentFeedItemRead } from "./activityTaskCommentFeedItemRead";
export interface LimitOffsetPageTypeVarCustomizedActivityTaskCommentFeedItemRead {
items: ActivityTaskCommentFeedItemRead[];
/** @minimum 1 */
limit: number;
/** @minimum 0 */
offset: number;
/** @minimum 0 */
total: number;
}

View File

@@ -0,0 +1,19 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type ListTaskCommentFeedApiV1ActivityTaskCommentsGetParams = {
board_id?: string | null;
/**
* @minimum 1
* @maximum 200
*/
limit?: number;
/**
* @minimum 0
*/
offset?: number;
};

View File

@@ -0,0 +1,11 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type StreamTaskCommentFeedApiV1ActivityTaskCommentsStreamGetParams = {
board_id?: string | null;
since?: string | null;
};

View File

@@ -0,0 +1,15 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type SyncGatewayTemplatesApiV1GatewaysGatewayIdTemplatesSyncPostParams =
{
include_main?: boolean;
reset_sessions?: boolean;
rotate_tokens?: boolean;
force_bootstrap?: boolean;
board_id?: string | null;
};

View File

@@ -0,0 +1,356 @@
"use client";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import Link from "next/link";
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
import { ArrowUpRight, Activity as ActivityIcon } from "lucide-react";
import { ApiError } from "@/api/mutator";
import {
type listTaskCommentFeedApiV1ActivityTaskCommentsGetResponse,
streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet,
useListTaskCommentFeedApiV1ActivityTaskCommentsGet,
} from "@/api/generated/activity/activity";
import type { ActivityTaskCommentFeedItemRead } from "@/api/generated/model";
import { Markdown } from "@/components/atoms/Markdown";
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar";
import { DashboardShell } from "@/components/templates/DashboardShell";
import { Button } from "@/components/ui/button";
import { createExponentialBackoff } from "@/lib/backoff";
import { apiDatetimeToMs, parseApiDatetime } from "@/lib/datetime";
import { cn } from "@/lib/utils";
const SSE_RECONNECT_BACKOFF = {
baseMs: 1_000,
factor: 2,
jitter: 0.2,
maxMs: 5 * 60_000,
} as const;
const formatShortTimestamp = (value: string) => {
const date = parseApiDatetime(value);
if (!date) return "—";
return date.toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const latestTimestamp = (items: ActivityTaskCommentFeedItemRead[]) => {
let latest = 0;
for (const item of items) {
const time = apiDatetimeToMs(item.created_at) ?? 0;
latest = Math.max(latest, time);
}
return latest ? new Date(latest).toISOString() : null;
};
const FeedCard = memo(function FeedCard({
item,
}: {
item: ActivityTaskCommentFeedItemRead;
}) {
const message = (item.message ?? "").trim();
const authorName = item.agent_name?.trim() || "Admin";
const authorRole = item.agent_role?.trim() || null;
const authorAvatar = (authorName[0] ?? "A").toUpperCase();
const taskHref = `/boards/${item.board_id}?taskId=${item.task_id}`;
const boardHref = `/boards/${item.board_id}`;
return (
<div className="rounded-xl border border-slate-200 bg-white p-4 transition hover:border-slate-300">
<div className="flex items-start gap-3">
<div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-slate-100 text-xs font-semibold text-slate-700">
{authorAvatar}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<Link
href={taskHref}
className={cn(
"block text-sm font-semibold leading-snug text-slate-900 transition hover:text-slate-950 hover:underline",
)}
title={item.task_title}
style={{
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
>
{item.task_title}
</Link>
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-slate-500">
<Link
href={boardHref}
className="font-semibold text-slate-700 hover:text-slate-900 hover:underline"
>
{item.board_name}
</Link>
<span className="text-slate-300">·</span>
<span className="font-medium text-slate-700">{authorName}</span>
{authorRole ? (
<>
<span className="text-slate-300">·</span>
<span className="text-slate-500">{authorRole}</span>
</>
) : null}
<span className="text-slate-300">·</span>
<span className="text-slate-400">
{formatShortTimestamp(item.created_at)}
</span>
</div>
</div>
<Link
href={taskHref}
className="inline-flex flex-shrink-0 items-center gap-1 rounded-md px-2 py-1 text-[11px] font-semibold text-slate-600 transition hover:bg-slate-50 hover:text-slate-900"
aria-label="View task"
>
View task
<ArrowUpRight className="h-3 w-3" />
</Link>
</div>
</div>
</div>
{message ? (
<div className="mt-3 select-text cursor-text text-sm leading-relaxed text-slate-900 break-words">
<Markdown content={message} variant="basic" />
</div>
) : (
<p className="mt-3 text-sm text-slate-500"></p>
)}
</div>
);
});
FeedCard.displayName = "FeedCard";
export default function ActivityPage() {
const { isSignedIn } = useAuth();
const feedQuery = useListTaskCommentFeedApiV1ActivityTaskCommentsGet<
listTaskCommentFeedApiV1ActivityTaskCommentsGetResponse,
ApiError
>(
{ limit: 200 },
{
query: {
enabled: Boolean(isSignedIn),
refetchOnMount: "always",
refetchOnWindowFocus: false,
retry: false,
},
},
);
const [feedItems, setFeedItems] = useState<ActivityTaskCommentFeedItemRead[]>(
[],
);
const feedItemsRef = useRef<ActivityTaskCommentFeedItemRead[]>([]);
const seenIdsRef = useRef<Set<string>>(new Set());
const initializedRef = useRef(false);
useEffect(() => {
feedItemsRef.current = feedItems;
}, [feedItems]);
useEffect(() => {
if (initializedRef.current) return;
if (feedQuery.data?.status !== 200) return;
const items = feedQuery.data.data.items ?? [];
initializedRef.current = true;
setFeedItems((prev) => {
const map = new Map<string, ActivityTaskCommentFeedItemRead>();
[...prev, ...items].forEach((item) => map.set(item.id, item));
const merged = [...map.values()];
merged.sort((a, b) => {
const aTime = apiDatetimeToMs(a.created_at) ?? 0;
const bTime = apiDatetimeToMs(b.created_at) ?? 0;
return bTime - aTime;
});
const next = merged.slice(0, 200);
seenIdsRef.current = new Set(next.map((item) => item.id));
return next;
});
}, [feedQuery.data]);
const pushFeedItem = useCallback((item: ActivityTaskCommentFeedItemRead) => {
setFeedItems((prev) => {
if (seenIdsRef.current.has(item.id)) return prev;
seenIdsRef.current.add(item.id);
const next = [item, ...prev];
return next.slice(0, 200);
});
}, []);
useEffect(() => {
if (!isSignedIn) return;
let isCancelled = false;
const abortController = new AbortController();
const backoff = createExponentialBackoff(SSE_RECONNECT_BACKOFF);
let reconnectTimeout: number | undefined;
const connect = async () => {
try {
const since = latestTimestamp(feedItemsRef.current);
const streamResult =
await streamTaskCommentFeedApiV1ActivityTaskCommentsStreamGet(
since ? { since } : undefined,
{
headers: { Accept: "text/event-stream" },
signal: abortController.signal,
},
);
if (streamResult.status !== 200) {
throw new Error("Unable to connect task comment feed stream.");
}
const response = streamResult.data as Response;
if (!(response instanceof Response) || !response.body) {
throw new Error("Unable to connect task comment feed stream.");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (!isCancelled) {
const { value, done } = await reader.read();
if (done) break;
if (value && value.length) {
backoff.reset();
}
buffer += decoder.decode(value, { stream: true });
buffer = buffer.replace(/\r\n/g, "\n");
let boundary = buffer.indexOf("\n\n");
while (boundary !== -1) {
const raw = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 2);
const lines = raw.split("\n");
let eventType = "message";
let data = "";
for (const line of lines) {
if (line.startsWith("event:")) {
eventType = line.slice(6).trim();
} else if (line.startsWith("data:")) {
data += line.slice(5).trim();
}
}
if (eventType === "comment" && data) {
try {
const payload = JSON.parse(data) as {
comment?: ActivityTaskCommentFeedItemRead;
};
if (payload.comment) {
pushFeedItem(payload.comment);
}
} catch {
// ignore malformed
}
}
boundary = buffer.indexOf("\n\n");
}
}
} catch {
// Reconnect handled below.
}
if (!isCancelled) {
if (reconnectTimeout !== undefined) {
window.clearTimeout(reconnectTimeout);
}
const delay = backoff.nextDelayMs();
reconnectTimeout = window.setTimeout(() => {
reconnectTimeout = undefined;
void connect();
}, delay);
}
};
void connect();
return () => {
isCancelled = true;
abortController.abort();
if (reconnectTimeout !== undefined) {
window.clearTimeout(reconnectTimeout);
}
};
}, [isSignedIn, pushFeedItem]);
const orderedFeed = useMemo(() => {
return [...feedItems].sort((a, b) => {
const aTime = apiDatetimeToMs(a.created_at) ?? 0;
const bTime = apiDatetimeToMs(b.created_at) ?? 0;
return bTime - aTime;
});
}, [feedItems]);
return (
<DashboardShell>
<SignedOut>
<div className="col-span-2 flex min-h-[calc(100vh-64px)] items-center justify-center bg-slate-50 p-10 text-center">
<div className="rounded-xl border border-slate-200 bg-white px-8 py-6 shadow-sm">
<p className="text-sm text-slate-600">Sign in to view the feed.</p>
<SignInButton
mode="modal"
forceRedirectUrl="/activity"
signUpForceRedirectUrl="/activity"
>
<Button className="mt-4">Sign in</Button>
</SignInButton>
</div>
</div>
</SignedOut>
<SignedIn>
<DashboardSidebar />
<main className="flex-1 overflow-y-auto bg-slate-50">
<div className="sticky top-0 z-30 border-b border-slate-200 bg-white">
<div className="px-8 py-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2">
<ActivityIcon className="h-5 w-5 text-slate-600" />
<h1 className="text-2xl font-semibold tracking-tight text-slate-900">
Live feed
</h1>
</div>
<p className="mt-1 text-sm text-slate-500">
Realtime task comments across all boards.
</p>
</div>
</div>
</div>
</div>
<div className="p-8">
{feedQuery.isLoading && feedItems.length === 0 ? (
<p className="text-sm text-slate-500">Loading feed</p>
) : feedQuery.error ? (
<div className="rounded-lg border border-slate-200 bg-white p-4 text-sm text-slate-700 shadow-sm">
{feedQuery.error.message || "Unable to load feed."}
</div>
) : orderedFeed.length === 0 ? (
<div className="rounded-xl border border-slate-200 bg-white p-10 text-center shadow-sm">
<p className="text-sm font-medium text-slate-900">
Waiting for new comments
</p>
<p className="mt-1 text-sm text-slate-500">
When agents post updates, they will show up here.
</p>
</div>
) : (
<div className="space-y-4">
{orderedFeed.map((item) => (
<FeedCard key={item.id} item={item} />
))}
</div>
)}
</div>
</main>
</SignedIn>
</DashboardShell>
);
}

View File

@@ -1,7 +1,7 @@
"use client";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
import {
@@ -12,10 +12,8 @@ import {
Settings,
X,
} from "lucide-react";
import ReactMarkdown, { type Components } from "react-markdown";
import remarkBreaks from "remark-breaks";
import remarkGfm from "remark-gfm";
import { Markdown } from "@/components/atoms/Markdown";
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar";
import { TaskBoard } from "@/components/organisms/TaskBoard";
import { DashboardShell } from "@/components/templates/DashboardShell";
@@ -144,118 +142,6 @@ const SSE_RECONNECT_BACKOFF = {
maxMs: 5 * 60_000,
} as const;
const MARKDOWN_TABLE_COMPONENTS: Components = {
table: ({ node: _node, className, ...props }) => (
<div className="my-3 overflow-x-auto">
<table className={cn("w-full border-collapse", className)} {...props} />
</div>
),
thead: ({ node: _node, className, ...props }) => (
<thead className={cn("bg-slate-50", className)} {...props} />
),
tbody: ({ node: _node, className, ...props }) => (
<tbody className={cn("divide-y divide-slate-100", className)} {...props} />
),
tr: ({ node: _node, className, ...props }) => (
<tr className={cn("align-top", className)} {...props} />
),
th: ({ node: _node, className, ...props }) => (
<th
className={cn(
"border border-slate-200 px-3 py-2 text-left text-xs font-semibold",
className,
)}
{...props}
/>
),
td: ({ node: _node, className, ...props }) => (
<td
className={cn("border border-slate-200 px-3 py-2 align-top", className)}
{...props}
/>
),
};
const MARKDOWN_COMPONENTS_BASIC: Components = {
...MARKDOWN_TABLE_COMPONENTS,
p: ({ node: _node, className, ...props }) => (
<p className={cn("mb-2 last:mb-0", className)} {...props} />
),
ul: ({ node: _node, className, ...props }) => (
<ul className={cn("mb-2 list-disc pl-5", className)} {...props} />
),
ol: ({ node: _node, className, ...props }) => (
<ol className={cn("mb-2 list-decimal pl-5", className)} {...props} />
),
li: ({ node: _node, className, ...props }) => (
<li className={cn("mb-1", className)} {...props} />
),
strong: ({ node: _node, className, ...props }) => (
<strong className={cn("font-semibold", className)} {...props} />
),
};
const MARKDOWN_COMPONENTS_DESCRIPTION: Components = {
...MARKDOWN_COMPONENTS_BASIC,
p: ({ node: _node, className, ...props }) => (
<p className={cn("mb-3 last:mb-0", className)} {...props} />
),
h1: ({ node: _node, className, ...props }) => (
<h1 className={cn("mb-2 text-base font-semibold", className)} {...props} />
),
h2: ({ node: _node, className, ...props }) => (
<h2 className={cn("mb-2 text-sm font-semibold", className)} {...props} />
),
h3: ({ node: _node, className, ...props }) => (
<h3 className={cn("mb-2 text-sm font-semibold", className)} {...props} />
),
code: ({ node: _node, className, ...props }) => (
<code
className={cn("rounded bg-slate-100 px-1 py-0.5 text-xs", className)}
{...props}
/>
),
pre: ({ node: _node, className, ...props }) => (
<pre
className={cn(
"overflow-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100",
className,
)}
{...props}
/>
),
};
const MARKDOWN_REMARK_PLUGINS_BASIC = [remarkGfm];
const MARKDOWN_REMARK_PLUGINS_WITH_BREAKS = [remarkGfm, remarkBreaks];
type MarkdownVariant = "basic" | "comment" | "description";
const Markdown = memo(function Markdown({
content,
variant,
}: {
content: string;
variant: MarkdownVariant;
}) {
const trimmed = content.trim();
const remarkPlugins =
variant === "comment"
? MARKDOWN_REMARK_PLUGINS_WITH_BREAKS
: MARKDOWN_REMARK_PLUGINS_BASIC;
const components =
variant === "description"
? MARKDOWN_COMPONENTS_DESCRIPTION
: MARKDOWN_COMPONENTS_BASIC;
return (
<ReactMarkdown remarkPlugins={remarkPlugins} components={components}>
{trimmed}
</ReactMarkdown>
);
});
Markdown.displayName = "Markdown";
const formatShortTimestamp = (value: string) => {
const date = parseApiDatetime(value);
if (!date) return "—";
@@ -405,9 +291,11 @@ LiveFeedCard.displayName = "LiveFeedCard";
export default function BoardDetailPage() {
const router = useRouter();
const params = useParams();
const searchParams = useSearchParams();
const boardIdParam = params?.boardId;
const boardId = Array.isArray(boardIdParam) ? boardIdParam[0] : boardIdParam;
const { isSignedIn } = useAuth();
const taskIdFromUrl = searchParams.get("taskId");
const [board, setBoard] = useState<Board | null>(null);
const [tasks, setTasks] = useState<Task[]>([]);
@@ -416,6 +304,7 @@ export default function BoardDetailPage() {
const [error, setError] = useState<string | null>(null);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
const selectedTaskIdRef = useRef<string | null>(null);
const openedTaskIdFromUrlRef = useRef<string | null>(null);
const [comments, setComments] = useState<TaskComment[]>([]);
const [liveFeed, setLiveFeed] = useState<TaskComment[]>([]);
const [isCommentsLoading, setIsCommentsLoading] = useState(false);
@@ -1408,6 +1297,15 @@ export default function BoardDetailPage() {
[loadComments],
);
useEffect(() => {
if (!taskIdFromUrl) return;
if (openedTaskIdFromUrlRef.current === taskIdFromUrl) return;
const exists = tasks.some((task) => task.id === taskIdFromUrl);
if (!exists) return;
openedTaskIdFromUrlRef.current = taskIdFromUrl;
openComments({ id: taskIdFromUrl });
}, [openComments, taskIdFromUrl, tasks]);
const closeComments = () => {
setIsDetailOpen(false);
selectedTaskIdRef.current = null;

View File

@@ -0,0 +1,122 @@
"use client";
import { memo } from "react";
import ReactMarkdown, { type Components } from "react-markdown";
import remarkBreaks from "remark-breaks";
import remarkGfm from "remark-gfm";
import { cn } from "@/lib/utils";
const MARKDOWN_TABLE_COMPONENTS: Components = {
table: ({ node: _node, className, ...props }) => (
<div className="my-3 overflow-x-auto">
<table className={cn("w-full border-collapse", className)} {...props} />
</div>
),
thead: ({ node: _node, className, ...props }) => (
<thead className={cn("bg-slate-50", className)} {...props} />
),
tbody: ({ node: _node, className, ...props }) => (
<tbody className={cn("divide-y divide-slate-100", className)} {...props} />
),
tr: ({ node: _node, className, ...props }) => (
<tr className={cn("align-top", className)} {...props} />
),
th: ({ node: _node, className, ...props }) => (
<th
className={cn(
"border border-slate-200 px-3 py-2 text-left text-xs font-semibold",
className,
)}
{...props}
/>
),
td: ({ node: _node, className, ...props }) => (
<td
className={cn("border border-slate-200 px-3 py-2 align-top", className)}
{...props}
/>
),
};
const MARKDOWN_COMPONENTS_BASIC: Components = {
...MARKDOWN_TABLE_COMPONENTS,
p: ({ node: _node, className, ...props }) => (
<p className={cn("mb-2 last:mb-0", className)} {...props} />
),
ul: ({ node: _node, className, ...props }) => (
<ul className={cn("mb-2 list-disc pl-5", className)} {...props} />
),
ol: ({ node: _node, className, ...props }) => (
<ol className={cn("mb-2 list-decimal pl-5", className)} {...props} />
),
li: ({ node: _node, className, ...props }) => (
<li className={cn("mb-1", className)} {...props} />
),
strong: ({ node: _node, className, ...props }) => (
<strong className={cn("font-semibold", className)} {...props} />
),
};
const MARKDOWN_COMPONENTS_DESCRIPTION: Components = {
...MARKDOWN_COMPONENTS_BASIC,
p: ({ node: _node, className, ...props }) => (
<p className={cn("mb-3 last:mb-0", className)} {...props} />
),
h1: ({ node: _node, className, ...props }) => (
<h1 className={cn("mb-2 text-base font-semibold", className)} {...props} />
),
h2: ({ node: _node, className, ...props }) => (
<h2 className={cn("mb-2 text-sm font-semibold", className)} {...props} />
),
h3: ({ node: _node, className, ...props }) => (
<h3 className={cn("mb-2 text-sm font-semibold", className)} {...props} />
),
code: ({ node: _node, className, ...props }) => (
<code
className={cn("rounded bg-slate-100 px-1 py-0.5 text-xs", className)}
{...props}
/>
),
pre: ({ node: _node, className, ...props }) => (
<pre
className={cn(
"overflow-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100",
className,
)}
{...props}
/>
),
};
const MARKDOWN_REMARK_PLUGINS_BASIC = [remarkGfm];
const MARKDOWN_REMARK_PLUGINS_WITH_BREAKS = [remarkGfm, remarkBreaks];
export type MarkdownVariant = "basic" | "comment" | "description";
export const Markdown = memo(function Markdown({
content,
variant,
}: {
content: string;
variant: MarkdownVariant;
}) {
const trimmed = content.trim();
const remarkPlugins =
variant === "comment"
? MARKDOWN_REMARK_PLUGINS_WITH_BREAKS
: MARKDOWN_REMARK_PLUGINS_BASIC;
const components =
variant === "description"
? MARKDOWN_COMPONENTS_DESCRIPTION
: MARKDOWN_COMPONENTS_BASIC;
return (
<ReactMarkdown remarkPlugins={remarkPlugins} components={components}>
{trimmed}
</ReactMarkdown>
);
});
Markdown.displayName = "Markdown";

View File

@@ -2,7 +2,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { BarChart3, Bot, LayoutGrid, Network } from "lucide-react";
import { Activity, BarChart3, Bot, LayoutGrid, Network } from "lucide-react";
import { ApiError } from "@/api/mutator";
import {
@@ -81,6 +81,18 @@ export function DashboardSidebar() {
<LayoutGrid className="h-4 w-4" />
Boards
</Link>
<Link
href="/activity"
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
pathname.startsWith("/activity")
? "bg-blue-100 text-blue-800 font-medium"
: "hover:bg-slate-100"
)}
>
<Activity className="h-4 w-4" />
Live feed
</Link>
<Link
href="/agents"
className={cn(