feat: cron agent binding + doctor UI refresh
This commit is contained in:
@@ -72,6 +72,8 @@
|
|||||||
- Memory: embedding providers support OpenAI or local `node-llama-cpp`; config adds defaults + per-agent overrides, provider/fallback metadata surfaced in tools/CLI.
|
- Memory: embedding providers support OpenAI or local `node-llama-cpp`; config adds defaults + per-agent overrides, provider/fallback metadata surfaced in tools/CLI.
|
||||||
- CLI/Tools: new `clawdbot memory` commands plus `memory_search`/`memory_get` tools returning snippets + line ranges and provider info.
|
- CLI/Tools: new `clawdbot memory` commands plus `memory_search`/`memory_get` tools returning snippets + line ranges and provider info.
|
||||||
- Runtime: memory index stored under `~/.clawdbot/memory/{agentId}.sqlite` with watch-on-by-default; inline status replies now stay auth-gated while inline prompts continue to the agent.
|
- Runtime: memory index stored under `~/.clawdbot/memory/{agentId}.sqlite` with watch-on-by-default; inline status replies now stay auth-gated while inline prompts continue to the agent.
|
||||||
|
- Doctor: rebuild Control UI assets when protocol schema is newer to avoid stale UI connect errors. (#786) — thanks @meaningfool.
|
||||||
|
- Cron: allow jobs to target a specific agent and expose agent selection in the macOS app + Control UI.
|
||||||
- Discord: add `discord.allowBots` to permit bot-authored messages (still ignores its own messages) with docs warning about bot loops. (#802) — thanks @zknicker.
|
- Discord: add `discord.allowBots` to permit bot-authored messages (still ignores its own messages) with docs warning about bot loops. (#802) — thanks @zknicker.
|
||||||
- CLI/Onboarding: `clawdbot dashboard` prints/copies the tokenized Control UI link and opens it; onboarding now auto-opens the dashboard with your token and keeps the link in the summary.
|
- CLI/Onboarding: `clawdbot dashboard` prints/copies the tokenized Control UI link and opens it; onboarding now auto-opens the dashboard with your token and keeps the link in the summary.
|
||||||
- Commands: native slash commands now default to `"auto"` (on for Discord/Telegram, off for Slack) with per-provider overrides (`discord/telegram/slack.commands.native`) and docs updated.
|
- Commands: native slash commands now default to `"auto"` (on for Discord/Telegram, off for Slack) with per-provider overrides (`discord/telegram/slack.commands.native`) and docs updated.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ extension CronJobEditor {
|
|||||||
guard let job else { return }
|
guard let job else { return }
|
||||||
self.name = job.name
|
self.name = job.name
|
||||||
self.description = job.description ?? ""
|
self.description = job.description ?? ""
|
||||||
|
self.agentId = job.agentId ?? ""
|
||||||
self.enabled = job.enabled
|
self.enabled = job.enabled
|
||||||
self.sessionTarget = job.sessionTarget
|
self.sessionTarget = job.sessionTarget
|
||||||
self.wakeMode = job.wakeMode
|
self.wakeMode = job.wakeMode
|
||||||
@@ -67,6 +68,7 @@ extension CronJobEditor {
|
|||||||
userInfo: [NSLocalizedDescriptionKey: "Name is required."])
|
userInfo: [NSLocalizedDescriptionKey: "Name is required."])
|
||||||
}
|
}
|
||||||
let description = self.description.trimmingCharacters(in: .whitespacesAndNewlines)
|
let description = self.description.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let agentId = self.agentId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let schedule: [String: Any]
|
let schedule: [String: Any]
|
||||||
switch self.scheduleKind {
|
switch self.scheduleKind {
|
||||||
case .at:
|
case .at:
|
||||||
@@ -148,6 +150,11 @@ extension CronJobEditor {
|
|||||||
"payload": payload,
|
"payload": payload,
|
||||||
]
|
]
|
||||||
if !description.isEmpty { root["description"] = description }
|
if !description.isEmpty { root["description"] = description }
|
||||||
|
if !agentId.isEmpty {
|
||||||
|
root["agentId"] = agentId
|
||||||
|
} else if self.job?.agentId != nil {
|
||||||
|
root["agentId"] = NSNull()
|
||||||
|
}
|
||||||
|
|
||||||
if self.sessionTarget == .isolated {
|
if self.sessionTarget == .isolated {
|
||||||
let trimmed = self.postPrefix.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = self.postPrefix.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ extension CronJobEditor {
|
|||||||
mutating func exerciseForTesting() {
|
mutating func exerciseForTesting() {
|
||||||
self.name = "Test job"
|
self.name = "Test job"
|
||||||
self.description = "Test description"
|
self.description = "Test description"
|
||||||
|
self.agentId = "ops"
|
||||||
self.enabled = true
|
self.enabled = true
|
||||||
self.sessionTarget = .isolated
|
self.sessionTarget = .isolated
|
||||||
self.wakeMode = .now
|
self.wakeMode = .now
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ struct CronJobEditor: View {
|
|||||||
|
|
||||||
@State var name: String = ""
|
@State var name: String = ""
|
||||||
@State var description: String = ""
|
@State var description: String = ""
|
||||||
|
@State var agentId: String = ""
|
||||||
@State var enabled: Bool = true
|
@State var enabled: Bool = true
|
||||||
@State var sessionTarget: CronSessionTarget = .main
|
@State var sessionTarget: CronSessionTarget = .main
|
||||||
@State var wakeMode: CronWakeMode = .nextHeartbeat
|
@State var wakeMode: CronWakeMode = .nextHeartbeat
|
||||||
@@ -77,6 +78,12 @@ struct CronJobEditor: View {
|
|||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
|
GridRow {
|
||||||
|
self.gridLabel("Agent ID")
|
||||||
|
TextField("Optional (default agent)", text: self.$agentId)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
GridRow {
|
GridRow {
|
||||||
self.gridLabel("Enabled")
|
self.gridLabel("Enabled")
|
||||||
Toggle("", isOn: self.$enabled)
|
Toggle("", isOn: self.$enabled)
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ struct CronJobState: Codable, Equatable {
|
|||||||
|
|
||||||
struct CronJob: Identifiable, Codable, Equatable {
|
struct CronJob: Identifiable, Codable, Equatable {
|
||||||
let id: String
|
let id: String
|
||||||
|
let agentId: String?
|
||||||
var name: String
|
var name: String
|
||||||
var description: String?
|
var description: String?
|
||||||
var enabled: Bool
|
var enabled: Bool
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ extension CronSettings {
|
|||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
StatusPill(text: job.sessionTarget.rawValue, tint: .secondary)
|
StatusPill(text: job.sessionTarget.rawValue, tint: .secondary)
|
||||||
StatusPill(text: job.wakeMode.rawValue, tint: .secondary)
|
StatusPill(text: job.wakeMode.rawValue, tint: .secondary)
|
||||||
|
if let agentId = job.agentId, !agentId.isEmpty {
|
||||||
|
StatusPill(text: "agent \(agentId)", tint: .secondary)
|
||||||
|
}
|
||||||
if let status = job.state.lastStatus {
|
if let status = job.state.lastStatus {
|
||||||
StatusPill(text: status, tint: status == "ok" ? .green : .orange)
|
StatusPill(text: status, tint: status == "ok" ? .green : .orange)
|
||||||
}
|
}
|
||||||
@@ -94,6 +97,9 @@ extension CronSettings {
|
|||||||
if let desc = job.description, !desc.isEmpty {
|
if let desc = job.description, !desc.isEmpty {
|
||||||
LabeledContent("Description") { Text(desc).font(.callout) }
|
LabeledContent("Description") { Text(desc).font(.callout) }
|
||||||
}
|
}
|
||||||
|
if let agentId = job.agentId, !agentId.isEmpty {
|
||||||
|
LabeledContent("Agent") { Text(agentId) }
|
||||||
|
}
|
||||||
LabeledContent("Session") { Text(job.sessionTarget.rawValue) }
|
LabeledContent("Session") { Text(job.sessionTarget.rawValue) }
|
||||||
LabeledContent("Wake") { Text(job.wakeMode.rawValue) }
|
LabeledContent("Wake") { Text(job.wakeMode.rawValue) }
|
||||||
LabeledContent("Next run") {
|
LabeledContent("Next run") {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ struct CronSettings_Previews: PreviewProvider {
|
|||||||
store.jobs = [
|
store.jobs = [
|
||||||
CronJob(
|
CronJob(
|
||||||
id: "job-1",
|
id: "job-1",
|
||||||
|
agentId: "ops",
|
||||||
name: "Daily summary",
|
name: "Daily summary",
|
||||||
description: nil,
|
description: nil,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -59,6 +60,7 @@ extension CronSettings {
|
|||||||
|
|
||||||
let job = CronJob(
|
let job = CronJob(
|
||||||
id: "job-1",
|
id: "job-1",
|
||||||
|
agentId: "ops",
|
||||||
name: "Daily summary",
|
name: "Daily summary",
|
||||||
description: "Summary job",
|
description: "Summary job",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ struct CronJobEditorSmokeTests {
|
|||||||
@Test func cronJobEditorBuildsBodyForExistingJob() {
|
@Test func cronJobEditorBuildsBodyForExistingJob() {
|
||||||
let job = CronJob(
|
let job = CronJob(
|
||||||
id: "job-1",
|
id: "job-1",
|
||||||
|
agentId: "ops",
|
||||||
name: "Daily summary",
|
name: "Daily summary",
|
||||||
|
description: nil,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
createdAtMs: 1_700_000_000_000,
|
createdAtMs: 1_700_000_000_000,
|
||||||
updatedAtMs: 1_700_000_000_000,
|
updatedAtMs: 1_700_000_000_000,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ struct CronModelsTests {
|
|||||||
@Test func displayNameTrimsWhitespaceAndFallsBack() {
|
@Test func displayNameTrimsWhitespaceAndFallsBack() {
|
||||||
let base = CronJob(
|
let base = CronJob(
|
||||||
id: "x",
|
id: "x",
|
||||||
|
agentId: nil,
|
||||||
name: " hello ",
|
name: " hello ",
|
||||||
description: nil,
|
description: nil,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -81,6 +82,7 @@ struct CronModelsTests {
|
|||||||
@Test func nextRunDateAndLastRunDateDeriveFromState() {
|
@Test func nextRunDateAndLastRunDateDeriveFromState() {
|
||||||
let job = CronJob(
|
let job = CronJob(
|
||||||
id: "x",
|
id: "x",
|
||||||
|
agentId: nil,
|
||||||
name: "t",
|
name: "t",
|
||||||
description: nil,
|
description: nil,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ struct SettingsViewSmokeTests {
|
|||||||
|
|
||||||
let job1 = CronJob(
|
let job1 = CronJob(
|
||||||
id: "job-1",
|
id: "job-1",
|
||||||
|
agentId: "ops",
|
||||||
name: " Morning Check-in ",
|
name: " Morning Check-in ",
|
||||||
description: nil,
|
description: nil,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -32,6 +33,7 @@ struct SettingsViewSmokeTests {
|
|||||||
|
|
||||||
let job2 = CronJob(
|
let job2 = CronJob(
|
||||||
id: "job-2",
|
id: "job-2",
|
||||||
|
agentId: nil,
|
||||||
name: "",
|
name: "",
|
||||||
description: nil,
|
description: nil,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ cat ~/.clawdbot/clawdbot.json
|
|||||||
|
|
||||||
## What it does (summary)
|
## What it does (summary)
|
||||||
- Optional pre-flight update for git installs (interactive only).
|
- Optional pre-flight update for git installs (interactive only).
|
||||||
|
- UI protocol freshness check (rebuilds Control UI when the protocol schema is newer).
|
||||||
- Health check + restart prompt.
|
- Health check + restart prompt.
|
||||||
- Skills status summary (eligible/missing/blocked).
|
- Skills status summary (eligible/missing/blocked).
|
||||||
- Legacy config migration and normalization.
|
- Legacy config migration and normalization.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
|||||||
import { type ClawdbotConfig, loadConfig } from "../config/config.js";
|
import { type ClawdbotConfig, loadConfig } from "../config/config.js";
|
||||||
import { resolveClawdbotAgentDir } from "./agent-paths.js";
|
import { resolveClawdbotAgentDir } from "./agent-paths.js";
|
||||||
import {
|
import {
|
||||||
|
type AuthProfileStore,
|
||||||
ensureAuthProfileStore,
|
ensureAuthProfileStore,
|
||||||
listProfilesForProvider,
|
listProfilesForProvider,
|
||||||
} from "./auth-profiles.js";
|
} from "./auth-profiles.js";
|
||||||
@@ -70,9 +71,10 @@ async function readJson(pathname: string): Promise<unknown> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMinimaxApiProvider(): ProviderConfig {
|
function buildMinimaxApiProvider(apiKey?: string): ProviderConfig {
|
||||||
return {
|
return {
|
||||||
baseUrl: MINIMAX_API_BASE_URL,
|
baseUrl: MINIMAX_API_BASE_URL,
|
||||||
|
...(apiKey ? { apiKey } : {}),
|
||||||
api: "anthropic-messages",
|
api: "anthropic-messages",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
@@ -88,6 +90,35 @@ function buildMinimaxApiProvider(): ProviderConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveMinimaxApiKeyFromStore(
|
||||||
|
store: AuthProfileStore,
|
||||||
|
): string | undefined {
|
||||||
|
const profileIds = listProfilesForProvider(store, "minimax");
|
||||||
|
for (const profileId of profileIds) {
|
||||||
|
const cred = store.profiles[profileId];
|
||||||
|
if (!cred) continue;
|
||||||
|
if (cred.type === "api_key") {
|
||||||
|
const key = cred.key?.trim();
|
||||||
|
if (key) return key;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cred.type === "token") {
|
||||||
|
const token = cred.token?.trim();
|
||||||
|
if (!token) continue;
|
||||||
|
if (
|
||||||
|
typeof cred.expires === "number" &&
|
||||||
|
Number.isFinite(cred.expires) &&
|
||||||
|
cred.expires > 0 &&
|
||||||
|
Date.now() >= cred.expires
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveImplicitProviders(params: {
|
function resolveImplicitProviders(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
agentDir: string;
|
agentDir: string;
|
||||||
@@ -95,10 +126,10 @@ function resolveImplicitProviders(params: {
|
|||||||
const providers: Record<string, ProviderConfig> = {};
|
const providers: Record<string, ProviderConfig> = {};
|
||||||
const minimaxEnv = resolveEnvApiKey("minimax");
|
const minimaxEnv = resolveEnvApiKey("minimax");
|
||||||
const authStore = ensureAuthProfileStore(params.agentDir);
|
const authStore = ensureAuthProfileStore(params.agentDir);
|
||||||
const hasMinimaxProfile =
|
const minimaxKey =
|
||||||
listProfilesForProvider(authStore, "minimax").length > 0;
|
minimaxEnv?.apiKey ?? resolveMinimaxApiKeyFromStore(authStore);
|
||||||
if (minimaxEnv || hasMinimaxProfile) {
|
if (minimaxKey) {
|
||||||
providers.minimax = buildMinimaxApiProvider();
|
providers.minimax = buildMinimaxApiProvider(minimaxKey);
|
||||||
}
|
}
|
||||||
return providers;
|
return providers;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { stripHeartbeatToken } from "./heartbeat.js";
|
import {
|
||||||
|
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||||
|
stripHeartbeatToken,
|
||||||
|
} from "./heartbeat.js";
|
||||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
||||||
|
|
||||||
describe("stripHeartbeatToken", () => {
|
describe("stripHeartbeatToken", () => {
|
||||||
@@ -52,7 +55,7 @@ describe("stripHeartbeatToken", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("keeps heartbeat replies when remaining content exceeds threshold", () => {
|
it("keeps heartbeat replies when remaining content exceeds threshold", () => {
|
||||||
const long = "A".repeat(400);
|
const long = "A".repeat(DEFAULT_HEARTBEAT_ACK_MAX_CHARS + 1);
|
||||||
expect(
|
expect(
|
||||||
stripHeartbeatToken(`${long} ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" }),
|
stripHeartbeatToken(`${long} ${HEARTBEAT_TOKEN}`, { mode: "heartbeat" }),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export async function maybeRepairUiProtocolFreshness(
|
|||||||
{ timeoutMs: 5000 },
|
{ timeoutMs: 5000 },
|
||||||
).catch(() => null);
|
).catch(() => null);
|
||||||
|
|
||||||
if (gitLog?.stdout.trim()) {
|
if (gitLog && gitLog.code === 0 && gitLog.stdout.trim()) {
|
||||||
note(
|
note(
|
||||||
`UI assets are older than the protocol schema.\nFunctional changes since last build:\n${gitLog.stdout
|
`UI assets are older than the protocol schema.\nFunctional changes since last build:\n${gitLog.stdout
|
||||||
.trim()
|
.trim()
|
||||||
@@ -64,7 +64,7 @@ export async function maybeRepairUiProtocolFreshness(
|
|||||||
// Use scripts/ui.js to build, assuming node is available as we are running in it.
|
// Use scripts/ui.js to build, assuming node is available as we are running in it.
|
||||||
// We use the same node executable to run the script.
|
// We use the same node executable to run the script.
|
||||||
const uiScriptPath = path.join(root, "scripts/ui.js");
|
const uiScriptPath = path.join(root, "scripts/ui.js");
|
||||||
await runCommandWithTimeout(
|
const buildResult = await runCommandWithTimeout(
|
||||||
[process.execPath, uiScriptPath, "build"],
|
[process.execPath, uiScriptPath, "build"],
|
||||||
{
|
{
|
||||||
cwd: root,
|
cwd: root,
|
||||||
@@ -72,11 +72,21 @@ export async function maybeRepairUiProtocolFreshness(
|
|||||||
env: { ...process.env, FORCE_COLOR: "1" },
|
env: { ...process.env, FORCE_COLOR: "1" },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
if (buildResult.code === 0) {
|
||||||
note("UI rebuild complete.", "UI");
|
note("UI rebuild complete.", "UI");
|
||||||
|
} else {
|
||||||
|
const details = [
|
||||||
|
`UI rebuild failed (exit ${buildResult.code ?? "unknown"}).`,
|
||||||
|
buildResult.stderr.trim() ? buildResult.stderr.trim() : null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
note(details, "UI");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_err) {
|
}
|
||||||
|
} catch {
|
||||||
// If files don't exist, we can't check.
|
// If files don't exist, we can't check.
|
||||||
// If git fails, we silently skip.
|
// If git fails, we silently skip.
|
||||||
// runtime.debug(`UI freshness check failed: ${String(err)}`);
|
// runtime.debug(`UI freshness check failed: ${String(err)}`);
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ import {
|
|||||||
maybeMigrateLegacyConfigFile,
|
maybeMigrateLegacyConfigFile,
|
||||||
normalizeLegacyConfigValues,
|
normalizeLegacyConfigValues,
|
||||||
} from "./doctor-legacy-config.js";
|
} from "./doctor-legacy-config.js";
|
||||||
import { maybeRepairUiProtocolFreshness } from "./doctor-ui.js";
|
|
||||||
import { createDoctorPrompter, type DoctorOptions } from "./doctor-prompter.js";
|
import { createDoctorPrompter, type DoctorOptions } from "./doctor-prompter.js";
|
||||||
import {
|
import {
|
||||||
maybeRepairSandboxImages,
|
maybeRepairSandboxImages,
|
||||||
@@ -81,6 +80,7 @@ import {
|
|||||||
detectLegacyStateMigrations,
|
detectLegacyStateMigrations,
|
||||||
runLegacyStateMigrations,
|
runLegacyStateMigrations,
|
||||||
} from "./doctor-state-migrations.js";
|
} from "./doctor-state-migrations.js";
|
||||||
|
import { maybeRepairUiProtocolFreshness } from "./doctor-ui.js";
|
||||||
import {
|
import {
|
||||||
detectLegacyWorkspaceDirs,
|
detectLegacyWorkspaceDirs,
|
||||||
formatLegacyWorkspaceWarning,
|
formatLegacyWorkspaceWarning,
|
||||||
@@ -249,6 +249,8 @@ export async function doctorCommand(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await maybeRepairUiProtocolFreshness(runtime, prompter);
|
||||||
|
|
||||||
await maybeMigrateLegacyConfigFile(runtime);
|
await maybeMigrateLegacyConfigFile(runtime);
|
||||||
|
|
||||||
const snapshot = await readConfigFileSnapshot();
|
const snapshot = await readConfigFileSnapshot();
|
||||||
@@ -479,11 +481,13 @@ export async function doctorCommand(
|
|||||||
note(
|
note(
|
||||||
[
|
[
|
||||||
`Eligible: ${skillsReport.skills.filter((s) => s.eligible).length}`,
|
`Eligible: ${skillsReport.skills.filter((s) => s.eligible).length}`,
|
||||||
`Missing requirements: ${skillsReport.skills.filter(
|
`Missing requirements: ${
|
||||||
|
skillsReport.skills.filter(
|
||||||
(s) => !s.eligible && !s.disabled && !s.blockedByAllowlist,
|
(s) => !s.eligible && !s.disabled && !s.blockedByAllowlist,
|
||||||
).length
|
).length
|
||||||
}`,
|
}`,
|
||||||
`Blocked by allowlist: ${skillsReport.skills.filter((s) => s.blockedByAllowlist).length
|
`Blocked by allowlist: ${
|
||||||
|
skillsReport.skills.filter((s) => s.blockedByAllowlist).length
|
||||||
}`,
|
}`,
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
"Skills status",
|
"Skills status",
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ declare global {
|
|||||||
const DEFAULT_CRON_FORM: CronFormState = {
|
const DEFAULT_CRON_FORM: CronFormState = {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
|
agentId: "",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scheduleKind: "every",
|
scheduleKind: "every",
|
||||||
scheduleAt: "",
|
scheduleAt: "",
|
||||||
|
|||||||
@@ -99,9 +99,11 @@ export async function addCronJob(state: CronState) {
|
|||||||
try {
|
try {
|
||||||
const schedule = buildCronSchedule(state.cronForm);
|
const schedule = buildCronSchedule(state.cronForm);
|
||||||
const payload = buildCronPayload(state.cronForm);
|
const payload = buildCronPayload(state.cronForm);
|
||||||
|
const agentId = state.cronForm.agentId.trim();
|
||||||
const job = {
|
const job = {
|
||||||
name: state.cronForm.name.trim(),
|
name: state.cronForm.name.trim(),
|
||||||
description: state.cronForm.description.trim() || undefined,
|
description: state.cronForm.description.trim() || undefined,
|
||||||
|
agentId: agentId || undefined,
|
||||||
enabled: state.cronForm.enabled,
|
enabled: state.cronForm.enabled,
|
||||||
schedule,
|
schedule,
|
||||||
sessionTarget: state.cronForm.sessionTarget,
|
sessionTarget: state.cronForm.sessionTarget,
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export type IMessageForm = {
|
|||||||
export type CronFormState = {
|
export type CronFormState = {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
agentId: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
scheduleKind: "at" | "every" | "cron";
|
scheduleKind: "at" | "every" | "cron";
|
||||||
scheduleAt: string;
|
scheduleAt: string;
|
||||||
|
|||||||
@@ -82,6 +82,15 @@ export function renderCron(props: CronProps) {
|
|||||||
props.onFormChange({ description: (e.target as HTMLInputElement).value })}
|
props.onFormChange({ description: (e.target as HTMLInputElement).value })}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Agent ID</span>
|
||||||
|
<input
|
||||||
|
.value=${props.form.agentId}
|
||||||
|
@input=${(e: Event) =>
|
||||||
|
props.onFormChange({ agentId: (e.target as HTMLInputElement).value })}
|
||||||
|
placeholder="default"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<label class="field checkbox">
|
<label class="field checkbox">
|
||||||
<span>Enabled</span>
|
<span>Enabled</span>
|
||||||
<input
|
<input
|
||||||
@@ -338,6 +347,7 @@ function renderJob(job: CronJob, props: CronProps) {
|
|||||||
<div class="list-title">${job.name}</div>
|
<div class="list-title">${job.name}</div>
|
||||||
<div class="list-sub">${formatCronSchedule(job)}</div>
|
<div class="list-sub">${formatCronSchedule(job)}</div>
|
||||||
<div class="muted">${formatCronPayload(job)}</div>
|
<div class="muted">${formatCronPayload(job)}</div>
|
||||||
|
${job.agentId ? html`<div class="muted">Agent: ${job.agentId}</div>` : nothing}
|
||||||
<div class="chip-row" style="margin-top: 6px;">
|
<div class="chip-row" style="margin-top: 6px;">
|
||||||
<span class="chip">${job.enabled ? "enabled" : "disabled"}</span>
|
<span class="chip">${job.enabled ? "enabled" : "disabled"}</span>
|
||||||
<span class="chip">${job.sessionTarget}</span>
|
<span class="chip">${job.sessionTarget}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user