mirror of
https://github.com/cline/cline.git
synced 2026-09-18 17:50:56 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9708da1ff3 | ||
|
|
4c85eaa7bb |
@@ -228,6 +228,22 @@ export async function getOrCreateSessionId<
|
||||
sessionId,
|
||||
metadata: {
|
||||
transport: input.transport,
|
||||
// Delivery descriptor for this connector thread. Lets the
|
||||
// agent-facing schedule_task tool (deliverTo: "connector") post a
|
||||
// scheduled run's result back into this thread, reusing the same
|
||||
// per-adapter delivery path as user-typed /schedule.
|
||||
delivery: {
|
||||
adapter: input.transport,
|
||||
threadId: input.thread.id,
|
||||
bindingKey: input.thread.id,
|
||||
...(input.thread.channelId
|
||||
? { channelId: input.thread.channelId }
|
||||
: {}),
|
||||
...(threadState.participantKey
|
||||
? { participantKey: threadState.participantKey }
|
||||
: {}),
|
||||
...(input.hookBotUserName ? { userName: input.hookBotUserName } : {}),
|
||||
},
|
||||
...input.sessionMetadata,
|
||||
...(remoteConfigMetadata ?? {}),
|
||||
...(threadState.participantKey
|
||||
|
||||
@@ -352,6 +352,7 @@ function HistoryListContent({
|
||||
fg={isSel ? palette.textOnSelection : undefined}
|
||||
flexGrow={1}
|
||||
>
|
||||
{row.source === "schedule" ? "⏰ " : ""}
|
||||
{title}
|
||||
</text>
|
||||
{showCost && cost != null && cost > 0 && (
|
||||
|
||||
@@ -4,11 +4,26 @@ import {
|
||||
consumeWorkspaceMetadata,
|
||||
handleChatSessionCommand,
|
||||
prewarmWorkspaceMetadata,
|
||||
rewriteDesktopTeamPrompt,
|
||||
shouldUpdateSessionConnection,
|
||||
WORKSPACE_METADATA_PREWARM_TTL_MS,
|
||||
} from "./chat-session";
|
||||
import type { SidecarContext } from "./types";
|
||||
|
||||
describe("rewriteDesktopTeamPrompt", () => {
|
||||
it("rewrites /team for the core runtime", () => {
|
||||
expect(rewriteDesktopTeamPrompt("/team inspect the app", new Set())).toBe(
|
||||
'<user_command slash="team">spawn a team of agents for the following task: inspect the app</user_command>',
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects /team when the Teams tool is disabled", () => {
|
||||
expect(() =>
|
||||
rewriteDesktopTeamPrompt("/team inspect the app", new Set(["teams"])),
|
||||
).toThrow("Agent teams are disabled");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSessionConnectionUpdate", () => {
|
||||
it("does not clear reasoning settings when config omits reasoning fields", () => {
|
||||
const update = buildSessionConnectionUpdate({
|
||||
|
||||
@@ -6,12 +6,13 @@ import {
|
||||
buildWorkspaceMetadata,
|
||||
type ClineCore,
|
||||
type CoreSessionConfig,
|
||||
readGlobalSettings,
|
||||
type SessionPendingPrompt,
|
||||
SessionSource,
|
||||
splitCoreSessionConfig,
|
||||
} from "@cline/core";
|
||||
import type { Message } from "@cline/llms";
|
||||
import { buildClineSystemPrompt } from "@cline/shared";
|
||||
import { buildClineSystemPrompt, formatUserCommandBlock } from "@cline/shared";
|
||||
import { emitChunk, nowMs, sendEvent } from "./context";
|
||||
import { readSessionManifest, sharedSessionDataDir } from "./paths";
|
||||
import type {
|
||||
@@ -37,6 +38,31 @@ const workspaceMetadataPromises = new Map<
|
||||
WorkspaceMetadataCacheEntry
|
||||
>();
|
||||
|
||||
export function rewriteDesktopTeamPrompt(
|
||||
prompt: string,
|
||||
disabledTools: ReadonlySet<string> = new Set(
|
||||
readGlobalSettings().disabledTools ?? [],
|
||||
),
|
||||
): string {
|
||||
const match = /^\/team\b([\s\S]*)$/i.exec(prompt.trim());
|
||||
if (!match) return prompt;
|
||||
const task = match[1]?.trim();
|
||||
if (!task) {
|
||||
throw new Error(
|
||||
"Usage: /team <task description>. Starts a team of agents for the given task.",
|
||||
);
|
||||
}
|
||||
if (disabledTools.has("teams")) {
|
||||
throw new Error(
|
||||
"Agent teams are disabled. Enable the Teams tool in Customizations → Tools.",
|
||||
);
|
||||
}
|
||||
return formatUserCommandBlock(
|
||||
`spawn a team of agents for the following task: ${task}`,
|
||||
"team",
|
||||
);
|
||||
}
|
||||
|
||||
function getWorkspaceMetadataPromise(
|
||||
cwd: string,
|
||||
load: WorkspaceMetadataLoader,
|
||||
@@ -217,16 +243,6 @@ function buildCoreSessionConfig(config: JsonRecord): JsonRecord {
|
||||
systemPrompt: config.systemPrompt ?? config.system_prompt ?? "",
|
||||
maxIterations: config.maxIterations ?? config.max_iterations,
|
||||
enableTools: config.enableTools ?? config.enable_tools ?? true,
|
||||
enableSpawnAgent:
|
||||
config.enableSpawn ??
|
||||
config.enableSpawnAgent ??
|
||||
config.enable_spawn ??
|
||||
false,
|
||||
enableAgentTeams:
|
||||
config.enableTeams ??
|
||||
config.enableAgentTeams ??
|
||||
config.enable_teams ??
|
||||
false,
|
||||
...(thinking !== undefined ? { thinking } : {}),
|
||||
...(reasoningEffort ? { reasoningEffort } : {}),
|
||||
...(thinkingBudgetTokens !== undefined ? { thinkingBudgetTokens } : {}),
|
||||
@@ -518,6 +534,7 @@ async function handleSend(
|
||||
if (!sessionId) throw new Error("sessionId is required");
|
||||
const prompt = request.prompt?.trim();
|
||||
if (!prompt) throw new Error("prompt is required");
|
||||
const runtimePrompt = rewriteDesktopTeamPrompt(prompt);
|
||||
const manager = getSessionManager(ctx);
|
||||
const session = ctx.liveSessions.get(sessionId);
|
||||
if (request.config) {
|
||||
@@ -551,7 +568,7 @@ async function handleSend(
|
||||
// turn finishes and emit pending_prompts / pending_prompt_submitted events.
|
||||
await manager.send({
|
||||
sessionId,
|
||||
prompt,
|
||||
prompt: runtimePrompt,
|
||||
delivery: "queue",
|
||||
userImages: request.attachments?.userImages,
|
||||
});
|
||||
@@ -575,7 +592,7 @@ async function handleSend(
|
||||
);
|
||||
const result = await manager.send({
|
||||
sessionId,
|
||||
prompt,
|
||||
prompt: runtimePrompt,
|
||||
delivery,
|
||||
userImages: request.attachments?.userImages,
|
||||
});
|
||||
|
||||
@@ -638,6 +638,8 @@ async function listUserInstructionConfigs(
|
||||
|
||||
const disabledTools = new Set(readGlobalSettings().disabledTools ?? []);
|
||||
const builtinToolCatalog = getCoreBuiltinToolCatalog({
|
||||
enableSpawnAgent: true,
|
||||
enableAgentTeams: true,
|
||||
disabledToolIds: disabledTools,
|
||||
});
|
||||
|
||||
|
||||
@@ -706,7 +706,7 @@ export function ChatInputBar({
|
||||
value={editingQueuedPromptValue}
|
||||
/>
|
||||
) : (
|
||||
<div className="line-clamp-2 whitespace-pre-wrap break-words text-xs text-foreground">
|
||||
<div className="line-clamp-2 whitespace-pre-wrap wrap-break-word text-xs text-foreground">
|
||||
{item.prompt}
|
||||
</div>
|
||||
)}
|
||||
@@ -1097,7 +1097,7 @@ export function ChatInputBar({
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label="Thinking level"
|
||||
className="h-7 min-w-[5.75rem] gap-1.5 border-0 bg-muted px-2 text-[11px] shadow-none data-[size=sm]:h-7 max-[560px]:col-span-2 max-[560px]:col-start-1 max-[560px]:row-start-2"
|
||||
className="h-7 min-w-23 gap-1.5 border-0 bg-muted px-2 text-[11px] shadow-none data-[size=sm]:h-7 max-[560px]:col-span-2 max-[560px]:col-start-1 max-[560px]:row-start-2"
|
||||
size="sm"
|
||||
title={
|
||||
modelSupportsReasoning === false
|
||||
|
||||
@@ -124,8 +124,6 @@ export function normalizeRuntimeConfig(
|
||||
cwd: normalizedCwd || normalizedWorkspaceRoot,
|
||||
thinking,
|
||||
reasoningEffort: thinking === false ? undefined : config.reasoningEffort,
|
||||
enableSpawn: false,
|
||||
enableTeams: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Origin-session delivery for agent-created scheduled tasks.
|
||||
*
|
||||
* When an agent creates a schedule with `deliverTo: "origin_session"`, the
|
||||
* scheduled run still executes in its own isolated session. On completion, its
|
||||
* result is fed back into the session that created the schedule as a queued
|
||||
* follow-up turn, so the main agent can continue working with it.
|
||||
*
|
||||
* Delivery is best-effort: if the origin session is not currently active
|
||||
* (persisted-but-not-in-memory), `runTurn` rejects and we skip rather than
|
||||
* throw. Appending into a persisted-but-inactive session is a follow-up.
|
||||
*/
|
||||
|
||||
import type { RuntimeHost } from "../../runtime/host/runtime-host";
|
||||
|
||||
interface LooseMessage {
|
||||
role?: string;
|
||||
content?: unknown;
|
||||
}
|
||||
|
||||
function extractText(content: unknown): string {
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.map((part) => {
|
||||
if (typeof part === "string") {
|
||||
return part;
|
||||
}
|
||||
if (
|
||||
part &&
|
||||
typeof part === "object" &&
|
||||
"text" in part &&
|
||||
typeof (part as { text?: unknown }).text === "string"
|
||||
) {
|
||||
return (part as { text: string }).text;
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function extractLastAssistantText(
|
||||
messages: readonly LooseMessage[],
|
||||
): string | undefined {
|
||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||
if (messages[i]?.role === "assistant") {
|
||||
const text = extractText(messages[i]?.content).trim();
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export interface OriginSessionDeliveryInput {
|
||||
host: RuntimeHost;
|
||||
/** Session that created the schedule (delivery target). */
|
||||
originSessionId: string;
|
||||
/** Session the scheduled run executed in (source of the reply). */
|
||||
runSessionId?: string;
|
||||
scheduleId: string;
|
||||
/** Normalized execution status ("success" | "failed" | ...). */
|
||||
status: string;
|
||||
errorMessage?: string;
|
||||
logger?: { log?: (message: string, meta?: unknown) => void };
|
||||
}
|
||||
|
||||
/**
|
||||
* Deliver a completed scheduled run's result into its origin session as a
|
||||
* queued follow-up turn. Returns true when the turn was queued, false when the
|
||||
* origin session was not active (skipped) or delivery otherwise failed.
|
||||
*/
|
||||
export async function deliverScheduleResultToOriginSession(
|
||||
input: OriginSessionDeliveryInput,
|
||||
): Promise<boolean> {
|
||||
const { host, originSessionId, runSessionId } = input;
|
||||
if (!originSessionId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let body: string;
|
||||
if (input.status === "success" && runSessionId) {
|
||||
const messages = (await host
|
||||
.readSessionMessages(runSessionId)
|
||||
.catch(() => [])) as readonly LooseMessage[];
|
||||
const text = extractLastAssistantText(messages);
|
||||
body = text
|
||||
? `[Scheduled task ${input.scheduleId} completed]\n\n${text}`
|
||||
: `[Scheduled task ${input.scheduleId} completed with no textual output.]`;
|
||||
} else {
|
||||
body = `[Scheduled task ${input.scheduleId} ${input.status}]${
|
||||
input.errorMessage ? `: ${input.errorMessage}` : "."
|
||||
}`;
|
||||
}
|
||||
|
||||
try {
|
||||
await host.runTurn({
|
||||
sessionId: originSessionId,
|
||||
prompt: body,
|
||||
delivery: "queue",
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
input.logger?.log?.(
|
||||
`schedule origin-session delivery skipped (session not active): ${originSessionId}`,
|
||||
{ error: error instanceof Error ? error.message : String(error) },
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -415,6 +415,14 @@ export class CronRunner {
|
||||
: run.status === "queued"
|
||||
? "pending"
|
||||
: "failed";
|
||||
const deliveryMode =
|
||||
typeof spec.metadata?.deliveryMode === "string"
|
||||
? spec.metadata.deliveryMode
|
||||
: undefined;
|
||||
const originSessionId =
|
||||
typeof spec.metadata?.originSessionId === "string"
|
||||
? spec.metadata.originSessionId
|
||||
: undefined;
|
||||
this.options.eventPublisher(eventType, {
|
||||
scheduleId: spec.externalId,
|
||||
executionId: run.runId,
|
||||
@@ -426,6 +434,10 @@ export class CronRunner {
|
||||
: undefined,
|
||||
status,
|
||||
errorMessage: run.error,
|
||||
// Delivery routing hints for agent-created schedules. Consumers use
|
||||
// these to feed a run's output back into the origin session.
|
||||
...(deliveryMode ? { deliveryMode } : {}),
|
||||
...(originSessionId ? { originSessionId } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -526,7 +538,12 @@ export class CronRunner {
|
||||
provider,
|
||||
model,
|
||||
mode,
|
||||
source: spec.source?.trim() || "user",
|
||||
// Hub-created schedules surface their run sessions in history tagged
|
||||
// with source "schedule" so users can distinguish scheduled runs.
|
||||
source:
|
||||
spec.source?.trim() === "hub-schedule"
|
||||
? "schedule"
|
||||
: spec.source?.trim() || "user",
|
||||
systemPrompt: await this.buildSystemPrompt(
|
||||
spec,
|
||||
workspaceRoot,
|
||||
|
||||
@@ -60,6 +60,10 @@ export class HubScheduleCommandService {
|
||||
tags: Array.isArray(envelope.payload?.tags)
|
||||
? (envelope.payload?.tags as string[])
|
||||
: undefined,
|
||||
originSessionId:
|
||||
typeof envelope.payload?.originSessionId === "string"
|
||||
? envelope.payload.originSessionId
|
||||
: undefined,
|
||||
}),
|
||||
});
|
||||
case "schedule.get":
|
||||
|
||||
@@ -83,6 +83,12 @@ export interface ListSchedulesOptions {
|
||||
enabled?: boolean;
|
||||
limit?: number;
|
||||
tags?: string[];
|
||||
/**
|
||||
* Only return schedules created by an agent from this origin session
|
||||
* (matched against `metadata.originSessionId`). Used to surface the
|
||||
* schedule↔session linkage ("N scheduled tasks opened for this session").
|
||||
*/
|
||||
originSessionId?: string;
|
||||
}
|
||||
|
||||
export interface ListScheduleExecutionsOptions {
|
||||
@@ -249,9 +255,22 @@ export class HubScheduleService {
|
||||
}
|
||||
|
||||
public listSchedules(options: ListSchedulesOptions = {}): ScheduleRecord[] {
|
||||
return this.store
|
||||
.listHubSchedules(options)
|
||||
const { originSessionId, limit } = options;
|
||||
// When filtering by origin session we must filter on the mapped record's
|
||||
// metadata, so drop the store-level limit and re-apply it afterwards.
|
||||
const storeOptions = originSessionId
|
||||
? { ...options, limit: undefined }
|
||||
: options;
|
||||
const schedules = this.store
|
||||
.listHubSchedules(storeOptions)
|
||||
.map((spec) => specToSchedule(spec));
|
||||
if (!originSessionId) {
|
||||
return schedules;
|
||||
}
|
||||
const filtered = schedules.filter(
|
||||
(schedule) => schedule.metadata?.originSessionId === originSessionId,
|
||||
);
|
||||
return typeof limit === "number" ? filtered.slice(0, limit) : filtered;
|
||||
}
|
||||
|
||||
public updateSchedule(
|
||||
|
||||
@@ -19,6 +19,7 @@ export const DefaultToolNames = {
|
||||
SKILLS: "skills",
|
||||
ASK: "ask_question",
|
||||
SUBMIT_AND_EXIT: "submit_and_exit",
|
||||
SCHEDULE_TASK: "schedule_task",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -34,4 +35,5 @@ export const ALL_DEFAULT_TOOL_NAMES: DefaultToolName[] = [
|
||||
DefaultToolNames.SKILLS,
|
||||
DefaultToolNames.ASK,
|
||||
DefaultToolNames.SUBMIT_AND_EXIT,
|
||||
DefaultToolNames.SCHEDULE_TASK,
|
||||
];
|
||||
|
||||
@@ -46,6 +46,8 @@ import {
|
||||
ReadFilesInputSchema,
|
||||
ReadFilesInputUnionSchema,
|
||||
RunCommandsInputSchema,
|
||||
type ScheduleTaskInput,
|
||||
ScheduleTaskInputSchema,
|
||||
type SearchCodebaseInput,
|
||||
SearchCodebaseInputSchema,
|
||||
SearchCodebaseUnionInputSchema,
|
||||
@@ -62,6 +64,7 @@ import type {
|
||||
DefaultToolsConfig,
|
||||
EditorExecutor,
|
||||
FileReadExecutor,
|
||||
ScheduleTaskExecutor,
|
||||
SearchExecutor,
|
||||
ShellExecutor,
|
||||
SkillsExecutorWithMetadata,
|
||||
@@ -445,6 +448,40 @@ export function createShellTool(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the schedule_task tool
|
||||
*
|
||||
* Lets the agent create a recurring scheduled task. The actual scheduling work
|
||||
* is delegated to the injected {@link ScheduleTaskExecutor}, which closes over a
|
||||
* host-provided schedule client — that is why this tool only appears when a
|
||||
* `scheduleTask` executor is supplied.
|
||||
*/
|
||||
export function createScheduleTaskTool(
|
||||
executor: ScheduleTaskExecutor,
|
||||
config: Pick<DefaultToolsConfig, "scheduleTaskTimeoutMs"> = {},
|
||||
): AgentTool<ScheduleTaskInput, string> {
|
||||
const timeoutMs = config.scheduleTaskTimeoutMs ?? 15000;
|
||||
|
||||
return createTool<ScheduleTaskInput, string>({
|
||||
name: "schedule_task",
|
||||
description:
|
||||
"Schedule a recurring task that runs a prompt on a cron cadence. " +
|
||||
"Use `schedule` for a five-field cron pattern (e.g. '0 9 * * *' for 09:00 daily). " +
|
||||
"`deliverTo` controls where each run's output goes: 'new_session' (an independent session that shows up in session history), " +
|
||||
"'origin_session' (delivered back into THIS session as follow-up work for you to continue), or " +
|
||||
"'connector' (posted into the current chat thread as a notification; only valid inside a connector session). " +
|
||||
"Defaults to 'new_session'. Use this when the user asks to run something on a schedule or be reminded periodically.",
|
||||
inputSchema: zodToJsonSchema(ScheduleTaskInputSchema),
|
||||
timeoutMs,
|
||||
retryable: false,
|
||||
maxRetries: 0,
|
||||
execute: async (input, context) => {
|
||||
const validatedInput = validateWithZod(ScheduleTaskInputSchema, input);
|
||||
return executor(validatedInput, context);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the fetch_web_content tool
|
||||
*
|
||||
@@ -821,6 +858,7 @@ export function createDefaultTools(
|
||||
enableSkills = true,
|
||||
enableAskQuestion = true,
|
||||
enableSubmitAndExit = false,
|
||||
enableScheduleTask = true,
|
||||
...config
|
||||
} = options;
|
||||
|
||||
@@ -872,5 +910,10 @@ export function createDefaultTools(
|
||||
tools.push(createSubmitAndExitTool(submitExecutor, config));
|
||||
}
|
||||
|
||||
// Add schedule_task tool if enabled and executor provided
|
||||
if (enableScheduleTask && executors.scheduleTask) {
|
||||
tools.push(createScheduleTaskTool(executors.scheduleTask, config));
|
||||
}
|
||||
|
||||
return tools as unknown as AgentTool[];
|
||||
}
|
||||
|
||||
@@ -41,6 +41,14 @@ export {
|
||||
createFileReadExecutor,
|
||||
type FileReadExecutorOptions,
|
||||
} from "./file-read";
|
||||
export {
|
||||
createScheduleTaskExecutor,
|
||||
type ScheduleTaskClient,
|
||||
type ScheduleTaskConnectorDelivery,
|
||||
type ScheduleTaskCreateInput,
|
||||
type ScheduleTaskCreateResult,
|
||||
type ScheduleTaskExecutorOptions,
|
||||
} from "./schedule-task";
|
||||
export { createSearchExecutor, type SearchExecutorOptions } from "./search";
|
||||
export {
|
||||
createWebFetchExecutor,
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import type { AgentToolContext } from "@cline/shared";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ScheduleTaskInput } from "../schemas";
|
||||
import {
|
||||
createScheduleTaskExecutor,
|
||||
type ScheduleTaskCreateInput,
|
||||
} from "./schedule-task";
|
||||
|
||||
function ctx(sessionId?: string): AgentToolContext {
|
||||
return { agentId: "agent-1", iteration: 0, sessionId };
|
||||
}
|
||||
|
||||
function baseInput(
|
||||
overrides: Partial<ScheduleTaskInput> = {},
|
||||
): ScheduleTaskInput {
|
||||
return {
|
||||
name: "Daily summary",
|
||||
prompt: "Summarize activity",
|
||||
schedule: "0 9 * * *",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("createScheduleTaskExecutor", () => {
|
||||
it("defaults deliverTo to new_session and records originSessionId in metadata", async () => {
|
||||
const createSchedule = vi.fn(async (_input: ScheduleTaskCreateInput) => ({
|
||||
scheduleId: "sched_1",
|
||||
nextRunAt: 1_000,
|
||||
}));
|
||||
const executor = createScheduleTaskExecutor({
|
||||
client: { createSchedule },
|
||||
defaults: { workspaceRoot: "/repo", cwd: "/repo/app" },
|
||||
});
|
||||
|
||||
const result = await executor(baseInput(), ctx("origin-1"));
|
||||
|
||||
expect(createSchedule).toHaveBeenCalledTimes(1);
|
||||
const call = createSchedule.mock.calls[0][0];
|
||||
expect(call.name).toBe("Daily summary");
|
||||
expect(call.cronPattern).toBe("0 9 * * *");
|
||||
expect(call.workspaceRoot).toBe("/repo");
|
||||
expect(call.cwd).toBe("/repo/app");
|
||||
expect(call.createdBy).toBe("agent");
|
||||
expect(call.originSessionId).toBe("origin-1");
|
||||
expect(call.metadata).toMatchObject({
|
||||
deliveryMode: "new_session",
|
||||
originSessionId: "origin-1",
|
||||
});
|
||||
expect(result).toContain("sched_1");
|
||||
expect(result).toContain("new_session");
|
||||
});
|
||||
|
||||
it("passes deliveryMode=origin_session through metadata", async () => {
|
||||
const createSchedule = vi.fn(async () => ({ scheduleId: "sched_2" }));
|
||||
const executor = createScheduleTaskExecutor({
|
||||
client: { createSchedule },
|
||||
defaults: { workspaceRoot: "/repo" },
|
||||
});
|
||||
|
||||
await executor(baseInput({ deliverTo: "origin_session" }), ctx("origin-2"));
|
||||
|
||||
const call = createSchedule.mock.calls[0][0];
|
||||
expect(call.metadata?.deliveryMode).toBe("origin_session");
|
||||
expect(call.metadata?.originSessionId).toBe("origin-2");
|
||||
});
|
||||
|
||||
it("attaches the connector delivery descriptor when provided", async () => {
|
||||
const createSchedule = vi.fn(async () => ({ scheduleId: "sched_3" }));
|
||||
const executor = createScheduleTaskExecutor({
|
||||
client: { createSchedule },
|
||||
defaults: { workspaceRoot: "/repo" },
|
||||
connectorDelivery: { adapter: "telegram", threadId: "telegram:42" },
|
||||
});
|
||||
|
||||
await executor(baseInput({ deliverTo: "connector" }), ctx("origin-3"));
|
||||
|
||||
const call = createSchedule.mock.calls[0][0];
|
||||
expect(call.metadata?.deliveryMode).toBe("connector");
|
||||
expect(call.metadata?.delivery).toEqual({
|
||||
adapter: "telegram",
|
||||
threadId: "telegram:42",
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves delivery unset for connector mode when no descriptor is provided (host resolves it)", async () => {
|
||||
const createSchedule = vi.fn(async () => ({ scheduleId: "sched_4" }));
|
||||
const executor = createScheduleTaskExecutor({
|
||||
client: { createSchedule },
|
||||
defaults: { workspaceRoot: "/repo" },
|
||||
});
|
||||
|
||||
await executor(baseInput({ deliverTo: "connector" }), ctx("origin-4"));
|
||||
|
||||
const call = createSchedule.mock.calls[0][0];
|
||||
expect(call.metadata?.deliveryMode).toBe("connector");
|
||||
expect(call.metadata?.delivery).toBeUndefined();
|
||||
});
|
||||
|
||||
it("prefers explicit workspaceRoot/cwd from the tool input over defaults", async () => {
|
||||
const createSchedule = vi.fn(async () => ({ scheduleId: "sched_5" }));
|
||||
const executor = createScheduleTaskExecutor({
|
||||
client: { createSchedule },
|
||||
defaults: { workspaceRoot: "/repo", cwd: "/repo" },
|
||||
});
|
||||
|
||||
await executor(
|
||||
baseInput({ workspaceRoot: "/other", cwd: "/other/pkg" }),
|
||||
ctx("origin-5"),
|
||||
);
|
||||
|
||||
const call = createSchedule.mock.calls[0][0];
|
||||
expect(call.workspaceRoot).toBe("/other");
|
||||
expect(call.cwd).toBe("/other/pkg");
|
||||
});
|
||||
|
||||
it("records the timezone in metadata when provided", async () => {
|
||||
const createSchedule = vi.fn(async () => ({ scheduleId: "sched_6" }));
|
||||
const executor = createScheduleTaskExecutor({
|
||||
client: { createSchedule },
|
||||
defaults: { workspaceRoot: "/repo" },
|
||||
});
|
||||
|
||||
await executor(
|
||||
baseInput({ timezone: "America/New_York" }),
|
||||
ctx("origin-6"),
|
||||
);
|
||||
|
||||
const call = createSchedule.mock.calls[0][0];
|
||||
expect(call.metadata?.timezone).toBe("America/New_York");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Executor implementation for the `schedule_task` agent tool.
|
||||
*
|
||||
* The tool lets an agent create a recurring scheduled task from inside a
|
||||
* session. This module is host-agnostic: it defines a minimal
|
||||
* {@link ScheduleTaskClient} seam that the host wires to a real schedule
|
||||
* service (e.g. the hub `HubScheduleService`, or a `LocalScheduleClient`), and
|
||||
* a factory that builds the executor closure around it.
|
||||
*/
|
||||
|
||||
import type { ScheduleTaskExecutor } from "../types";
|
||||
|
||||
/**
|
||||
* Delivery descriptor for `deliverTo: "connector"` — mirrors the `delivery`
|
||||
* block the connector host stashes in a schedule's metadata for user-typed
|
||||
* `/schedule create`, so the existing per-adapter delivery path can post the
|
||||
* result into the originating chat thread with no extra wiring.
|
||||
*/
|
||||
export interface ScheduleTaskConnectorDelivery {
|
||||
adapter: string;
|
||||
threadId?: string;
|
||||
bindingKey?: string;
|
||||
channelId?: string;
|
||||
participantKey?: string;
|
||||
userName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized input the executor passes to the host's schedule client.
|
||||
*
|
||||
* `workspaceRoot` is optional: when the model omits it and the host provides no
|
||||
* default, the host client is expected to resolve it from the origin session
|
||||
* (`metadata.originSessionId`).
|
||||
*/
|
||||
export interface ScheduleTaskCreateInput {
|
||||
name: string;
|
||||
cronPattern: string;
|
||||
prompt: string;
|
||||
workspaceRoot?: string;
|
||||
cwd?: string;
|
||||
mode?: "act" | "plan";
|
||||
timezone?: string;
|
||||
/** Origin session id (also mirrored in `metadata.originSessionId`). */
|
||||
originSessionId?: string;
|
||||
/** Who created the schedule; the tool passes "agent". */
|
||||
createdBy?: string;
|
||||
/**
|
||||
* Free-form metadata persisted on the schedule spec. The tool populates
|
||||
* `deliveryMode`, `originSessionId`, and (for connector delivery) `delivery`.
|
||||
*/
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ScheduleTaskCreateResult {
|
||||
scheduleId: string;
|
||||
/** Epoch millis of the next scheduled run, when known. */
|
||||
nextRunAt?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal schedule client the executor depends on. The host implements this by
|
||||
* delegating to whatever schedule service it has access to.
|
||||
*/
|
||||
export interface ScheduleTaskClient {
|
||||
createSchedule(
|
||||
input: ScheduleTaskCreateInput,
|
||||
): Promise<ScheduleTaskCreateResult>;
|
||||
}
|
||||
|
||||
export interface ScheduleTaskExecutorOptions {
|
||||
/** Client used to persist the schedule. */
|
||||
client: ScheduleTaskClient;
|
||||
/**
|
||||
* Session defaults used when the model omits `workspaceRoot`/`cwd`.
|
||||
*/
|
||||
defaults?: {
|
||||
workspaceRoot?: string;
|
||||
cwd?: string;
|
||||
};
|
||||
/**
|
||||
* When the current session is connector-backed, the delivery descriptor for
|
||||
* its chat thread. Required for `deliverTo: "connector"`; absent otherwise.
|
||||
*/
|
||||
connectorDelivery?: ScheduleTaskConnectorDelivery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the `schedule_task` executor closure. The returned executor reads the
|
||||
* origin session id from the tool context and records it (plus the chosen
|
||||
* delivery mode) on the schedule's metadata so downstream delivery and the
|
||||
* schedule↔session linkage can find it.
|
||||
*/
|
||||
export function createScheduleTaskExecutor(
|
||||
options: ScheduleTaskExecutorOptions,
|
||||
): ScheduleTaskExecutor {
|
||||
return async (input, context) => {
|
||||
const deliverTo = input.deliverTo ?? "new_session";
|
||||
|
||||
// workspaceRoot/cwd may be omitted; the host client resolves them from the
|
||||
// origin session when they are absent.
|
||||
const workspaceRoot =
|
||||
input.workspaceRoot?.trim() || options.defaults?.workspaceRoot?.trim();
|
||||
const cwd = input.cwd?.trim() || options.defaults?.cwd?.trim();
|
||||
const originSessionId = context.sessionId?.trim();
|
||||
|
||||
const metadata: Record<string, unknown> = {
|
||||
deliveryMode: deliverTo,
|
||||
...(originSessionId ? { originSessionId } : {}),
|
||||
...(input.timezone ? { timezone: input.timezone } : {}),
|
||||
};
|
||||
|
||||
// For connector delivery, use a host-provided descriptor when present
|
||||
// (e.g. a client-side executor that already knows its thread). Otherwise
|
||||
// leave it unset so the host client can resolve it from the origin
|
||||
// session's metadata (the hub does this); it validates/errors there.
|
||||
if (deliverTo === "connector" && options.connectorDelivery) {
|
||||
metadata.delivery = options.connectorDelivery;
|
||||
}
|
||||
|
||||
const result = await options.client.createSchedule({
|
||||
name: input.name,
|
||||
cronPattern: input.schedule,
|
||||
prompt: input.prompt,
|
||||
workspaceRoot,
|
||||
cwd,
|
||||
mode: input.mode,
|
||||
timezone: input.timezone,
|
||||
originSessionId,
|
||||
createdBy: "agent",
|
||||
metadata,
|
||||
});
|
||||
|
||||
const parts = [
|
||||
`Scheduled task "${input.name}" created (id: ${result.scheduleId}).`,
|
||||
`Cron: ${input.schedule}${input.timezone ? ` in ${input.timezone}` : ""}.`,
|
||||
`Delivery: ${deliverTo}.`,
|
||||
];
|
||||
if (typeof result.nextRunAt === "number") {
|
||||
parts.push(`Next run: ${new Date(result.nextRunAt).toISOString()}.`);
|
||||
}
|
||||
return parts.join(" ");
|
||||
};
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export {
|
||||
createDefaultTools,
|
||||
createEditorTool,
|
||||
createReadFilesTool,
|
||||
createScheduleTaskTool,
|
||||
createSearchTool,
|
||||
createShellTool,
|
||||
createSkillsTool,
|
||||
@@ -31,6 +32,7 @@ export {
|
||||
createDefaultShellExecutor,
|
||||
createEditorExecutor,
|
||||
createFileReadExecutor,
|
||||
createScheduleTaskExecutor,
|
||||
createSearchExecutor,
|
||||
createShellExecutor,
|
||||
createWebFetchExecutor,
|
||||
@@ -39,6 +41,11 @@ export {
|
||||
type FileReadExecutorOptions,
|
||||
PatchActionType,
|
||||
type PatchFileChange,
|
||||
type ScheduleTaskClient,
|
||||
type ScheduleTaskConnectorDelivery,
|
||||
type ScheduleTaskCreateInput,
|
||||
type ScheduleTaskCreateResult,
|
||||
type ScheduleTaskExecutorOptions,
|
||||
type SearchExecutorOptions,
|
||||
type ShellExecutorOptions,
|
||||
type WebFetchExecutorOptions,
|
||||
@@ -86,6 +93,10 @@ export {
|
||||
ReadFilesInputSchema,
|
||||
type RunCommandsInput,
|
||||
RunCommandsInputSchema,
|
||||
type ScheduleTaskDeliverTo,
|
||||
ScheduleTaskDeliverToSchema,
|
||||
type ScheduleTaskInput,
|
||||
ScheduleTaskInputSchema,
|
||||
type SearchCodebaseInput,
|
||||
SearchCodebaseInputSchema,
|
||||
type SkillsInput,
|
||||
@@ -107,6 +118,7 @@ export type {
|
||||
DefaultToolsConfig,
|
||||
EditorExecutor,
|
||||
FileReadExecutor,
|
||||
ScheduleTaskExecutor,
|
||||
SearchExecutor,
|
||||
ShellExecutor,
|
||||
SkillsExecutor,
|
||||
|
||||
@@ -44,6 +44,7 @@ const TOOL_NAME_TO_FLAG: Record<
|
||||
| "enableSkills"
|
||||
| "enableAskQuestion"
|
||||
| "enableSubmitAndExit"
|
||||
| "enableScheduleTask"
|
||||
>
|
||||
> = {
|
||||
read_files: "enableReadFiles",
|
||||
@@ -55,6 +56,7 @@ const TOOL_NAME_TO_FLAG: Record<
|
||||
skills: "enableSkills",
|
||||
ask_question: "enableAskQuestion",
|
||||
submit_and_exit: "enableSubmitAndExit",
|
||||
schedule_task: "enableScheduleTask",
|
||||
};
|
||||
|
||||
export const DEFAULT_MODEL_TOOL_ROUTING_RULES: ToolRoutingRule[] = [
|
||||
|
||||
@@ -346,3 +346,79 @@ export type AskQuestionInput = z.infer<typeof AskQuestionInputSchema>;
|
||||
* Input for the submit and exit tool
|
||||
*/
|
||||
export type SubmitInput = z.infer<typeof SubmitInputSchema>;
|
||||
|
||||
/**
|
||||
* Where a scheduled task's output is delivered.
|
||||
* - `new_session`: each run creates its own session that appears in session
|
||||
* history (marked with source=schedule).
|
||||
* - `origin_session`: the run's output is delivered back into the session that
|
||||
* created the schedule, as follow-up work for the main agent.
|
||||
* - `connector`: the run's output is posted into the connector chat thread this
|
||||
* session belongs to (only valid inside a connector-backed session).
|
||||
*/
|
||||
export const ScheduleTaskDeliverToSchema = z
|
||||
.enum(["new_session", "origin_session", "connector"])
|
||||
.describe(
|
||||
"Where each scheduled run's output goes: 'new_session' (independent session in history), 'origin_session' (fed back into this session as follow-up work), or 'connector' (posted into the current chat thread; only valid in a connector session).",
|
||||
);
|
||||
|
||||
/**
|
||||
* Schema for the schedule_task tool input
|
||||
*/
|
||||
export const ScheduleTaskInputSchema = z
|
||||
.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe("Short human-readable name for the scheduled task."),
|
||||
prompt: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe(
|
||||
"The task instructions the scheduled run should execute each time it fires.",
|
||||
),
|
||||
schedule: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe(
|
||||
"Five-field cron pattern describing the cadence, e.g. '0 9 * * *' for 09:00 every day.",
|
||||
),
|
||||
deliverTo: ScheduleTaskDeliverToSchema.optional().describe(
|
||||
"Where each run's output goes. Defaults to 'new_session' when omitted.",
|
||||
),
|
||||
timezone: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Optional IANA timezone for interpreting the cron pattern, e.g. 'America/New_York'.",
|
||||
),
|
||||
mode: z
|
||||
.enum(["act", "plan"])
|
||||
.optional()
|
||||
.describe("Optional agent mode for the scheduled run."),
|
||||
workspaceRoot: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Optional absolute workspace root for the run; defaults to this session's workspace.",
|
||||
),
|
||||
cwd: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Optional working directory for the run; defaults to this session's cwd.",
|
||||
),
|
||||
})
|
||||
.describe(
|
||||
"Create a recurring scheduled task that runs a prompt on a cron cadence.",
|
||||
);
|
||||
|
||||
/**
|
||||
* Input for the schedule_task tool
|
||||
*/
|
||||
export type ScheduleTaskInput = z.infer<typeof ScheduleTaskInputSchema>;
|
||||
|
||||
/**
|
||||
* Delivery target for `deliverTo: 'new_session' | 'origin_session' | 'connector'`.
|
||||
*/
|
||||
export type ScheduleTaskDeliverTo = z.infer<typeof ScheduleTaskDeliverToSchema>;
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
ApplyPatchInput,
|
||||
EditFileInput,
|
||||
ReadFileRequest,
|
||||
ScheduleTaskInput,
|
||||
StructuredCommandInput,
|
||||
} from "./schemas";
|
||||
|
||||
@@ -192,6 +193,23 @@ export type VerifySubmitExecutor = (
|
||||
context: AgentToolContext,
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* Executor for creating a scheduled (recurring) task
|
||||
*
|
||||
* The executor closes over a schedule client (and, when the session is
|
||||
* connector-backed, the current thread's delivery descriptor). It is only
|
||||
* injected for hosts that can reach a schedule service, which is why
|
||||
* `schedule_task` only appears when this executor is provided.
|
||||
*
|
||||
* @param input - Validated schedule_task input
|
||||
* @param context - Tool execution context (carries the origin `sessionId`)
|
||||
* @returns A human-readable confirmation string for the model
|
||||
*/
|
||||
export type ScheduleTaskExecutor = (
|
||||
input: ScheduleTaskInput,
|
||||
context: AgentToolContext,
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* Collection of all tool executors
|
||||
*/
|
||||
@@ -214,6 +232,8 @@ export interface ToolExecutors {
|
||||
askQuestion?: AskQuestionExecutor;
|
||||
/** Final submission implementation */
|
||||
submit?: VerifySubmitExecutor;
|
||||
/** Scheduled-task creation implementation */
|
||||
scheduleTask?: ScheduleTaskExecutor;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -232,7 +252,8 @@ export type DefaultToolName =
|
||||
| "editor"
|
||||
| "skills"
|
||||
| "ask_question"
|
||||
| "submit_and_exit";
|
||||
| "submit_and_exit"
|
||||
| "schedule_task";
|
||||
|
||||
/**
|
||||
* Configuration for enabling/disabling default tools
|
||||
@@ -292,6 +313,13 @@ export interface DefaultToolsConfig {
|
||||
*/
|
||||
enableSubmitAndExit?: boolean;
|
||||
|
||||
/**
|
||||
* Enable the schedule_task tool. Only takes effect when a `scheduleTask`
|
||||
* executor is also provided (it requires a schedule client the host injects).
|
||||
* @default true
|
||||
*/
|
||||
enableScheduleTask?: boolean;
|
||||
|
||||
/**
|
||||
* Current working directory for tools that need it
|
||||
*/
|
||||
@@ -344,6 +372,12 @@ export interface DefaultToolsConfig {
|
||||
* @default 15000
|
||||
*/
|
||||
submitTimeoutMs?: number;
|
||||
|
||||
/**
|
||||
* Timeout for schedule_task operations in milliseconds
|
||||
* @default 15000
|
||||
*/
|
||||
scheduleTaskTimeoutMs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,12 +3,15 @@ import type {
|
||||
HubCommandEnvelope,
|
||||
HubEventEnvelope,
|
||||
HubReplyEnvelope,
|
||||
HubScheduleCreateInput,
|
||||
ToolApprovalRequest,
|
||||
} from "@cline/shared";
|
||||
import { captureSdkError, createSessionId } from "@cline/shared";
|
||||
import { deliverScheduleResultToOriginSession } from "../../cron/delivery/origin-session-delivery";
|
||||
import { CronService } from "../../cron/service/cron-service";
|
||||
import { HubScheduleCommandService } from "../../cron/service/schedule-command-service";
|
||||
import { HubScheduleService } from "../../cron/service/schedule-service";
|
||||
import { createScheduleTaskExecutor } from "../../extensions/tools";
|
||||
import { LocalRuntimeHost } from "../../runtime/host/local-runtime-host";
|
||||
import type {
|
||||
PendingPromptsRuntimeService,
|
||||
@@ -184,13 +187,78 @@ export class HubServerTransport implements NativeHubTransport {
|
||||
private readonly ctx: HubTransportContext;
|
||||
|
||||
constructor(readonly options: HubWebSocketServerOptions) {
|
||||
// The schedule_task tool executor runs inside hub-hosted agent sessions.
|
||||
// It reaches this hub's own HubScheduleService (assigned later in this
|
||||
// constructor) via a lazy reference and resolves workspaceRoot/cwd from
|
||||
// the origin session when the agent omits them.
|
||||
const sessionHostRef: {
|
||||
current?: RuntimeHost & Partial<PendingPromptsRuntimeService>;
|
||||
} = {};
|
||||
const scheduleTaskExecutor = createScheduleTaskExecutor({
|
||||
client: {
|
||||
createSchedule: async (input) => {
|
||||
let workspaceRoot = input.workspaceRoot?.trim();
|
||||
let cwd = input.cwd?.trim();
|
||||
const originSessionId = input.originSessionId?.trim();
|
||||
const metadata: Record<string, unknown> = {
|
||||
...(input.metadata ?? {}),
|
||||
};
|
||||
const needsConnectorDelivery =
|
||||
metadata.deliveryMode === "connector" && !metadata.delivery;
|
||||
if (
|
||||
(!workspaceRoot || !cwd || needsConnectorDelivery) &&
|
||||
originSessionId
|
||||
) {
|
||||
const originSession = await sessionHostRef.current
|
||||
?.getSession(originSessionId)
|
||||
.catch(() => undefined);
|
||||
workspaceRoot = workspaceRoot || originSession?.workspaceRoot;
|
||||
cwd = cwd || originSession?.cwd;
|
||||
if (needsConnectorDelivery) {
|
||||
const delivery = originSession?.metadata?.delivery;
|
||||
if (delivery && typeof delivery === "object") {
|
||||
metadata.delivery = delivery;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (metadata.deliveryMode === "connector" && !metadata.delivery) {
|
||||
throw new Error(
|
||||
"schedule_task deliverTo='connector' is only available inside a connector-backed session (no thread delivery target found).",
|
||||
);
|
||||
}
|
||||
if (!workspaceRoot) {
|
||||
throw new Error(
|
||||
"schedule_task could not resolve a workspaceRoot for the scheduled run.",
|
||||
);
|
||||
}
|
||||
const created = this.schedules.createSchedule({
|
||||
name: input.name,
|
||||
cronPattern: input.cronPattern,
|
||||
prompt: input.prompt,
|
||||
workspaceRoot,
|
||||
cwd,
|
||||
mode: input.mode,
|
||||
createdBy: input.createdBy,
|
||||
metadata: metadata as HubScheduleCreateInput["metadata"],
|
||||
});
|
||||
return {
|
||||
scheduleId: created.scheduleId,
|
||||
nextRunAt: created.nextRunAt,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
this.sessionHost =
|
||||
options.sessionHost ??
|
||||
new LocalRuntimeHost({
|
||||
sessionService: new CoreSessionService(new SqliteSessionStore()),
|
||||
fetch: options.fetch,
|
||||
telemetry: options.telemetry,
|
||||
capabilities: {
|
||||
toolExecutors: { scheduleTask: scheduleTaskExecutor },
|
||||
},
|
||||
});
|
||||
sessionHostRef.current = this.sessionHost;
|
||||
this.ctx = {
|
||||
clients: this.clients,
|
||||
sessionState: this.sessionState,
|
||||
@@ -239,6 +307,41 @@ export class HubServerTransport implements NativeHubTransport {
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
// For agent-created schedules with deliverTo:"origin_session",
|
||||
// feed the run's result back into the originating session as a
|
||||
// queued follow-up turn (best-effort; skipped if not active).
|
||||
const record =
|
||||
payload && typeof payload === "object"
|
||||
? (payload as Record<string, unknown>)
|
||||
: undefined;
|
||||
if (record?.deliveryMode === "origin_session") {
|
||||
const originSessionId =
|
||||
typeof record.originSessionId === "string"
|
||||
? record.originSessionId
|
||||
: undefined;
|
||||
if (originSessionId) {
|
||||
void deliverScheduleResultToOriginSession({
|
||||
host: this.sessionHost,
|
||||
originSessionId,
|
||||
runSessionId:
|
||||
typeof record.sessionId === "string"
|
||||
? record.sessionId
|
||||
: undefined,
|
||||
scheduleId:
|
||||
typeof record.scheduleId === "string"
|
||||
? record.scheduleId
|
||||
: "unknown",
|
||||
status:
|
||||
typeof record.status === "string" ? record.status : "unknown",
|
||||
errorMessage:
|
||||
typeof record.errorMessage === "string"
|
||||
? record.errorMessage
|
||||
: undefined,
|
||||
}).catch(() => {
|
||||
// Best-effort delivery.
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
this.scheduleCommands = new HubScheduleCommandService(this.schedules);
|
||||
|
||||
@@ -45,6 +45,7 @@ export const SessionSource = {
|
||||
IDE: "ide",
|
||||
JETBRAINS: "jetbrains",
|
||||
NEOVIM: "neovim",
|
||||
SCHEDULE: "schedule",
|
||||
UNKNOWN: "unknown",
|
||||
} as const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user