From a06e9a4f56733ab9189a86360dbb594596fbc0ea Mon Sep 17 00:00:00 2001 From: Kai Liu Date: Tue, 21 Apr 2026 15:47:20 +0800 Subject: [PATCH 1/2] feat(pr-tracker): simplify config to use source channel defaults - Remove target channel picker/storage: polls post back to the source channel they were scanned from - Remove tracker-level agent override and global defaultAgentProvider; scheduler uses getChannelAgentProvider(sourceChannelId) - Switch poll interval UI to minutes (30-min default); DB still stores seconds so existing rows/migrations aren't touched - Add Switch UI component; replace per-tracker Enable/Disable button - Shrink the three per-tracker action buttons to 32px squares - Legacy DB columns (agent_provider, target_*) kept for backwards compat - Bump version to 0.1.36 --- package.json | 2 +- packages/config/local/pr-trackers.test.ts | 29 +-- packages/config/local/pr-trackers.ts | 152 +-------------- packages/core/cli-handlers/pr-tracker.ts | 49 +---- packages/core/pr-tracker/scheduler.ts | 47 +---- packages/core/web/routes/pr-trackers.ts | 14 -- .../web-ui/src/lib/components/ui/index.ts | 1 + .../src/lib/components/ui/switch.svelte | 43 +++++ .../routes/(settings)/pr-tracker/+page.svelte | 180 +++++++----------- 9 files changed, 147 insertions(+), 370 deletions(-) create mode 100644 packages/web-ui/src/lib/components/ui/switch.svelte diff --git a/package.json b/package.json index 1ce39057..a7bff292 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ode", - "version": "0.1.35", + "version": "0.1.36", "description": "Coding anywhere with your coding agents connected", "module": "packages/core/index.ts", "type": "module", diff --git a/packages/config/local/pr-trackers.test.ts b/packages/config/local/pr-trackers.test.ts index 9cbf2b98..e80890c5 100644 --- a/packages/config/local/pr-trackers.test.ts +++ b/packages/config/local/pr-trackers.test.ts @@ -7,7 +7,6 @@ import type { GitHubRepo } from "@/utils/git-remote"; import { clearPrTrackersForTests, closePrTrackerDatabaseForTests, - DEFAULT_PR_AGENT_PROVIDER, DEFAULT_PR_POLL_INTERVAL_SEC, DEFAULT_PR_PROMPT_TEMPLATE, deletePrTracker, @@ -111,7 +110,6 @@ describe("pr-trackers settings", () => { test("returns defaults on first access", () => { const settings = getPrTrackerSettings(); expect(settings.defaultPollIntervalSec).toBe(DEFAULT_PR_POLL_INTERVAL_SEC); - expect(settings.defaultAgentProvider).toBe(DEFAULT_PR_AGENT_PROVIDER); expect(settings.defaultPromptTemplate).toBe(DEFAULT_PR_PROMPT_TEMPLATE); expect(settings.defaultGithubToken).toBe(""); }); @@ -119,24 +117,16 @@ describe("pr-trackers settings", () => { test("updates persist", () => { updatePrTrackerSettings({ defaultPollIntervalSec: 600, - defaultAgentProvider: "claudecode", defaultGithubToken: " ghp_abc ", }); const settings = getPrTrackerSettings(); expect(settings.defaultPollIntervalSec).toBe(600); - expect(settings.defaultAgentProvider).toBe("claudecode"); expect(settings.defaultGithubToken).toBe("ghp_abc"); }); test("rejects too-short poll interval", () => { expect(() => updatePrTrackerSettings({ defaultPollIntervalSec: 30 })).toThrow(); }); - - test("rejects invalid agent provider", () => { - expect(() => - updatePrTrackerSettings({ defaultAgentProvider: "bogus" }) - ).toThrow(/Unsupported agent/); - }); }); describe("pr-trackers scan", () => { @@ -206,14 +196,13 @@ describe("pr-trackers scan", () => { }); describe("pr-trackers enable/update", () => { - test("enabling without a target channel throws", () => { + test("enabling a scanned tracker does not require any extra config", () => { scanPrTrackers( makeProbe({ "/tmp/repos/ode": { host: "github.com", owner: "anomalyco", repo: "ode" } }) ); const tracker = listPrTrackers()[0]!; - expect(() => updatePrTracker(tracker.id, { enabled: true })).toThrow( - /target channel/ - ); + const updated = updatePrTracker(tracker.id, { enabled: true }); + expect(updated.enabled).toBe(true); }); test("first enable snaps last_polled_at to now so we don't backfill", () => { @@ -226,12 +215,10 @@ describe("pr-trackers enable/update", () => { const before = Date.now(); const updated = updatePrTracker(tracker.id, { enabled: true, - targetChannelId: "C_WEB", }); const after = Date.now(); expect(updated.enabled).toBe(true); - expect(updated.targetChannelId).toBe("C_WEB"); expect(updated.lastPolledAt).not.toBeNull(); expect(updated.lastPolledAt!).toBeGreaterThanOrEqual(before); expect(updated.lastPolledAt!).toBeLessThanOrEqual(after); @@ -242,7 +229,7 @@ describe("pr-trackers enable/update", () => { makeProbe({ "/tmp/repos/ode": { host: "github.com", owner: "anomalyco", repo: "ode" } }) ); const id = listPrTrackers()[0]!.id; - const enabled = updatePrTracker(id, { enabled: true, targetChannelId: "C_DEV" }); + const enabled = updatePrTracker(id, { enabled: true }); const originalCursor = enabled.lastPolledAt!; updatePrTracker(id, { enabled: false }); @@ -251,7 +238,7 @@ describe("pr-trackers enable/update", () => { const afterPoll = getPrTrackerById(id)!; expect(afterPoll.lastPolledAt).toBe(originalCursor + 10_000); - const reEnabled = updatePrTracker(id, { enabled: true, targetChannelId: "C_DEV" }); + const reEnabled = updatePrTracker(id, { enabled: true }); expect(reEnabled.lastPolledAt).toBe(originalCursor + 10_000); }); @@ -262,7 +249,7 @@ describe("pr-trackers enable/update", () => { scanPrTrackers(makeProbe({})); const id = listPrTrackers()[0]!.id; expect(() => - updatePrTracker(id, { enabled: true, targetChannelId: "C_DEV" }) + updatePrTracker(id, { enabled: true }) ).toThrow(/missing/); }); @@ -289,7 +276,7 @@ describe("pr-trackers due selection", () => { makeProbe({ "/tmp/repos/ode": { host: "github.com", owner: "anomalyco", repo: "ode" } }) ); const id = listPrTrackers()[0]!.id; - updatePrTracker(id, { enabled: true, targetChannelId: "C_DEV", pollIntervalSec: 60 }); + updatePrTracker(id, { enabled: true, pollIntervalSec: 60 }); // Simulate a very old last-poll timestamp. markPrTrackerPolled(id, { success: true, pollCompletedAt: 0 }); const due = listDuePrTrackers(Date.now()); @@ -301,7 +288,7 @@ describe("pr-trackers due selection", () => { makeProbe({ "/tmp/repos/ode": { host: "github.com", owner: "anomalyco", repo: "ode" } }) ); const id = listPrTrackers()[0]!.id; - updatePrTracker(id, { enabled: true, targetChannelId: "C_DEV", pollIntervalSec: 1800 }); + updatePrTracker(id, { enabled: true, pollIntervalSec: 1800 }); markPrTrackerPolled(id, { success: true }); expect(listDuePrTrackers(Date.now())).toHaveLength(0); }); diff --git a/packages/config/local/pr-trackers.ts b/packages/config/local/pr-trackers.ts index ded38907..c07a01cc 100644 --- a/packages/config/local/pr-trackers.ts +++ b/packages/config/local/pr-trackers.ts @@ -2,7 +2,6 @@ import { Database } from "bun:sqlite"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { AGENT_PROVIDERS, isAgentProviderId } from "@/shared/agent-provider"; import { getGitHubRepoFromCwd, type GitHubRepo } from "@/utils/git-remote"; import { loadOdeConfig } from "./ode-store"; @@ -25,7 +24,7 @@ export type PrTrackerPlatform = "slack" | "discord" | "lark"; export type PrTrackerRecord = { id: string; - /** Source channel: where the working directory comes from. */ + /** Source channel: where the working directory comes from and where poll results are posted back. */ sourceWorkspaceId: string; sourceWorkspaceName: string | null; sourceChannelId: string; @@ -37,18 +36,11 @@ export type PrTrackerRecord = { repoHost: string; enabled: boolean; /** Null = use global default. */ - agentProvider: string | null; - /** Null = use global default. */ promptTemplate: string | null; /** Null = use global default. */ pollIntervalSec: number | null; /** Null = use global default token / gh CLI fallback. */ githubToken: string | null; - /** Where the agent result is posted. Required to be set before enable. */ - targetWorkspaceId: string | null; - targetChannelId: string | null; - targetChannelName: string | null; - targetPlatform: PrTrackerPlatform | null; /** * Cursor: last successful poll completion. The next poll asks GitHub for * comments/reviews `since` this timestamp. Set to `now` when the tracker @@ -86,7 +78,6 @@ export type PrTrackerEventRecord = { export type PrTrackerSettings = { defaultPollIntervalSec: number; - defaultAgentProvider: string; defaultPromptTemplate: string; /** Optional global GitHub token. Empty string = fall back to `gh auth token`. */ defaultGithubToken: string; @@ -94,7 +85,6 @@ export type PrTrackerSettings = { }; export const DEFAULT_PR_POLL_INTERVAL_SEC = 30 * 60; // 30 minutes -export const DEFAULT_PR_AGENT_PROVIDER = "opencode"; export const DEFAULT_PR_PROMPT_TEMPLATE = `A pull request in {{repo_full_name}} has new activity: PR #{{pr_number}}: {{pr_title}} by {{pr_author}} URL: {{pr_url}} @@ -197,7 +187,7 @@ function initializeDatabase(db: Database): void { CREATE TABLE IF NOT EXISTS pr_tracker_settings ( id INTEGER PRIMARY KEY CHECK (id = 1), default_poll_interval_sec INTEGER NOT NULL, - default_agent_provider TEXT NOT NULL, + default_agent_provider TEXT NOT NULL DEFAULT 'opencode', default_prompt_template TEXT NOT NULL, default_github_token TEXT NOT NULL DEFAULT '', updated_at INTEGER NOT NULL @@ -242,14 +232,11 @@ type PrTrackerRow = { repo_name: string; repo_host: string; enabled: number; - agent_provider: string | null; + // Legacy columns (`agent_provider`, `target_*`) are kept on disk for + // backwards compatibility with existing DBs but are no longer read. prompt_template: string | null; poll_interval_sec: number | null; github_token: string | null; - target_workspace_id: string | null; - target_channel_id: string | null; - target_channel_name: string | null; - target_platform: PrTrackerPlatform | null; last_polled_at: number | null; last_success_at: number | null; last_error: string | null; @@ -271,14 +258,9 @@ function mapTrackerRow(row: PrTrackerRow): PrTrackerRecord { repoName: row.repo_name, repoHost: row.repo_host, enabled: row.enabled === 1, - agentProvider: row.agent_provider, promptTemplate: row.prompt_template, pollIntervalSec: row.poll_interval_sec, githubToken: row.github_token, - targetWorkspaceId: row.target_workspace_id, - targetChannelId: row.target_channel_id, - targetChannelName: row.target_channel_name, - targetPlatform: row.target_platform, lastPolledAt: row.last_polled_at, lastSuccessAt: row.last_success_at, lastError: row.last_error, @@ -332,10 +314,9 @@ function ensureSettingsRow(db: Database): void { default_prompt_template, default_github_token, updated_at - ) VALUES (1, ?, ?, ?, '', ?) + ) VALUES (1, ?, 'opencode', ?, '', ?) `).run( DEFAULT_PR_POLL_INTERVAL_SEC, - DEFAULT_PR_AGENT_PROVIDER, DEFAULT_PR_PROMPT_TEMPLATE, now ); @@ -348,7 +329,6 @@ export function getPrTrackerSettings(): PrTrackerSettings { .query(` SELECT default_poll_interval_sec, - default_agent_provider, default_prompt_template, default_github_token, updated_at @@ -357,14 +337,12 @@ export function getPrTrackerSettings(): PrTrackerSettings { `) .get() as { default_poll_interval_sec: number; - default_agent_provider: string; default_prompt_template: string; default_github_token: string; updated_at: number; }; return { defaultPollIntervalSec: row.default_poll_interval_sec, - defaultAgentProvider: row.default_agent_provider, defaultPromptTemplate: row.default_prompt_template, defaultGithubToken: row.default_github_token, updatedAt: row.updated_at, @@ -373,7 +351,6 @@ export function getPrTrackerSettings(): PrTrackerSettings { export type UpdatePrTrackerSettingsParams = Partial<{ defaultPollIntervalSec: number; - defaultAgentProvider: string; defaultPromptTemplate: string; defaultGithubToken: string; }>; @@ -389,10 +366,6 @@ export function updatePrTrackerSettings( params.defaultPollIntervalSec !== undefined ? normalizePollInterval(params.defaultPollIntervalSec) : current.defaultPollIntervalSec; - const agentProvider = - params.defaultAgentProvider !== undefined - ? normalizeRequiredAgent(params.defaultAgentProvider) - : current.defaultAgentProvider; const promptTemplate = params.defaultPromptTemplate !== undefined ? normalizeRequiredText(params.defaultPromptTemplate, "promptTemplate") @@ -407,12 +380,11 @@ export function updatePrTrackerSettings( UPDATE pr_tracker_settings SET default_poll_interval_sec = ?, - default_agent_provider = ?, default_prompt_template = ?, default_github_token = ?, updated_at = ? WHERE id = 1 - `).run(pollInterval, agentProvider, promptTemplate, githubToken, now); + `).run(pollInterval, promptTemplate, githubToken, now); return getPrTrackerSettings(); } @@ -428,82 +400,12 @@ function normalizePollInterval(value: number): number { return Math.floor(value); } -function normalizeOptionalAgent(value: string | null | undefined): string | null { - if (value === null || value === undefined) return null; - const trimmed = value.trim().toLowerCase(); - if (trimmed.length === 0) return null; - if (!isAgentProviderId(trimmed)) { - throw new Error( - `Unsupported agent "${value}". Expected one of: ${AGENT_PROVIDERS.join(", ")}` - ); - } - return trimmed; -} - -function normalizeRequiredAgent(value: string): string { - const out = normalizeOptionalAgent(value); - if (!out) throw new Error("agentProvider is required"); - return out; -} - function normalizeRequiredText(value: string, field: string): string { const trimmed = value.trim(); if (!trimmed) throw new Error(`${field} cannot be empty`); return trimmed; } -function resolveConfigChannelId(channelId: string): string { - const trimmed = channelId.trim(); - if (!trimmed) return trimmed; - const delimiter = "::"; - const index = trimmed.lastIndexOf(delimiter); - if (index < 0) return trimmed; - const raw = trimmed.slice(index + delimiter.length).trim(); - return raw || trimmed; -} - -type ChannelSnapshot = { - platform: PrTrackerPlatform; - workspaceId: string; - workspaceName: string; - channelId: string; - channelName: string; - workingDirectory: string; -}; - -function getChannelSnapshot(channelId: string): ChannelSnapshot { - const resolved = resolveConfigChannelId(channelId); - const config = loadOdeConfig(); - for (const workspace of config.workspaces) { - const channel = workspace.channelDetails.find((item) => item.id === resolved); - if (!channel) continue; - return { - platform: workspace.type, - workspaceId: workspace.id, - workspaceName: workspace.name || workspace.id, - channelId: channel.id, - channelName: channel.name || channel.id, - workingDirectory: channel.workingDirectory?.trim() || "", - }; - } - throw new Error(`Channel ${channelId} not found in configured workspaces`); -} - -function getChannelSnapshotForTarget(channelId: string): { - platform: PrTrackerPlatform; - workspaceId: string; - channelId: string; - channelName: string; -} { - const snap = getChannelSnapshot(channelId); - return { - platform: snap.platform, - workspaceId: snap.workspaceId, - channelId: snap.channelId, - channelName: snap.channelName, - }; -} - // --------------------------------------------------------------------------- // Scan: walk every workspace channel, derive (cwd, github repo) pairs, and // upsert pr_trackers rows so the UI can list them. Existing rows are kept @@ -713,11 +615,9 @@ export function listDuePrTrackers(nowMs: number = Date.now()): PrTrackerRecord[] export type UpdatePrTrackerParams = Partial<{ enabled: boolean; - agentProvider: string | null; promptTemplate: string | null; pollIntervalSec: number | null; githubToken: string | null; - targetChannelId: string | null; }>; export function updatePrTracker(id: string, params: UpdatePrTrackerParams): PrTrackerRecord { @@ -732,11 +632,6 @@ export function updatePrTracker(id: string, params: UpdatePrTrackerParams): PrTr const enabled = params.enabled !== undefined ? (params.enabled ? 1 : 0) : existing.enabled ? 1 : 0; - const agentProvider = - params.agentProvider !== undefined - ? normalizeOptionalAgent(params.agentProvider) - : existing.agentProvider; - const promptTemplate = params.promptTemplate !== undefined ? params.promptTemplate === null || params.promptTemplate.trim() === "" @@ -758,31 +653,6 @@ export function updatePrTracker(id: string, params: UpdatePrTrackerParams): PrTr : params.githubToken.trim() : existing.githubToken; - let targetWorkspaceId = existing.targetWorkspaceId; - let targetChannelId = existing.targetChannelId; - let targetChannelName = existing.targetChannelName; - let targetPlatform = existing.targetPlatform; - - if (params.targetChannelId !== undefined) { - if (params.targetChannelId === null || params.targetChannelId.trim() === "") { - targetWorkspaceId = null; - targetChannelId = null; - targetChannelName = null; - targetPlatform = null; - } else { - const snap = getChannelSnapshotForTarget(params.targetChannelId); - targetWorkspaceId = snap.workspaceId; - targetChannelId = snap.channelId; - targetChannelName = snap.channelName; - targetPlatform = snap.platform; - } - } - - // Enforce: enabling requires a target channel. - if (enabled === 1 && !targetChannelId) { - throw new Error("Cannot enable a tracker without a target channel"); - } - // First-enable bookkeeping: when transitioning from disabled→enabled and // there's no prior poll cursor, set last_polled_at to "now" so we don't // backfill historical events. Re-enabling later keeps the existing cursor. @@ -795,27 +665,17 @@ export function updatePrTracker(id: string, params: UpdatePrTrackerParams): PrTr UPDATE pr_trackers SET enabled = ?, - agent_provider = ?, prompt_template = ?, poll_interval_sec = ?, github_token = ?, - target_workspace_id = ?, - target_channel_id = ?, - target_channel_name = ?, - target_platform = ?, last_polled_at = ?, updated_at = ? WHERE id = ? `).run( enabled, - agentProvider, promptTemplate, pollIntervalSec, githubToken, - targetWorkspaceId, - targetChannelId, - targetChannelName, - targetPlatform, nextLastPolledAt, now, id diff --git a/packages/core/cli-handlers/pr-tracker.ts b/packages/core/cli-handlers/pr-tracker.ts index acef42a9..87152604 100644 --- a/packages/core/cli-handlers/pr-tracker.ts +++ b/packages/core/cli-handlers/pr-tracker.ts @@ -103,16 +103,12 @@ function printTrackerRow(tracker: PrTrackerRecord): void { ? "enabled" : "disabled"; const source = `${tracker.sourceWorkspaceName || tracker.sourceWorkspaceId}/${tracker.sourceChannelName || tracker.sourceChannelId}`; - const target = tracker.targetChannelId - ? `${tracker.targetChannelName || tracker.targetChannelId}` - : "(none)"; console.log( [ tracker.id, state.padEnd(9), `${tracker.repoOwner}/${tracker.repoName}`.padEnd(32), - `from=${source}`, - `to=${target}`, + `channel=${source}`, `lastPoll=${formatTimestamp(tracker.lastPolledAt)}`, ].join(" "), ); @@ -124,15 +120,9 @@ function printTrackerDetail(tracker: PrTrackerRecord, settings?: PrTrackerSettin console.log(`enabled: ${tracker.enabled}`); console.log(`missingSince: ${formatTimestamp(tracker.missingSince)}`); console.log( - `sourceChannel: ${tracker.sourceWorkspaceName || tracker.sourceWorkspaceId} / ${tracker.sourceChannelName || tracker.sourceChannelId} (${tracker.sourceChannelId})`, + `channel: ${tracker.sourceWorkspaceName || tracker.sourceWorkspaceId} / ${tracker.sourceChannelName || tracker.sourceChannelId} (${tracker.sourceChannelId})`, ); console.log(`workingDirectory: ${tracker.workingDirectory}`); - console.log( - `targetChannel: ${tracker.targetChannelId ? `${tracker.targetChannelName || tracker.targetChannelId} (${tracker.targetChannelId})` : "(none)"}`, - ); - console.log( - `agent: ${tracker.agentProvider ?? `(default: ${settings?.defaultAgentProvider ?? "opencode"})`}`, - ); console.log( `pollIntervalSec: ${tracker.pollIntervalSec ?? `(default: ${settings?.defaultPollIntervalSec ?? "-"})`}`, ); @@ -170,21 +160,22 @@ function printHelp(): void { " ode pr-tracker list [--enabled | --disabled | --missing] [--json]", " ode pr-tracker show [--json]", " ode pr-tracker scan [--json]", - " ode pr-tracker enable [--target-channel ]", + " ode pr-tracker enable ", " ode pr-tracker disable ", - " ode pr-tracker update [--agent ] [--prompt ] [--prompt-file ]", - " [--interval ] [--token ] [--target-channel ]", + " ode pr-tracker update [--prompt ] [--prompt-file ]", + " [--interval ] [--token ]", " ode pr-tracker run ", " ode pr-tracker delete ", " ode pr-tracker events [--limit N] [--json]", - " ode pr-tracker settings [--show | --set-interval | --set-agent ", + " ode pr-tracker settings [--show | --set-interval ", " --set-prompt-file | --set-token ]", "", "Notes:", " Tracker rows are populated by `ode pr-tracker scan`, which walks every", " configured channel's workingDirectory and extracts GitHub repos from", " the origin remote. Each tracker is disabled by default.", - " Enabling a tracker requires a --target-channel (where the agent posts results).", + " Poll results are posted back to the source channel using its configured", + " default agent.", ].join("\n"), ); } @@ -195,7 +186,6 @@ function printHelp(): void { type ListPayload = { trackers: PrTrackerRecord[]; - channels: Array<{ value: string; label: string }>; settings: PrTrackerSettings; }; @@ -258,21 +248,15 @@ async function handleScan(args: CliArgs): Promise { async function handleUpdate(args: CliArgs, idFromCmd?: string): Promise { const { flags, positional } = parseFlags(args, { - agent: true, prompt: true, "prompt-file": true, interval: true, token: true, - "target-channel": true, }); const id = idFromCmd ?? positional[0]; if (!id) throw new Error("Tracker id is required"); const body: Record = {}; - if (flags.agent !== undefined) { - const raw = (flags.agent as string).trim().toLowerCase(); - body.agentProvider = raw === "default" ? null : raw; - } if (flags["prompt-file"] !== undefined) { const file = Bun.file(flags["prompt-file"] as string); body.promptTemplate = (await file.text()).trim(); @@ -288,10 +272,6 @@ async function handleUpdate(args: CliArgs, idFromCmd?: string): Promise( `/api/pr-trackers/${encodeURIComponent(id)}`, @@ -305,19 +285,15 @@ async function handleUpdate(args: CliArgs, idFromCmd?: string): Promise { - const { flags, positional } = parseFlags(args, { "target-channel": true }); + const { positional } = parseFlags(args, {}); const id = positional[0]; if (!id) throw new Error("Tracker id is required: ode pr-tracker enable "); - const body: Record = { enabled: true }; - if (flags["target-channel"] !== undefined) { - body.targetChannelId = (flags["target-channel"] as string).trim() || null; - } const result = await apiFetch<{ tracker: PrTrackerRecord }>( `/api/pr-trackers/${encodeURIComponent(id)}`, { method: "PUT", headers: { "content-type": "application/json" }, - body: JSON.stringify(body), + body: JSON.stringify({ enabled: true }), }, ); console.log(`Tracker ${id} enabled.`); @@ -383,14 +359,12 @@ async function handleSettings(args: CliArgs): Promise { const { flags } = parseFlags(args, { show: false, "set-interval": true, - "set-agent": true, "set-prompt-file": true, "set-token": true, }); const mutates = flags["set-interval"] !== undefined || - flags["set-agent"] !== undefined || flags["set-prompt-file"] !== undefined || flags["set-token"] !== undefined; @@ -401,9 +375,6 @@ async function handleSettings(args: CliArgs): Promise { if (!Number.isFinite(n)) throw new Error("--set-interval must be a number"); body.defaultPollIntervalSec = n; } - if (flags["set-agent"] !== undefined) { - body.defaultAgentProvider = (flags["set-agent"] as string).trim(); - } if (flags["set-prompt-file"] !== undefined) { const file = Bun.file(flags["set-prompt-file"] as string); body.defaultPromptTemplate = (await file.text()).trim(); diff --git a/packages/core/pr-tracker/scheduler.ts b/packages/core/pr-tracker/scheduler.ts index 747415f3..980ac352 100644 --- a/packages/core/pr-tracker/scheduler.ts +++ b/packages/core/pr-tracker/scheduler.ts @@ -3,13 +3,11 @@ import type { OpenCodeMessageContext } from "@/agents"; import { getChannelAgentProvider, getChannelBaseBranch, - getChannelModel, getChannelSystemMessage, getUserGeneralSettings, resolveChannelCwd, } from "@/config"; import { - DEFAULT_PR_AGENT_PROVIDER, DEFAULT_PR_PROMPT_TEMPLATE, getPrTrackerById, getPrTrackerSettings, @@ -32,7 +30,6 @@ import { sendChannelMessage as sendLarkChannelMessage } from "@/ims/lark/client" import { sendChannelMessage as sendSlackChannelMessage } from "@/ims/slack/client"; import { type AgentProviderId, - isAgentProviderId, } from "@/shared/agent-provider"; import { log } from "@/utils"; import { @@ -117,12 +114,9 @@ let prTrackerTimer: ReturnType | null = null; const runningTrackerIds = new Set(); function resolveAgentProvider(tracker: PrTrackerRecord): AgentProviderId { - const override = tracker.agentProvider?.trim(); - if (override && isAgentProviderId(override)) return override; - const fallback = getPrTrackerSettings().defaultAgentProvider; - if (isAgentProviderId(fallback)) return fallback; - if (isAgentProviderId(DEFAULT_PR_AGENT_PROVIDER)) return DEFAULT_PR_AGENT_PROVIDER; - // Last resort: take the tracker's source channel default. + // Tracker poll runs use the source channel's configured default agent. + // No tracker-level override; per-channel agent already lives in the channel + // settings. return getChannelAgentProvider(tracker.sourceChannelId); } @@ -141,27 +135,19 @@ function resolveToken(tracker: PrTrackerRecord): string | null { }); } -function resolveTargetPlatform(tracker: PrTrackerRecord): "slack" | "discord" | "lark" { - return (tracker.targetPlatform ?? tracker.sourcePlatform) as - | "slack" - | "discord" - | "lark"; -} - -async function sendToTargetChannel( +async function sendToSourceChannel( tracker: PrTrackerRecord, text: string, ): Promise { - if (!tracker.targetChannelId) return undefined; - const platform = resolveTargetPlatform(tracker); + const platform = tracker.sourcePlatform; if (platform === "slack") { // Per spec: post as a top-level channel message, not threaded. - return await sendSlackChannelMessage(tracker.targetChannelId, text); + return await sendSlackChannelMessage(tracker.sourceChannelId, text); } if (platform === "discord") { - return await sendDiscordChannelMessage(tracker.targetChannelId, text); + return await sendDiscordChannelMessage(tracker.sourceChannelId, text); } - return await sendLarkChannelMessage(tracker.targetChannelId, text); + return await sendLarkChannelMessage(tracker.sourceChannelId, text); } function syntheticThreadIdForPr(trackerId: string, prNumber: number, ts: number): string { @@ -307,10 +293,10 @@ async function runForPr(tracker: PrTrackerRecord, summary: PrSummary): Promise in ${repoFullName}`; // Slack-only mrkdwn link; Discord/Lark receive the URL inline. const bodyHeader = - tracker.targetPlatform === "slack" + tracker.sourcePlatform === "slack" ? header : `PR update: #${summary.prNumber} ${summary.title} (${summary.url}) in ${repoFullName}`; - await sendToTargetChannel(tracker, `${bodyHeader}\n\n${finalText}`); + await sendToSourceChannel(tracker, `${bodyHeader}\n\n${finalText}`); // Record per-event dedupe rows + an aggregate row. for (const event of summary.events) { @@ -424,19 +410,6 @@ export async function pollTracker(trackerId: string): Promise { error: "tracker disabled", }; } - if (!tracker.targetChannelId) { - markPrTrackerPolled(tracker.id, { - success: false, - errorMessage: "target channel not configured", - }); - return { - trackerId, - prsScanned: 0, - prsHandled: 0, - prsSkipped: 0, - error: "no target channel", - }; - } const token = resolveToken(tracker); const since = tracker.lastPolledAt ?? Date.now() - 24 * 60 * 60_000; diff --git a/packages/core/web/routes/pr-trackers.ts b/packages/core/web/routes/pr-trackers.ts index ef020d54..b2f2f983 100644 --- a/packages/core/web/routes/pr-trackers.ts +++ b/packages/core/web/routes/pr-trackers.ts @@ -11,7 +11,6 @@ import { type UpdatePrTrackerParams, type UpdatePrTrackerSettingsParams, } from "@/config/local/pr-trackers"; -import { listTaskChannelOptions } from "@/config/local/tasks"; import { triggerPrTrackerNow } from "@/core/pr-tracker/scheduler"; import { log } from "@/utils"; import { jsonResponse, readJsonBody, runRoute } from "../http"; @@ -77,10 +76,6 @@ function parseTrackerUpdate(payload: Record): UpdatePrTrackerPa const update: UpdatePrTrackerParams = {}; const enabled = getBoolean(payload, "enabled"); if (enabled !== undefined) update.enabled = enabled; - if ("agentProvider" in payload) { - const raw = getOptionalString(payload, "agentProvider"); - update.agentProvider = raw === undefined ? null : raw; - } if ("promptTemplate" in payload) { const raw = getOptionalString(payload, "promptTemplate"); update.promptTemplate = raw === undefined ? null : raw; @@ -93,10 +88,6 @@ function parseTrackerUpdate(payload: Record): UpdatePrTrackerPa const raw = getOptionalString(payload, "githubToken"); update.githubToken = raw === undefined ? null : raw; } - if ("targetChannelId" in payload) { - const raw = getOptionalString(payload, "targetChannelId"); - update.targetChannelId = raw === undefined ? null : raw; - } return update; } @@ -106,8 +97,6 @@ function parseSettingsUpdate( const update: UpdatePrTrackerSettingsParams = {}; const interval = getNumber(payload, "defaultPollIntervalSec"); if (interval !== undefined) update.defaultPollIntervalSec = interval; - const agent = getOptionalString(payload, "defaultAgentProvider"); - if (agent !== undefined) update.defaultAgentProvider = agent ?? ""; const prompt = getOptionalString(payload, "defaultPromptTemplate"); if (prompt !== undefined) update.defaultPromptTemplate = prompt ?? ""; const token = getOptionalString(payload, "defaultGithubToken"); @@ -118,7 +107,6 @@ function parseSettingsUpdate( function buildListPayload() { return { trackers: listPrTrackers(), - channels: listTaskChannelOptions(), settings: getPrTrackerSettings(), }; } @@ -142,7 +130,6 @@ export function registerPrTrackerRoutes(app: Elysia): void { return { tracker, events: listPrTrackerEvents(id, 100), - channels: listTaskChannelOptions(), settings: getPrTrackerSettings(), }; }, @@ -185,7 +172,6 @@ export function registerPrTrackerRoutes(app: Elysia): void { resolveStatus: (message) => { if (message === "Missing tracker id") return 400; if (message === "PR tracker not found") return 404; - if (message.includes("target channel")) return 400; if (message.includes("missing")) return 409; return 400; }, diff --git a/packages/web-ui/src/lib/components/ui/index.ts b/packages/web-ui/src/lib/components/ui/index.ts index f4862c20..edbc2708 100644 --- a/packages/web-ui/src/lib/components/ui/index.ts +++ b/packages/web-ui/src/lib/components/ui/index.ts @@ -6,4 +6,5 @@ export { default as Textarea } from "./textarea.svelte"; export { default as Select } from "./select.svelte"; export { default as Badge } from "./badge.svelte"; export { default as Separator } from "./separator.svelte"; +export { default as Switch } from "./switch.svelte"; export { default as ToggleGroup } from "./toggle-group.svelte"; diff --git a/packages/web-ui/src/lib/components/ui/switch.svelte b/packages/web-ui/src/lib/components/ui/switch.svelte new file mode 100644 index 00000000..43e23b38 --- /dev/null +++ b/packages/web-ui/src/lib/components/ui/switch.svelte @@ -0,0 +1,43 @@ + + + diff --git a/packages/web-ui/src/routes/(settings)/pr-tracker/+page.svelte b/packages/web-ui/src/routes/(settings)/pr-tracker/+page.svelte index e20c5289..9c625833 100644 --- a/packages/web-ui/src/routes/(settings)/pr-tracker/+page.svelte +++ b/packages/web-ui/src/routes/(settings)/pr-tracker/+page.svelte @@ -1,13 +1,8 @@ diff --git a/packages/web-ui/src/routes/(settings)/pr-tracker/+page.svelte b/packages/web-ui/src/routes/(settings)/pr-tracker/+page.svelte index 9c625833..816c3128 100644 --- a/packages/web-ui/src/routes/(settings)/pr-tracker/+page.svelte +++ b/packages/web-ui/src/routes/(settings)/pr-tracker/+page.svelte @@ -450,7 +450,7 @@ checked={tracker.enabled} disabled={isSaving || tracker.missingSince !== null} ariaLabel={tracker.enabled ? t("Disable", "停用") : t("Enable", "启用")} - on:click={() => void toggleEnabled(tracker)} + on:change={() => void toggleEnabled(tracker)} />