Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions packages/api-client/src/posthog-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ import type {
SuggestedReviewersArtefact,
SuggestedReviewerWriteEntry,
Task,
TaskChannel,
TaskRun,
TaskRunArtefact,
TaskThreadMessage,
UserBasic,
} from "@posthog/shared/domain-types";
import {
Expand Down Expand Up @@ -2120,6 +2122,7 @@ export class PostHogAPIClient {
createdBy?: number;
originProduct?: string;
internal?: boolean;
channel?: string;
}) {
const teamId = await this.getTeamId();
const params: Record<string, string | number | boolean> = {
Expand All @@ -2142,6 +2145,10 @@ export class PostHogAPIClient {
params.internal = true;
}

if (options?.channel) {
params.channel = options.channel;
}

const data = await this.api.get(`/api/projects/{project_id}/tasks/`, {
path: { project_id: teamId.toString() },
query: params,
Expand Down Expand Up @@ -2210,6 +2217,7 @@ export class PostHogAPIClient {
runtime_adapter?: string | null;
model?: string | null;
reasoning_effort?: string | null;
channel?: string | null;
},
) {
const teamId = await this.getTeamId();
Expand Down Expand Up @@ -2259,6 +2267,121 @@ export class PostHogAPIClient {
});
}

// Task channels + threads. Not in the generated OpenAPI client yet, so these
// go through the raw fetcher like the desktop file-system endpoints above.

// List backend task channels: all public channels plus the requester's
// personal "#me" channel (provisioned lazily server-side on first list).
async getTaskChannels(): Promise<TaskChannel[]> {
const teamId = await this.getTeamId();
const urlPath = `/api/projects/${teamId}/task_channels/`;
const response = await this.api.fetcher.fetch({
method: "get",
url: new URL(`${this.api.baseUrl}${urlPath}`),
path: urlPath,
});
if (!response.ok) {
throw new Error(`Failed to fetch task channels: ${response.statusText}`);
}
return (await response.json()) as TaskChannel[];
}

// Resolve-or-create a public channel by name (idempotent server-side).
async resolveTaskChannel(name: string): Promise<TaskChannel> {
const teamId = await this.getTeamId();
const urlPath = `/api/projects/${teamId}/task_channels/`;
const response = await this.api.fetcher.fetch({
method: "post",
url: new URL(`${this.api.baseUrl}${urlPath}`),
path: urlPath,
overrides: { body: JSON.stringify({ name }) },
});
if (!response.ok) {
throw new Error(`Failed to resolve task channel: ${response.statusText}`);
}
return (await response.json()) as TaskChannel;
}

async getTaskThreadMessages(taskId: string): Promise<TaskThreadMessage[]> {
const teamId = await this.getTeamId();
const urlPath = `/api/projects/${teamId}/tasks/${taskId}/thread_messages/`;
const response = await this.api.fetcher.fetch({
method: "get",
url: new URL(`${this.api.baseUrl}${urlPath}`),
path: urlPath,
});
if (!response.ok) {
throw new Error(
`Failed to fetch thread messages: ${response.statusText}`,
);
}
return (await response.json()) as TaskThreadMessage[];
}

async createTaskThreadMessage(
taskId: string,
content: string,
): Promise<TaskThreadMessage> {
const teamId = await this.getTeamId();
const urlPath = `/api/projects/${teamId}/tasks/${taskId}/thread_messages/`;
const response = await this.api.fetcher.fetch({
method: "post",
url: new URL(`${this.api.baseUrl}${urlPath}`),
path: urlPath,
overrides: { body: JSON.stringify({ content }) },
});
if (!response.ok) {
throw new Error(`Failed to post thread message: ${response.statusText}`);
}
return (await response.json()) as TaskThreadMessage;
}

async deleteTaskThreadMessage(
taskId: string,
messageId: string,
): Promise<void> {
const teamId = await this.getTeamId();
const urlPath = `/api/projects/${teamId}/tasks/${taskId}/thread_messages/${encodeURIComponent(messageId)}/`;
const response = await this.api.fetcher.fetch({
method: "delete",
url: new URL(`${this.api.baseUrl}${urlPath}`),
path: urlPath,
});
if (!response.ok && response.status !== 404) {
throw new Error(
`Failed to delete thread message: ${response.statusText}`,
);
}
}

// Forward a thread message into the task's live run. Task author only; the
// backend rejects with 400/403 otherwise (surfaced via the error body detail).
async sendTaskThreadMessageToAgent(
taskId: string,
messageId: string,
): Promise<TaskThreadMessage> {
const teamId = await this.getTeamId();
const urlPath = `/api/projects/${teamId}/tasks/${taskId}/thread_messages/${encodeURIComponent(messageId)}/send_to_agent/`;
const response = await this.api.fetcher.fetch({
method: "post",
url: new URL(`${this.api.baseUrl}${urlPath}`),
path: urlPath,
overrides: { body: JSON.stringify({}) },
});
if (!response.ok) {
const errorText = await response.text().catch(() => "");
let message = `Failed to send message to agent: ${response.statusText}`;
try {
const parsed = JSON.parse(errorText) as { detail?: string };
if (parsed.detail) message = parsed.detail;
} catch {
if (errorText) message = errorText;
}
throw new Error(message);
}
return (await response.json()) as TaskThreadMessage;
}

async sendRunCommand(
taskId: string,
runId: string,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/task-detail/taskCreationSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,7 @@ export class TaskCreationSaga extends Saga<
? (input.reasoningLevel ?? null)
: undefined,
signal_report: input.signalReportId ?? undefined,
channel: input.channelId ?? undefined,
});
return result as unknown as Task;
},
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/task-detail/taskInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface PrepareTaskInputOptions {
additionalDirectories?: string[];
channelContext?: string;
channelName?: string;
channelId?: string;
customInstructions?: string;
allowNoRepo?: boolean;
}
Expand Down Expand Up @@ -59,6 +60,7 @@ export function prepareTaskInput(
additionalDirectories: isCloud ? undefined : options.additionalDirectories,
channelContext: options.channelContext,
channelName: options.channelName,
channelId: options.channelId,
customInstructions: isCloud ? options.customInstructions : undefined,
allowNoRepo: options.allowNoRepo,
};
Expand Down
30 changes: 30 additions & 0 deletions packages/shared/src/domain-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,39 @@ export interface Task {
json_schema?: Record<string, unknown> | null;
signal_report?: string | null;
internal?: boolean;
/** Backend channel (tasks product Channel UUID) this task is owned by. */
channel?: string | null;
latest_run?: TaskRun;
}

/**
* A backend task channel — the shared feed a task is kicked off in. Distinct
* from the desktop file-system "channel" folders: those carry CONTEXT.md and
* artifacts, while this owns the task feed and threads. `personal` is the
* user's private "#me" channel.
*/
export interface TaskChannel {
id: string;
name: string;
channel_type: "public" | "personal";
created_at: string;
created_by?: UserBasic | null;
}

/**
* One human message in a task's thread. Thread messages never reach the agent
* unless the task author forwards one, which stamps the forwarded_* fields.
*/
export interface TaskThreadMessage {
id: string;
task: string;
content: string;
created_at: string;
author?: UserBasic | null;
forwarded_to_agent_at?: string | null;
forwarded_by?: UserBasic | null;
}

export type TaskRunStatus =
| "not_started"
| "queued"
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/task-creation-domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface TaskCreationInput {
channelContext?: string;
/** Display name of that channel, embedded in the context block for the UI. */
channelName?: string;
/** Backend channel UUID the created task is owned by (its feed home). */
channelId?: string;
/**
* The user's saved personalization (Settings → Personalization custom
* instructions). Cloud-only: local tasks already receive these through the
Expand Down
Loading
Loading