opencode-supertask 0.1.39 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/web/index.js CHANGED
@@ -9100,8 +9100,8 @@ var require_CronFileParser = __commonJS({
9100
9100
  * @throws If file cannot be read
9101
9101
  */
9102
9102
  static parseFileSync(filePath) {
9103
- const { readFileSync: readFileSync4 } = __require("fs");
9104
- const data = readFileSync4(filePath, "utf8");
9103
+ const { readFileSync: readFileSync3 } = __require("fs");
9104
+ const data = readFileSync3(filePath, "utf8");
9105
9105
  return _CronFileParser.#parseContent(data);
9106
9106
  }
9107
9107
  /**
@@ -13085,14 +13085,14 @@ function mapRelationalRow(tablesConfig, tableConfig, row, buildQueryResultSelect
13085
13085
  // src/web/index.tsx
13086
13086
  import {
13087
13087
  existsSync as existsSync5,
13088
- mkdirSync as mkdirSync4,
13089
- readFileSync as readFileSync3,
13088
+ mkdirSync as mkdirSync3,
13089
+ readFileSync as readFileSync2,
13090
13090
  readdirSync,
13091
13091
  renameSync as renameSync2,
13092
- statSync as statSync4,
13093
- writeFileSync as writeFileSync3
13092
+ statSync as statSync3,
13093
+ writeFileSync as writeFileSync2
13094
13094
  } from "fs";
13095
- import { homedir as homedir4 } from "os";
13095
+ import { homedir as homedir3 } from "os";
13096
13096
  import { basename as basename2, dirname as dirname4, join as join4 } from "path";
13097
13097
 
13098
13098
  // src/core/db/index.ts
@@ -16395,6 +16395,7 @@ var tasks = sqliteTable("tasks", {
16395
16395
  name: text("name").notNull(),
16396
16396
  agent: text("agent").notNull(),
16397
16397
  model: text("model").default("default"),
16398
+ variant: text("variant"),
16398
16399
  prompt: text("prompt").notNull(),
16399
16400
  cwd: text("cwd"),
16400
16401
  // 分类与优先级
@@ -16439,6 +16440,7 @@ var taskRuns = sqliteTable("task_runs", {
16439
16440
  taskId: integer("task_id").notNull().references(() => tasks.id, { onDelete: "cascade" }),
16440
16441
  sessionId: text("session_id"),
16441
16442
  model: text("model"),
16443
+ variant: text("variant"),
16442
16444
  status: text("status").default("running"),
16443
16445
  startedAt: integer("started_at", { mode: "timestamp" }).$defaultFn(() => /* @__PURE__ */ new Date()),
16444
16446
  finishedAt: integer("finished_at", { mode: "timestamp" }),
@@ -16459,6 +16461,7 @@ var taskTemplates = sqliteTable("task_templates", {
16459
16461
  name: text("name").notNull(),
16460
16462
  agent: text("agent").notNull(),
16461
16463
  model: text("model").default("default"),
16464
+ variant: text("variant"),
16462
16465
  prompt: text("prompt").notNull(),
16463
16466
  cwd: text("cwd"),
16464
16467
  category: text("category").default("general"),
@@ -16499,8 +16502,8 @@ var _sqlite = null;
16499
16502
  var _db = null;
16500
16503
  var _migrationRan = false;
16501
16504
  function getMigrationsFolder() {
16502
- const __dirname2 = dirname(fileURLToPath(import.meta.url));
16503
- let dir = __dirname2;
16505
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16506
+ let dir = __dirname;
16504
16507
  for (let i = 0; i < 5; i++) {
16505
16508
  const candidate = join(dir, "drizzle", "meta", "_journal.json");
16506
16509
  if (existsSync(candidate)) {
@@ -16508,7 +16511,7 @@ function getMigrationsFolder() {
16508
16511
  }
16509
16512
  dir = dirname(dir);
16510
16513
  }
16511
- return join(__dirname2, "../../drizzle");
16514
+ return join(__dirname, "../../drizzle");
16512
16515
  }
16513
16516
  function ensureGatewayLock(sqliteDb) {
16514
16517
  sqliteDb.exec(`
@@ -16657,26 +16660,60 @@ var CATALOG_CACHE_MS = 3e4;
16657
16660
  var COMMAND_TIMEOUT_MS = 2e4;
16658
16661
  var MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
16659
16662
  var ANSI_PATTERN = /\x1B(?:[@-_][0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
16663
+ var OpenCodeCommandExitError = class extends Error {
16664
+ };
16660
16665
  var catalogCache = /* @__PURE__ */ new Map();
16661
16666
  function cleanOutput(value) {
16662
16667
  return value.replace(ANSI_PATTERN, "").replace(/\r/g, "");
16663
16668
  }
16664
16669
  function runOpenCode(executable, args, cwd, timeoutMs) {
16665
- return new Promise((resolve3, reject) => {
16670
+ return new Promise((resolve2, reject) => {
16666
16671
  const child = spawn(executable, args, {
16667
16672
  cwd,
16668
16673
  env: process.env,
16669
- stdio: ["ignore", "pipe", "pipe"]
16674
+ stdio: ["ignore", "pipe", "pipe"],
16675
+ detached: process.platform !== "win32"
16670
16676
  });
16671
16677
  let stdout = "";
16672
16678
  let stderr = "";
16673
16679
  let failure = null;
16674
- let finished = false;
16680
+ let settled = false;
16681
+ let forceKillTimer = null;
16682
+ let finalRejectTimer = null;
16683
+ let timer;
16684
+ const signalProcessTree = (signal) => {
16685
+ if (process.platform !== "win32" && child.pid) {
16686
+ try {
16687
+ process.kill(-child.pid, signal);
16688
+ return;
16689
+ } catch {
16690
+ }
16691
+ }
16692
+ child.kill(signal);
16693
+ };
16694
+ const rejectOnce = (error) => {
16695
+ failure ??= error;
16696
+ if (settled) return;
16697
+ settled = true;
16698
+ clearTimeout(timer);
16699
+ if (forceKillTimer) clearTimeout(forceKillTimer);
16700
+ if (finalRejectTimer) clearTimeout(finalRejectTimer);
16701
+ reject(failure);
16702
+ };
16703
+ const terminate = (error) => {
16704
+ if (failure) return;
16705
+ failure = error;
16706
+ signalProcessTree("SIGTERM");
16707
+ forceKillTimer = setTimeout(() => {
16708
+ signalProcessTree("SIGKILL");
16709
+ rejectOnce(error);
16710
+ }, 1e3);
16711
+ finalRejectTimer = setTimeout(() => rejectOnce(error), 2e3);
16712
+ };
16675
16713
  const append = (current, chunk) => {
16676
16714
  const next = current + chunk.toString();
16677
16715
  if (Buffer.byteLength(next) > MAX_OUTPUT_BYTES && failure === null) {
16678
- failure = new Error(`OpenCode \u8F93\u51FA\u8D85\u8FC7 ${MAX_OUTPUT_BYTES} bytes`);
16679
- child.kill("SIGTERM");
16716
+ terminate(new Error(`OpenCode \u8F93\u51FA\u8D85\u8FC7 ${MAX_OUTPUT_BYTES} bytes`));
16680
16717
  }
16681
16718
  return next.slice(-MAX_OUTPUT_BYTES);
16682
16719
  };
@@ -16687,32 +16724,80 @@ function runOpenCode(executable, args, cwd, timeoutMs) {
16687
16724
  stderr = append(stderr, chunk);
16688
16725
  });
16689
16726
  child.once("error", (error) => {
16690
- failure ??= error;
16727
+ if (forceKillTimer) return;
16728
+ rejectOnce(error);
16691
16729
  });
16692
- const timer = setTimeout(() => {
16693
- failure ??= new Error(`OpenCode \u547D\u4EE4\u8D85\u8FC7 ${timeoutMs}ms \u672A\u5B8C\u6210`);
16694
- child.kill("SIGTERM");
16730
+ timer = setTimeout(() => {
16731
+ terminate(new Error(`OpenCode \u547D\u4EE4\u8D85\u8FC7 ${timeoutMs}ms \u672A\u5B8C\u6210`));
16695
16732
  }, timeoutMs);
16696
16733
  child.once("close", (code) => {
16697
- if (finished) return;
16698
- finished = true;
16699
16734
  clearTimeout(timer);
16735
+ if (forceKillTimer) return;
16736
+ if (finalRejectTimer) clearTimeout(finalRejectTimer);
16737
+ if (settled) return;
16738
+ settled = true;
16700
16739
  if (failure) {
16701
16740
  reject(failure);
16702
16741
  return;
16703
16742
  }
16704
16743
  if (code !== 0) {
16705
16744
  const detail = cleanOutput(stderr).trim() || `\u9000\u51FA\u7801 ${code ?? "null"}`;
16706
- reject(new Error(`OpenCode ${args.join(" ")} \u5931\u8D25\uFF1A${detail}`));
16745
+ reject(new OpenCodeCommandExitError(`OpenCode ${args.join(" ")} \u5931\u8D25\uFF1A${detail}`));
16707
16746
  return;
16708
16747
  }
16709
- resolve3(cleanOutput(stdout));
16748
+ resolve2(cleanOutput(stdout));
16710
16749
  });
16711
16750
  });
16712
16751
  }
16713
16752
  function parseOpenCodeModels(output) {
16714
16753
  return [...new Set(cleanOutput(output).split("\n").map((line) => line.trim()).filter((line) => /^[^\s/]+\/.+/.test(line)))].sort((left, right) => left.localeCompare(right));
16715
16754
  }
16755
+ function isRecord(value) {
16756
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16757
+ }
16758
+ function findJsonObjectEnd(value, start) {
16759
+ let depth = 0;
16760
+ let inString = false;
16761
+ let escaped = false;
16762
+ for (let index2 = start; index2 < value.length; index2 += 1) {
16763
+ const character = value[index2];
16764
+ if (inString) {
16765
+ if (escaped) escaped = false;
16766
+ else if (character === "\\") escaped = true;
16767
+ else if (character === '"') inString = false;
16768
+ continue;
16769
+ }
16770
+ if (character === '"') inString = true;
16771
+ else if (character === "{") depth += 1;
16772
+ else if (character === "}" && --depth === 0) return index2 + 1;
16773
+ }
16774
+ return null;
16775
+ }
16776
+ function parseOpenCodeModelMetadata(output) {
16777
+ const cleaned = cleanOutput(output);
16778
+ const models = /* @__PURE__ */ new Set();
16779
+ const variantsByModel = {};
16780
+ const headingPattern = /^([^\s/]+\/[^\r\n]+)\r?\n\s*(?=\{)/gm;
16781
+ for (const match2 of cleaned.matchAll(headingPattern)) {
16782
+ const model = match2[1].trim();
16783
+ models.add(model);
16784
+ let objectStart = (match2.index ?? 0) + match2[0].length;
16785
+ while (/\s/.test(cleaned[objectStart] ?? "")) objectStart += 1;
16786
+ if (cleaned[objectStart] !== "{") continue;
16787
+ const objectEnd = findJsonObjectEnd(cleaned, objectStart);
16788
+ if (objectEnd === null) continue;
16789
+ try {
16790
+ const metadata = JSON.parse(cleaned.slice(objectStart, objectEnd));
16791
+ if (!isRecord(metadata) || !isRecord(metadata.variants)) continue;
16792
+ variantsByModel[model] = Object.keys(metadata.variants).filter((variant) => variant.trim().length > 0).sort((left, right) => left.localeCompare(right));
16793
+ } catch {
16794
+ }
16795
+ }
16796
+ return {
16797
+ models: [...models].sort((left, right) => left.localeCompare(right)),
16798
+ variantsByModel
16799
+ };
16800
+ }
16716
16801
  function parseOpenCodeAgents(output) {
16717
16802
  const agents = /* @__PURE__ */ new Map();
16718
16803
  for (const line of cleanOutput(output).split("\n")) {
@@ -16731,19 +16816,23 @@ async function loadOpenCodeCatalog(cwd, options = {}) {
16731
16816
  const executable = options.executable ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
16732
16817
  const timeoutMs = options.timeoutMs ?? COMMAND_TIMEOUT_MS;
16733
16818
  const cacheKey = `${executable}\0${cwd}`;
16734
- const cached = catalogCache.get(cacheKey);
16735
- if (options.useCache !== false && cached && cached.expiresAt > Date.now()) {
16736
- return cached.result;
16819
+ const cached2 = catalogCache.get(cacheKey);
16820
+ if (options.useCache !== false && cached2 && cached2.expiresAt > Date.now()) {
16821
+ return cached2.result;
16737
16822
  }
16738
16823
  const result = Promise.all([
16739
- runOpenCode(executable, ["models"], cwd, timeoutMs),
16824
+ runOpenCode(executable, ["models", "--verbose"], cwd, timeoutMs).catch((error) => {
16825
+ if (!(error instanceof OpenCodeCommandExitError)) throw error;
16826
+ return runOpenCode(executable, ["models"], cwd, timeoutMs);
16827
+ }),
16740
16828
  runOpenCode(executable, ["agent", "list"], cwd, timeoutMs)
16741
16829
  ]).then(([modelsOutput, agentsOutput]) => {
16742
- const models = parseOpenCodeModels(modelsOutput);
16830
+ const metadata = parseOpenCodeModelMetadata(modelsOutput);
16831
+ const models = metadata.models.length > 0 ? metadata.models : parseOpenCodeModels(modelsOutput);
16743
16832
  const agents = parseOpenCodeAgents(agentsOutput).filter((agent) => agent.mode !== "subagent");
16744
16833
  if (models.length === 0) throw new Error("OpenCode \u6CA1\u6709\u8FD4\u56DE\u53EF\u7528\u6A21\u578B");
16745
16834
  if (agents.length === 0) throw new Error("OpenCode \u6CA1\u6709\u8FD4\u56DE\u53EF\u76F4\u63A5\u8FD0\u884C\u7684\u4E3B Agent");
16746
- return { cwd, models, agents };
16835
+ return { cwd, models, variantsByModel: metadata.variantsByModel, agents };
16747
16836
  });
16748
16837
  if (options.useCache !== false) {
16749
16838
  catalogCache.set(cacheKey, { expiresAt: Date.now() + CATALOG_CACHE_MS, result });
@@ -16770,6 +16859,59 @@ import { tmpdir } from "os";
16770
16859
  import { basename, dirname as dirname2, resolve } from "path";
16771
16860
 
16772
16861
  // src/core/process-control.ts
16862
+ var OS_COMMAND_TIMEOUT_MS = 2e3;
16863
+ var OS_COMMAND_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
16864
+ var OS_COMMAND_RESULT_MARKER = "\n\0supertask-os-command-result:";
16865
+ var BOUNDED_OS_COMMAND_RUNNER = `
16866
+ const { writeSync } = await import('fs');
16867
+ let child;
16868
+ try {
16869
+ child = Bun.spawn(process.argv.slice(2), {
16870
+ stdin: 'ignore',
16871
+ stdout: 'pipe',
16872
+ stderr: 'ignore',
16873
+ });
16874
+ } catch {
16875
+ process.exit(127);
16876
+ }
16877
+ const terminateGroup = () => {
16878
+ if (process.platform !== 'win32') {
16879
+ try {
16880
+ process.kill(-process.pid, 'SIGKILL');
16881
+ } catch {
16882
+ }
16883
+ }
16884
+ try {
16885
+ child.kill('SIGKILL');
16886
+ } catch {
16887
+ }
16888
+ process.exit(1);
16889
+ };
16890
+ const timer = setTimeout(terminateGroup, ${OS_COMMAND_TIMEOUT_MS});
16891
+ try {
16892
+ const chunks = [];
16893
+ let outputBytes = 0;
16894
+ const readOutput = async () => {
16895
+ const reader = child.stdout.getReader();
16896
+ while (true) {
16897
+ const { done, value } = await reader.read();
16898
+ if (done) return;
16899
+ outputBytes += value.byteLength;
16900
+ if (outputBytes > ${OS_COMMAND_MAX_OUTPUT_BYTES}) terminateGroup();
16901
+ chunks.push(value);
16902
+ }
16903
+ };
16904
+ const [exitCode] = await Promise.all([child.exited, readOutput()]);
16905
+ clearTimeout(timer);
16906
+ for (const chunk of chunks) writeSync(1, chunk);
16907
+ writeSync(1, ${JSON.stringify(OS_COMMAND_RESULT_MARKER)} + String(exitCode));
16908
+ if (process.platform !== 'win32') terminateGroup();
16909
+ process.exit(0);
16910
+ } catch {
16911
+ clearTimeout(timer);
16912
+ terminateGroup();
16913
+ }
16914
+ `;
16773
16915
  function isProcessAlive(pid) {
16774
16916
  if (!Number.isInteger(pid) || pid <= 0) return false;
16775
16917
  try {
@@ -17336,7 +17478,7 @@ var TaskRunService = class {
17336
17478
  }
17337
17479
  if (current.childPid != null) {
17338
17480
  throw new LegacyRunAbandonConflictError(
17339
- `run #${runId} \u5DF2\u8BB0\u5F55 child PID ${current.childPid}\uFF0C\u5FC5\u987B\u7531 Worker/Watchdog \u786E\u8BA4\u8FDB\u7A0B\u6811\u9000\u51FA`
17481
+ `run #${runId} \u5DF2\u8BB0\u5F55 child PID ${current.childPid}\uFF0C\u5FC5\u987B\u7531 Worker/Watchdog \u786E\u8BA4\u53D7\u7BA1\u8FDB\u7A0B\u7EC4\u6392\u7A7A`
17340
17482
  );
17341
17483
  }
17342
17484
  if (current.workerPid != null && isProcessAlive(current.workerPid)) {
@@ -17394,6 +17536,22 @@ function normalizeTaskBatchId(batchId) {
17394
17536
  return batchId.trim() || null;
17395
17537
  }
17396
17538
 
17539
+ // src/core/model-variant.ts
17540
+ var MAX_MODEL_VARIANT_LENGTH = 128;
17541
+ var CONTROL_CHARACTER_PATTERN = /[\u0000-\u001F\u007F]/;
17542
+ function normalizeModelVariant(value) {
17543
+ if (value === void 0 || value === null) return value;
17544
+ const normalized = value.trim();
17545
+ if (!normalized) return null;
17546
+ if (normalized.length > MAX_MODEL_VARIANT_LENGTH) {
17547
+ throw new Error(`variant \u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 ${MAX_MODEL_VARIANT_LENGTH} \u4E2A\u5B57\u7B26`);
17548
+ }
17549
+ if (CONTROL_CHARACTER_PATTERN.test(normalized)) {
17550
+ throw new Error("variant \u4E0D\u80FD\u5305\u542B\u63A7\u5236\u5B57\u7B26");
17551
+ }
17552
+ return normalized;
17553
+ }
17554
+
17397
17555
  // src/core/services/task.service.ts
17398
17556
  var { tasks: tasks3, taskRuns: taskRuns3 } = schema_exports;
17399
17557
  var cleanupInvocation = 0;
@@ -17459,7 +17617,8 @@ var TaskService = class {
17459
17617
  static async add(data) {
17460
17618
  const normalizedData = {
17461
17619
  ...data,
17462
- batchId: normalizeTaskBatchId(data.batchId)
17620
+ batchId: normalizeTaskBatchId(data.batchId),
17621
+ variant: normalizeModelVariant(data.variant)
17463
17622
  };
17464
17623
  this.validateNewTask(normalizedData);
17465
17624
  return db.transaction((tx) => {
@@ -17487,7 +17646,11 @@ var TaskService = class {
17487
17646
  }
17488
17647
  static async update(id, data, scope = {}) {
17489
17648
  if (Object.keys(data).length === 0) throw new Error("\u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A\u8981\u4FEE\u6539\u7684\u5B57\u6BB5");
17490
- const normalizedData = data.batchId === void 0 ? data : { ...data, batchId: normalizeTaskBatchId(data.batchId) ?? null };
17649
+ const normalizedData = {
17650
+ ...data,
17651
+ ...data.batchId === void 0 ? {} : { batchId: normalizeTaskBatchId(data.batchId) ?? null },
17652
+ ...data.variant === void 0 ? {} : { variant: normalizeModelVariant(data.variant) ?? null }
17653
+ };
17491
17654
  return db.transaction((tx) => {
17492
17655
  const task = tx.select().from(tasks3).where(and(
17493
17656
  eq(tasks3.id, id),
@@ -17499,6 +17662,7 @@ var TaskService = class {
17499
17662
  name: normalizedData.name ?? task.name,
17500
17663
  agent: normalizedData.agent ?? task.agent,
17501
17664
  model: normalizedData.model ?? task.model,
17665
+ variant: normalizedData.variant === void 0 ? task.variant : normalizedData.variant,
17502
17666
  prompt: normalizedData.prompt ?? task.prompt,
17503
17667
  cwd: task.cwd,
17504
17668
  category: normalizedData.category ?? task.category,
@@ -17512,13 +17676,23 @@ var TaskService = class {
17512
17676
  });
17513
17677
  const maxRetries = normalizedData.maxRetries ?? task.maxRetries ?? 3;
17514
17678
  const exhausted = task.status === "failed" && (task.retryCount ?? 0) > maxRetries;
17515
- return tx.update(tasks3).set({
17679
+ const updated = tx.update(tasks3).set({
17516
17680
  ...normalizedData,
17517
17681
  ...exhausted ? {
17518
17682
  status: "dead_letter",
17519
17683
  retryAfter: null
17520
17684
  } : {}
17521
17685
  }).where(eq(tasks3.id, id)).returning().get() ?? null;
17686
+ if (exhausted && updated) {
17687
+ const finishedAt = /* @__PURE__ */ new Date();
17688
+ tx.update(tasks3).set({
17689
+ status: "dead_letter",
17690
+ finishedAt,
17691
+ retryAfter: null,
17692
+ resultLog: `\u4F9D\u8D56\u4EFB\u52A1 #${id} \u5DF2\u8FDB\u5165\u4E0D\u53EF\u6062\u590D\u7EC8\u6001`
17693
+ }).where(blockedDependentsOf(id)).run();
17694
+ }
17695
+ return updated;
17522
17696
  }, { behavior: "immediate" });
17523
17697
  }
17524
17698
  static validateNewTask(data) {
@@ -17539,7 +17713,7 @@ var TaskService = class {
17539
17713
  throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
17540
17714
  }
17541
17715
  }
17542
- static async next(scope = {}) {
17716
+ static buildRunnableTaskWhere(scope) {
17543
17717
  const baseConditions = [...this.buildScopeWhere(scope)];
17544
17718
  const nowMs = Date.now();
17545
17719
  const retryAfterFilter = or(
@@ -17574,40 +17748,43 @@ var TaskService = class {
17574
17748
  if (batchFilter) {
17575
17749
  conditions.push(batchFilter);
17576
17750
  }
17577
- const result = await db.select().from(tasks3).where(and(
17751
+ return and(
17578
17752
  ...conditions,
17579
17753
  or(
17580
17754
  isNull(tasks3.dependsOn),
17581
17755
  sql`EXISTS (
17582
- SELECT 1 FROM tasks AS dependency_task
17583
- WHERE dependency_task.id = ${tasks3.dependsOn}
17584
- AND dependency_task.status = 'done'
17585
- AND dependency_task.cwd IS ${tasks3.cwd}
17586
- )`
17756
+ SELECT 1 FROM tasks AS dependency_task
17757
+ WHERE dependency_task.id = ${tasks3.dependsOn}
17758
+ AND dependency_task.status = 'done'
17759
+ AND dependency_task.cwd IS ${tasks3.cwd}
17760
+ )`
17587
17761
  ),
17588
17762
  or(
17589
17763
  isNull(tasks3.batchId),
17590
17764
  sql`trim(${tasks3.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
17591
17765
  sql`NOT EXISTS (
17592
- SELECT 1 FROM tasks AS running_batch_task
17593
- WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
17594
- = trim(${tasks3.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
17595
- AND (
17596
- running_batch_task.status = 'running'
17597
- OR EXISTS (
17598
- SELECT 1 FROM task_runs AS running_batch_run
17599
- WHERE running_batch_run.task_id = running_batch_task.id
17600
- AND running_batch_run.status = 'running'
17601
- )
17766
+ SELECT 1 FROM tasks AS running_batch_task
17767
+ WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
17768
+ = trim(${tasks3.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
17769
+ AND (
17770
+ running_batch_task.status = 'running'
17771
+ OR EXISTS (
17772
+ SELECT 1 FROM task_runs AS running_batch_run
17773
+ WHERE running_batch_run.task_id = running_batch_task.id
17774
+ AND running_batch_run.status = 'running'
17602
17775
  )
17603
- )`
17776
+ )
17777
+ )`
17604
17778
  ),
17605
17779
  sql`NOT EXISTS (
17606
- SELECT 1 FROM task_runs AS candidate_active_run
17607
- WHERE candidate_active_run.task_id = ${tasks3.id}
17608
- AND candidate_active_run.status = 'running'
17609
- )`
17610
- )).orderBy(
17780
+ SELECT 1 FROM task_runs AS candidate_active_run
17781
+ WHERE candidate_active_run.task_id = ${tasks3.id}
17782
+ AND candidate_active_run.status = 'running'
17783
+ )`
17784
+ );
17785
+ }
17786
+ static async next(scope = {}) {
17787
+ const result = await db.select().from(tasks3).where(this.buildRunnableTaskWhere(scope)).orderBy(
17611
17788
  desc(tasks3.urgency),
17612
17789
  desc(tasks3.importance),
17613
17790
  asc(tasks3.createdAt),
@@ -17615,6 +17792,22 @@ var TaskService = class {
17615
17792
  ).limit(1);
17616
17793
  return result[0] ?? null;
17617
17794
  }
17795
+ static async claimNext(scope = {}) {
17796
+ return db.transaction((tx) => {
17797
+ const candidate = tx.select().from(tasks3).where(this.buildRunnableTaskWhere(scope)).orderBy(
17798
+ desc(tasks3.urgency),
17799
+ desc(tasks3.importance),
17800
+ asc(tasks3.createdAt),
17801
+ asc(tasks3.id)
17802
+ ).limit(1).get();
17803
+ if (!candidate) return null;
17804
+ return tx.update(tasks3).set({
17805
+ status: "running",
17806
+ startedAt: /* @__PURE__ */ new Date(),
17807
+ finishedAt: null
17808
+ }).where(eq(tasks3.id, candidate.id)).returning().get() ?? null;
17809
+ }, { behavior: "immediate" });
17810
+ }
17618
17811
  static async countRunning(scope = {}) {
17619
17812
  const scopeConditions = scope.legacyCwd ? [sql`${tasks3.cwd} IS NULL OR trim(${tasks3.cwd}) = ''`] : this.buildScopeWhere(scope);
17620
17813
  if (scope.batchId !== void 0) {
@@ -18273,7 +18466,8 @@ var TaskTemplateService = class {
18273
18466
  static async create(data) {
18274
18467
  const normalizedData = {
18275
18468
  ...data,
18276
- batchId: normalizeTaskBatchId(data.batchId)
18469
+ batchId: normalizeTaskBatchId(data.batchId),
18470
+ variant: normalizeModelVariant(data.variant)
18277
18471
  };
18278
18472
  this.validate(normalizedData);
18279
18473
  const now = Date.now();
@@ -18339,7 +18533,8 @@ var TaskTemplateService = class {
18339
18533
  static async update(id, data) {
18340
18534
  const normalizedData = {
18341
18535
  ...data,
18342
- batchId: normalizeTaskBatchId(data.batchId) ?? null
18536
+ batchId: normalizeTaskBatchId(data.batchId) ?? null,
18537
+ ...data.variant === void 0 ? {} : { variant: normalizeModelVariant(data.variant) ?? null }
18343
18538
  };
18344
18539
  this.validate(normalizedData);
18345
18540
  const now = Date.now();
@@ -18680,6 +18875,7 @@ function createTaskFromTemplate(templateId, options) {
18680
18875
  name: `${options.namePrefix ?? ""}${tmpl.name}`,
18681
18876
  agent: tmpl.agent,
18682
18877
  model: tmpl.model ?? "default",
18878
+ variant: tmpl.variant,
18683
18879
  prompt: tmpl.prompt,
18684
18880
  cwd: tmpl.cwd ?? null,
18685
18881
  category: tmpl.category ?? "general",
@@ -18704,452 +18900,101 @@ function createTaskFromTemplate(templateId, options) {
18704
18900
  }, { behavior: "immediate" });
18705
18901
  }
18706
18902
 
18707
- // src/daemon/pm2.ts
18708
- import { execSync, spawnSync } from "child_process";
18709
- import {
18710
- accessSync,
18711
- chmodSync as chmodSync2,
18712
- constants,
18713
- existsSync as existsSync4,
18714
- mkdirSync as mkdirSync3,
18715
- readFileSync as readFileSync2,
18716
- rmSync,
18717
- statSync as statSync3,
18718
- writeFileSync as writeFileSync2
18719
- } from "fs";
18720
- import { homedir as homedir3, userInfo } from "os";
18721
- import { delimiter, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "path";
18903
+ // src/web/gateway-diagnostic.ts
18904
+ import { spawn as spawn2 } from "child_process";
18905
+ import { existsSync as existsSync4 } from "fs";
18906
+ import { dirname as dirname3, join as join3 } from "path";
18722
18907
  import { fileURLToPath as fileURLToPath2 } from "url";
18723
- import { Database as Database5 } from "bun:sqlite";
18724
-
18725
- // src/daemon/management-lock.ts
18726
- import { Database as Database4 } from "bun:sqlite";
18727
-
18728
- // src/daemon/pm2.ts
18729
- var __dirname = dirname3(fileURLToPath2(import.meta.url));
18730
- var PROCESS_NAME = "supertask-gateway";
18731
- var MAC_LAUNCH_AGENT_LABEL = "com.supertask.pm2-resurrect";
18732
- var GATEWAY_LOCK_STALE_MS = 3e4;
18733
- function runtimeHome(env) {
18734
- return resolve2(env.HOME || homedir3());
18735
- }
18736
- function runtimePath(value, cwd) {
18737
- return resolve2(cwd, value);
18738
- }
18739
- function versionFile(env = process.env, cwd = process.cwd()) {
18740
- return env.SUPERTASK_VERSION_FILE ? runtimePath(env.SUPERTASK_VERSION_FILE, cwd) : join3(runtimeHome(env), ".local/share/opencode/supertask-gateway-version");
18741
- }
18742
- function getRunningVersion(env = process.env, cwd = process.cwd()) {
18743
- try {
18744
- const path = versionFile(env, cwd);
18745
- if (!existsSync4(path)) return null;
18746
- return readFileSync2(path, "utf-8").trim() || null;
18747
- } catch {
18748
- return null;
18749
- }
18750
- }
18751
- function packageVersionFromGatewayEntry(gatewayEntry) {
18752
- if (gatewayEntry === null) return null;
18753
- let directory = dirname3(gatewayEntry);
18754
- for (let depth = 0; depth < 6; depth += 1) {
18755
- const packagePath = join3(directory, "package.json");
18756
- if (existsSync4(packagePath)) {
18757
- try {
18758
- const pkg = JSON.parse(readFileSync2(packagePath, "utf8"));
18759
- if (pkg.name === "opencode-supertask" && typeof pkg.version === "string") {
18760
- return pkg.version;
18761
- }
18762
- } catch {
18763
- }
18764
- }
18765
- const parent = dirname3(directory);
18766
- if (parent === directory) break;
18767
- directory = parent;
18768
- }
18769
- return null;
18770
- }
18771
- function resolveRuntimeExecutable(command, env, cwd) {
18772
- if (isAbsolute2(command) || command.includes("/")) return runtimePath(command, cwd);
18773
- for (const entry of (env.PATH ?? "").split(delimiter)) {
18774
- if (!entry) continue;
18775
- const candidate = resolve2(cwd, entry, command);
18908
+ var CACHE_TTL_MS = 5e3;
18909
+ var DIAGNOSTIC_TIMEOUT_MS = 2e4;
18910
+ var cached = null;
18911
+ var pending = null;
18912
+ var activeProcessGroups = /* @__PURE__ */ new Set();
18913
+ function killDiagnosticGroup(pid) {
18914
+ if (process.platform !== "win32") {
18776
18915
  try {
18777
- accessSync(candidate, constants.X_OK);
18778
- return candidate;
18916
+ process.kill(-pid, "SIGKILL");
18917
+ return;
18779
18918
  } catch {
18780
18919
  }
18781
18920
  }
18782
- return command;
18783
- }
18784
- function runtimeScope(runtime) {
18785
- const { cwd, env } = runtime;
18786
- const home = runtimeHome(env);
18787
- return {
18788
- cwd: resolve2(cwd),
18789
- databasePath: env.SUPERTASK_DB_PATH ? runtimePath(env.SUPERTASK_DB_PATH, cwd) : join3(home, ".local/share/opencode/tasks.db"),
18790
- configPath: env.SUPERTASK_CONFIG_PATH ? runtimePath(env.SUPERTASK_CONFIG_PATH, cwd) : join3(home, ".config/opencode/supertask.json"),
18791
- opencodePath: resolveRuntimeExecutable(env.SUPERTASK_OPENCODE_BIN ?? "opencode", env, cwd),
18792
- home,
18793
- pm2Home: env.PM2_HOME ? runtimePath(env.PM2_HOME, cwd) : join3(home, ".pm2"),
18794
- managementLockPath: canonicalManagementLockPath(env, cwd)
18795
- };
18796
- }
18797
- function scopesMatch(left, right) {
18798
- return left.databasePath === right.databasePath && left.configPath === right.configPath && left.opencodePath === right.opencodePath && left.home === right.home && left.pm2Home === right.pm2Home && left.managementLockPath === right.managementLockPath;
18799
- }
18800
- function currentScope() {
18801
- return runtimeScope({ cwd: process.cwd(), env: process.env });
18802
- }
18803
- function diagnoseOpenCodeRuntime(runtime = {
18804
- cwd: process.cwd(),
18805
- env: process.env
18806
- }) {
18807
- const executable = runtimeScope(runtime).opencodePath;
18808
- const result = spawnSync(executable, ["--version"], {
18809
- cwd: runtime.cwd,
18810
- env: runtime.env,
18811
- encoding: "utf8",
18812
- timeout: 1e4,
18813
- killSignal: "SIGKILL"
18814
- });
18815
- const ok = result.status === 0 && result.error === void 0;
18816
- return {
18817
- ok,
18818
- executable,
18819
- version: ok ? result.stdout.trim() || result.stderr.trim() || null : null,
18820
- error: ok ? null : result.error?.message || result.stderr.trim() || result.stdout.trim() || `exit code ${result.status}`
18821
- };
18822
- }
18823
- function getGatewayDiagnostic(options = {}) {
18824
- const producerScope = currentScope();
18825
- if (!isPm2Installed()) {
18826
- return {
18827
- pm2Installed: false,
18828
- processFound: false,
18829
- status: null,
18830
- pid: null,
18831
- ready: false,
18832
- runningVersion: getRunningVersion(),
18833
- gatewayEntry: null,
18834
- gatewayPackageVersion: null,
18835
- logRotationInstalled: false,
18836
- startupConfigured: process.platform === "darwin" || process.platform === "linux" ? false : null,
18837
- currentScope: producerScope,
18838
- gatewayScope: null,
18839
- scopeMatches: false,
18840
- gatewayOpenCode: null
18841
- };
18842
- }
18843
- const processes = pm2JsonList();
18844
- const gateway = processes.find((item) => item.name === PROCESS_NAME);
18845
- const runtime = gatewayRuntimeFromProcess(gateway);
18846
- const gatewayEnv = runtime?.env ?? process.env;
18847
- const managedScope = runtime ? runtimeScope(runtime) : null;
18848
- const pid = typeof gateway?.pid === "number" ? gateway.pid : null;
18849
- const readyPath = managedScope?.databasePath ?? databasePath();
18850
- const lockedVersion = pid == null ? void 0 : gatewayVersionFromLock(pid, readyPath);
18851
- const gatewayEntry = runtime?.gatewayEntry ?? null;
18852
- return {
18853
- pm2Installed: true,
18854
- processFound: gateway != null,
18855
- status: gateway?.pm2_env?.status ?? null,
18856
- pid,
18857
- ready: pid != null && isGatewayReady(pid, readyPath),
18858
- runningVersion: lockedVersion === void 0 ? getRunningVersion(gatewayEnv, runtime?.cwd) : lockedVersion,
18859
- gatewayEntry,
18860
- gatewayPackageVersion: packageVersionFromGatewayEntry(gatewayEntry),
18861
- logRotationInstalled: processes.some((item) => item.name === "pm2-logrotate" && item.pm2_env?.status === "online"),
18862
- startupConfigured: process.platform === "darwin" ? isMacLaunchAgentConfigured(
18863
- gatewayEnv.PM2_HOME ?? join3(runtimeHome(gatewayEnv), ".pm2"),
18864
- runtime ?? void 0
18865
- ) : process.platform === "linux" ? isLinuxStartupConfigured(gatewayEnv, runtime?.cwd, runtime ?? void 0) : null,
18866
- currentScope: producerScope,
18867
- gatewayScope: managedScope,
18868
- scopeMatches: managedScope != null && scopesMatch(producerScope, managedScope),
18869
- gatewayOpenCode: runtime === null || !options.probeOpenCode ? null : diagnoseOpenCodeRuntime(runtime)
18870
- };
18871
- }
18872
- function pm2Bin(env = process.env) {
18873
- return env.SUPERTASK_PM2_BIN ?? (process.platform === "win32" ? "pm2.cmd" : "pm2");
18874
- }
18875
- function pm2CommandTimeoutMs(env = process.env) {
18876
- const configured = Number(env.SUPERTASK_PM2_COMMAND_TIMEOUT_MS ?? 15e3);
18877
- return Number.isFinite(configured) && configured > 0 ? configured : 15e3;
18878
- }
18879
- function resolvePm2Bin() {
18880
- const configured = pm2Bin();
18881
- if (isAbsolute2(configured)) return configured;
18882
- const result = spawnSync("which", [configured], { encoding: "utf8" });
18883
- const resolved = result.status === 0 ? result.stdout.trim().split("\n")[0] : "";
18884
- if (!resolved) throw new Error(`[supertask] \u65E0\u6CD5\u89E3\u6790 pm2 \u53EF\u6267\u884C\u6587\u4EF6: ${configured}`);
18885
- return resolved;
18886
- }
18887
- function launchAgentPath() {
18888
- return process.env.SUPERTASK_LAUNCH_AGENT_PATH ?? join3(runtimeHome(process.env), "Library/LaunchAgents", `${MAC_LAUNCH_AGENT_LABEL}.plist`);
18889
- }
18890
- function launchctlBin() {
18891
- return process.env.SUPERTASK_LAUNCHCTL_BIN ?? "launchctl";
18892
- }
18893
- function xmlUnescape(value) {
18894
- return value.replaceAll("&apos;", "'").replaceAll("&quot;", '"').replaceAll("&gt;", ">").replaceAll("&lt;", "<").replaceAll("&amp;", "&");
18895
- }
18896
- function plistValue(contents, key) {
18897
- const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18898
- const match2 = contents.match(new RegExp(`<key>\\s*${escapedKey}\\s*</key>\\s*<string>([^<]*)</string>`));
18899
- return match2?.[1] ? xmlUnescape(match2[1]) : null;
18900
- }
18901
- function isMacLaunchAgentConfigured(expectedPm2Home, expectedRuntime) {
18902
- if (typeof process.getuid !== "function") return false;
18903
- const plistPath = launchAgentPath();
18904
- if (!existsSync4(plistPath)) return false;
18905
18921
  try {
18906
- const contents = readFileSync2(plistPath, "utf8");
18907
- const argumentsBlock = contents.match(
18908
- /<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/
18909
- )?.[1];
18910
- const programArguments = argumentsBlock ? [...argumentsBlock.matchAll(/<string>([^<]*)<\/string>/g)].map((match2) => xmlUnescape(match2[1])) : [];
18911
- const bunPath = programArguments[0];
18912
- const supervisorEntry = programArguments[1];
18913
- const pm2Path = programArguments[2];
18914
- const pm2Home = plistValue(contents, "PM2_HOME");
18915
- const managementLock = plistValue(contents, "SUPERTASK_PM2_MANAGEMENT_LOCK");
18916
- if (!bunPath || !supervisorEntry || !pm2Path || !pm2Home || !managementLock) return false;
18917
- if (expectedPm2Home && pm2Home !== expectedPm2Home) return false;
18918
- const expectedManagementLock = expectedRuntime ? managementLockPath(expectedRuntime.env, expectedRuntime.cwd) : process.env.SUPERTASK_PM2_MANAGEMENT_LOCK ? managementLockPath() : join3(expectedPm2Home ?? currentScope().pm2Home, "supertask-gateway.manage.sqlite");
18919
- if (managementLock !== expectedManagementLock) return false;
18920
- accessSync(bunPath, constants.X_OK);
18921
- accessSync(supervisorEntry, constants.R_OK);
18922
- accessSync(pm2Path, constants.X_OK);
18923
- if (spawnSync(bunPath, ["--version"], {
18924
- stdio: "ignore",
18925
- timeout: pm2CommandTimeoutMs(),
18926
- killSignal: "SIGKILL"
18927
- }).status !== 0) return false;
18928
- if (spawnSync(pm2Path, ["--version"], {
18929
- stdio: "ignore",
18930
- env: { ...process.env, PM2_HOME: pm2Home },
18931
- timeout: pm2CommandTimeoutMs(),
18932
- killSignal: "SIGKILL"
18933
- }).status !== 0) return false;
18934
- const loaded = spawnSync(
18935
- launchctlBin(),
18936
- ["print", `gui/${process.getuid()}/${MAC_LAUNCH_AGENT_LABEL}`],
18937
- {
18938
- encoding: "utf8",
18939
- stdio: ["ignore", "pipe", "ignore"],
18940
- timeout: pm2CommandTimeoutMs(),
18941
- killSignal: "SIGKILL"
18942
- }
18943
- );
18944
- if (loaded.status !== 0) return false;
18945
- if (!loaded.stdout.includes("state = running")) return false;
18946
- if (!loaded.stdout.includes(`path = ${plistPath}`)) return false;
18947
- if (!loaded.stdout.includes(`program = ${bunPath}`)) return false;
18948
- const dumpPath = join3(pm2Home, "dump.pm2");
18949
- const dump = JSON.parse(readFileSync2(dumpPath, "utf8"));
18950
- if (!Array.isArray(dump)) return false;
18951
- const gateway = dump.find((item) => item.name === PROCESS_NAME);
18952
- const dumpRuntime = gatewayRuntimeFromProcess(gateway);
18953
- if (!dumpRuntime || !hasRestorableSavedGatewayRuntime(gateway)) return false;
18954
- return expectedRuntime === void 0 || dumpRuntime.gatewayEntry === expectedRuntime.gatewayEntry && dumpRuntime.bunPath === expectedRuntime.bunPath && dumpRuntime.cwd === expectedRuntime.cwd && scopesMatch(runtimeScope(dumpRuntime), runtimeScope(expectedRuntime));
18922
+ process.kill(pid, "SIGKILL");
18955
18923
  } catch {
18956
- return false;
18957
18924
  }
18958
18925
  }
18959
- function systemctlBin(env = process.env) {
18960
- return env.SUPERTASK_SYSTEMCTL_BIN ?? "systemctl";
18926
+ process.once("exit", () => {
18927
+ for (const pid of activeProcessGroups) killDiagnosticGroup(pid);
18928
+ });
18929
+ function diagnosticEntry() {
18930
+ const extension = import.meta.url.endsWith(".ts") ? "ts" : "js";
18931
+ const moduleDir = dirname3(fileURLToPath2(import.meta.url));
18932
+ const candidates = [
18933
+ join3(moduleDir, `../daemon/gateway-diagnostic-runner.${extension}`),
18934
+ join3(moduleDir, `../../src/daemon/gateway-diagnostic-runner.${extension}`)
18935
+ ];
18936
+ const entry = candidates.find((candidate) => existsSync4(candidate));
18937
+ if (!entry) throw new Error(`Gateway diagnostic runner \u4E0D\u5B58\u5728\uFF1A${candidates.join(", ")}`);
18938
+ return entry;
18961
18939
  }
18962
- function isLinuxStartupConfigured(env = process.env, cwd = process.cwd(), expectedRuntime) {
18963
- const unit = env.SUPERTASK_PM2_SYSTEMD_UNIT ?? `pm2-${userInfo().username}.service`;
18964
- const enabled = spawnSync(systemctlBin(env), ["is-enabled", unit], {
18965
- encoding: "utf8",
18966
- env,
18967
- stdio: ["ignore", "pipe", "ignore"],
18968
- timeout: pm2CommandTimeoutMs(env),
18969
- killSignal: "SIGKILL"
18970
- });
18971
- if (enabled.status !== 0 || enabled.stdout.trim() !== "enabled") return false;
18972
- const contents = spawnSync(systemctlBin(env), ["cat", unit], {
18973
- encoding: "utf8",
18974
- env,
18975
- stdio: ["ignore", "pipe", "ignore"],
18976
- timeout: pm2CommandTimeoutMs(env),
18977
- killSignal: "SIGKILL"
18940
+ async function runGatewayDiagnostic() {
18941
+ return new Promise((resolve2, reject) => {
18942
+ const child = spawn2(process.execPath, [diagnosticEntry()], {
18943
+ cwd: process.cwd(),
18944
+ env: process.env,
18945
+ detached: process.platform !== "win32",
18946
+ stdio: ["ignore", "pipe", "pipe"]
18947
+ });
18948
+ let stdout = "";
18949
+ let stderr = "";
18950
+ let timedOut = false;
18951
+ if (child.pid) activeProcessGroups.add(child.pid);
18952
+ child.stdout?.on("data", (chunk) => {
18953
+ stdout += chunk.toString();
18954
+ });
18955
+ child.stderr?.on("data", (chunk) => {
18956
+ stderr += chunk.toString();
18957
+ });
18958
+ child.once("error", reject);
18959
+ child.once("close", (exitCode) => {
18960
+ clearTimeout(timer);
18961
+ if (child.pid) {
18962
+ killDiagnosticGroup(child.pid);
18963
+ activeProcessGroups.delete(child.pid);
18964
+ }
18965
+ if (timedOut) {
18966
+ reject(new Error(`Gateway diagnostic runner \u8D85\u8FC7 ${DIAGNOSTIC_TIMEOUT_MS}ms \u672A\u5B8C\u6210`));
18967
+ return;
18968
+ }
18969
+ if (exitCode !== 0) {
18970
+ reject(new Error(stderr.trim() || `Gateway diagnostic runner \u9000\u51FA\u7801 ${exitCode}`));
18971
+ return;
18972
+ }
18973
+ try {
18974
+ resolve2(JSON.parse(stdout));
18975
+ } catch (error) {
18976
+ reject(error);
18977
+ }
18978
+ });
18979
+ const timer = setTimeout(() => {
18980
+ timedOut = true;
18981
+ if (child.pid) killDiagnosticGroup(child.pid);
18982
+ else child.kill("SIGKILL");
18983
+ }, DIAGNOSTIC_TIMEOUT_MS);
18978
18984
  });
18979
- if (contents.status !== 0) return false;
18980
- const expectedPm2Home = env.PM2_HOME ?? join3(runtimeHome(env), ".pm2");
18981
- if (!contents.stdout.includes("resurrect") || !contents.stdout.includes(expectedPm2Home)) {
18982
- return false;
18983
- }
18984
- try {
18985
- const dump = JSON.parse(readFileSync2(join3(expectedPm2Home, "dump.pm2"), "utf8"));
18986
- if (!Array.isArray(dump)) return false;
18987
- const gateway = dump.find((item) => item.name === PROCESS_NAME);
18988
- const runtime = gatewayRuntimeFromProcess(gateway);
18989
- if (!runtime || !hasRestorableSavedGatewayRuntime(gateway)) return false;
18990
- return runtime.cwd === resolve2(cwd) && scopesMatch(runtimeScope(runtime), runtimeScope({ cwd, env })) && (expectedRuntime === void 0 || runtime.gatewayEntry === expectedRuntime.gatewayEntry && runtime.bunPath === expectedRuntime.bunPath);
18991
- } catch {
18992
- return false;
18993
- }
18994
18985
  }
18995
- function isPm2Installed(env = process.env) {
18996
- const result = spawnSync(pm2Bin(env), ["--version"], {
18997
- stdio: "ignore",
18998
- env,
18999
- shell: process.platform === "win32",
19000
- timeout: pm2CommandTimeoutMs(env),
19001
- killSignal: "SIGKILL"
18986
+ async function getDashboardGatewayDiagnostic(options = {}) {
18987
+ if (!options.fresh && cached && cached.expiresAt > Date.now()) return cached.diagnostic;
18988
+ if (!options.fresh && pending) return pending;
18989
+ const operation = runGatewayDiagnostic().then((diagnostic) => {
18990
+ cached = { expiresAt: Date.now() + CACHE_TTL_MS, diagnostic };
18991
+ return diagnostic;
19002
18992
  });
19003
- return result.status === 0;
19004
- }
19005
- function resolveCommand(command) {
19006
- const lookup = process.platform === "win32" ? "where" : "which";
19007
- const result = spawnSync(lookup, [command], { encoding: "utf8" });
19008
- return result.status === 0 ? result.stdout.trim().split("\n")[0] || null : null;
19009
- }
19010
- function npmPreferredEnvironment() {
19011
- if (process.platform === "win32") return null;
19012
- const npm = resolveCommand("npm");
19013
- const node = resolveCommand("node");
19014
- if (!npm || !node) return null;
19015
- const command = resolvePm2Bin();
19016
- const path = [.../* @__PURE__ */ new Set([
19017
- dirname3(command),
19018
- dirname3(npm),
19019
- dirname3(node),
19020
- "/usr/local/bin",
19021
- "/usr/bin",
19022
- "/bin"
19023
- ])].join(delimiter);
19024
- return { command, env: { ...process.env, PATH: path } };
19025
- }
19026
- function pm2Exec(args, options = {}) {
19027
- const npmEnvironment = options.preferNpm ? npmPreferredEnvironment() : null;
19028
- const effectiveEnv = options.env ?? npmEnvironment?.env ?? process.env;
19029
- const result = spawnSync(npmEnvironment?.command ?? pm2Bin(effectiveEnv), args, {
19030
- stdio: ["pipe", "pipe", "pipe"],
19031
- encoding: "utf-8",
19032
- env: effectiveEnv,
19033
- shell: process.platform === "win32",
19034
- timeout: options.timeoutMs ?? pm2CommandTimeoutMs(effectiveEnv),
19035
- killSignal: "SIGKILL"
18993
+ if (options.fresh) return operation;
18994
+ pending = operation.finally(() => {
18995
+ pending = null;
19036
18996
  });
19037
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
19038
- if (result.error) return { ok: false, output: result.error.message };
19039
- return { ok: result.status === 0, output };
19040
- }
19041
- function requirePm2(args, action, options = {}) {
19042
- const result = pm2Exec(args, options);
19043
- if (!result.ok) throw new Error(`[supertask] ${action} failed: ${result.output || "unknown pm2 error"}`);
19044
- return result.output;
19045
- }
19046
- function pm2JsonList(env = process.env) {
19047
- const output = requirePm2(["jlist"], "pm2 jlist", { env });
19048
- try {
19049
- const parsed = JSON.parse(output);
19050
- if (!Array.isArray(parsed)) throw new Error("result is not an array");
19051
- return parsed;
19052
- } catch (error) {
19053
- const message = error instanceof Error ? error.message : String(error);
19054
- throw new Error(`[supertask] Invalid pm2 jlist output: ${message}`);
19055
- }
19056
- }
19057
- function gatewayEntryFromProcess(processInfo) {
19058
- const args = processInfo?.pm2_env?.args ?? processInfo?.args;
19059
- const candidates = Array.isArray(args) ? [...args].reverse() : typeof args === "string" ? [args] : [];
19060
- const savedCwd = processInfo?.pm2_env?.pm_cwd ?? processInfo?.pm_cwd;
19061
- for (const candidate of candidates) {
19062
- const path = typeof savedCwd === "string" ? runtimePath(candidate, savedCwd) : candidate;
19063
- if (existsSync4(path)) return resolve2(path);
19064
- }
19065
- return null;
19066
- }
19067
- function gatewayEnvironmentFromProcess(processInfo) {
19068
- const saved = processInfo?.pm2_env?.env ?? processInfo?.env;
19069
- if (!saved) return { ...process.env };
19070
- const env = {};
19071
- for (const [key, value] of Object.entries(saved)) {
19072
- if (typeof value === "string") env[key] = value;
19073
- }
19074
- return env;
19075
- }
19076
- function gatewayRuntimeFromProcess(processInfo) {
19077
- const gatewayEntry = gatewayEntryFromProcess(processInfo);
19078
- if (!gatewayEntry) return null;
19079
- const savedBunPath = processInfo?.pm2_env?.pm_exec_path ?? processInfo?.pm_exec_path;
19080
- const savedCwd = processInfo?.pm2_env?.pm_cwd ?? processInfo?.pm_cwd;
19081
- const savedEnv = gatewayEnvironmentFromProcess(processInfo);
19082
- if (typeof savedBunPath !== "string" || typeof savedCwd !== "string") return null;
19083
- try {
19084
- accessSync(savedBunPath, constants.X_OK);
19085
- if (!statSync3(savedCwd).isDirectory()) return null;
19086
- if (spawnSync(savedBunPath, ["--version"], {
19087
- stdio: "ignore",
19088
- env: savedEnv,
19089
- timeout: pm2CommandTimeoutMs(savedEnv),
19090
- killSignal: "SIGKILL"
19091
- }).status !== 0) return null;
19092
- } catch {
19093
- return null;
19094
- }
19095
- const killTimeout = processInfo?.pm2_env?.kill_timeout ?? processInfo?.kill_timeout;
19096
- return {
19097
- gatewayEntry,
19098
- bunPath: savedBunPath,
19099
- cwd: resolve2(savedCwd),
19100
- env: savedEnv,
19101
- killTimeoutMs: Number.isInteger(killTimeout) && killTimeout >= 5e3 ? killTimeout : void 0
19102
- };
19103
- }
19104
- function hasRestorableSavedGatewayRuntime(processInfo) {
19105
- return gatewayRuntimeFromProcess(processInfo) !== null;
19106
- }
19107
- function databasePath(env = process.env, cwd = process.cwd()) {
19108
- return env.SUPERTASK_DB_PATH ? runtimePath(env.SUPERTASK_DB_PATH, cwd) : join3(runtimeHome(env), ".local/share/opencode/tasks.db");
19109
- }
19110
- function isGatewayReady(expectedPid, path = databasePath()) {
19111
- if (!existsSync4(path)) return false;
19112
- let database = null;
19113
- try {
19114
- database = new Database5(path, { readonly: true });
19115
- const row = database.query(
19116
- "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
19117
- ).get();
19118
- if (!row || row.ready_at == null) return false;
19119
- if (expectedPid !== void 0 && row.pid !== expectedPid) return false;
19120
- const ageMs = Date.now() - row.heartbeat_at;
19121
- return ageMs >= -5e3 && ageMs < GATEWAY_LOCK_STALE_MS;
19122
- } catch {
19123
- return false;
19124
- } finally {
19125
- database?.close();
19126
- }
19127
- }
19128
- function gatewayVersionFromLock(expectedPid, path) {
19129
- if (!existsSync4(path)) return void 0;
19130
- let database = null;
19131
- try {
19132
- database = new Database5(path, { readonly: true });
19133
- const row = database.query(
19134
- "SELECT pid, version FROM gateway_lock WHERE id = 1"
19135
- ).get();
19136
- if (!row || row.pid !== expectedPid) return void 0;
19137
- return row.version;
19138
- } catch {
19139
- return void 0;
19140
- } finally {
19141
- database?.close();
19142
- }
19143
- }
19144
- function managementLockPath(env = process.env, cwd = process.cwd()) {
19145
- const override = env.SUPERTASK_PM2_MANAGEMENT_LOCK;
19146
- if (override) return runtimePath(override, cwd);
19147
- return join3(runtimeScope({ env, cwd }).pm2Home, "supertask-gateway.manage.sqlite");
19148
- }
19149
- function canonicalManagementLockPath(env = process.env, cwd = process.cwd()) {
19150
- const home = runtimeHome(env);
19151
- const pm2Home = env.PM2_HOME ? runtimePath(env.PM2_HOME, cwd) : join3(home, ".pm2");
19152
- return join3(pm2Home, "supertask-gateway.manage.sqlite");
18997
+ return pending;
19153
18998
  }
19154
18999
 
19155
19000
  // src/web/ui.ts
@@ -19316,6 +19161,7 @@ var ZH = {
19316
19161
  "template.cwdHint": "OpenCode \u4F1A\u5728\u8FD9\u4E2A\u76EE\u5F55\u4E2D\u6267\u884C\u4EFB\u52A1\u3002",
19317
19162
  "template.agent": "Agent",
19318
19163
  "template.model": "\u6A21\u578B",
19164
+ "template.variant": "\u6A21\u578B Variant",
19319
19165
  "template.prompt": "\u63D0\u793A\u8BCD",
19320
19166
  "template.scheduleType": "\u6267\u884C\u65B9\u5F0F",
19321
19167
  "template.cronExpr": "Cron \u8868\u8FBE\u5F0F",
@@ -19353,9 +19199,11 @@ var ZH = {
19353
19199
  "catalog.chooseProject": "\u8BF7\u5148\u9009\u62E9\u9879\u76EE\u76EE\u5F55",
19354
19200
  "catalog.defaultModel": "\u8DDF\u968F Agent / OpenCode \u9ED8\u8BA4\u6A21\u578B",
19355
19201
  "catalog.defaultProvider": "\u9ED8\u8BA4\u6A21\u578B",
19202
+ "catalog.defaultVariant": "\u8DDF\u968F Agent / \u6A21\u578B\u9ED8\u8BA4\u8BBE\u7F6E",
19356
19203
  "catalog.provider": "\u6A21\u578B\u63D0\u4F9B\u5546",
19357
19204
  "catalog.model": "\u5177\u4F53\u6A21\u578B",
19358
- "catalog.modelHint": "\u5148\u9009\u63D0\u4F9B\u5546\uFF0C\u518D\u9009\u672C\u9879\u76EE opencode models \u8FD4\u56DE\u7684\u6A21\u578B\uFF1B\u9ED8\u8BA4\u9009\u9879\u4E0D\u4F1A\u4F20\u5165 -m\u3002",
19205
+ "catalog.modelHint": "\u5148\u9009\u63D0\u4F9B\u5546\uFF0C\u518D\u9009\u672C\u9879\u76EE opencode models --verbose \u8FD4\u56DE\u7684\u6A21\u578B\uFF1B\u9ED8\u8BA4\u9009\u9879\u4E0D\u4F1A\u4F20\u5165 -m\u3002",
19206
+ "catalog.variantHint": "\u4EC5\u663E\u793A\u6240\u9009\u6A21\u578B\u58F0\u660E\u652F\u6301\u7684 variants\uFF1B\u9ED8\u8BA4\u9009\u9879\u4E0D\u4F1A\u4F20\u5165 --variant\u3002",
19359
19207
  "catalog.agentHint": "\u6765\u81EA\u5F53\u524D\u9879\u76EE\u7684 opencode agent list\u3002",
19360
19208
  "catalog.loading": "\u6B63\u5728\u8BFB\u53D6\u6B64\u9879\u76EE\u53EF\u7528\u7684 Agent \u548C\u6A21\u578B\u2026",
19361
19209
  "catalog.loaded": "\u5DF2\u4ECE\u672C\u673A OpenCode \u8BFB\u53D6 {agents} \u4E2A Agent\u3001{models} \u4E2A\u6A21\u578B\u3002",
@@ -19386,6 +19234,7 @@ var ZH = {
19386
19234
  "details.copySuccess": "\u539F\u59CB\u6570\u636E\u5DF2\u590D\u5236",
19387
19235
  "details.id": "\u7F16\u53F7",
19388
19236
  "details.project": "\u9879\u76EE\u76EE\u5F55",
19237
+ "details.variant": "\u6A21\u578B Variant",
19389
19238
  "details.prompt": "\u63D0\u793A\u8BCD",
19390
19239
  "details.result": "\u6267\u884C\u7ED3\u679C / \u5931\u8D25\u539F\u56E0",
19391
19240
  "details.category": "\u5206\u7C7B",
@@ -19418,7 +19267,7 @@ var ZH = {
19418
19267
  "details.enabledYes": "\u5DF2\u542F\u7528",
19419
19268
  "details.enabledNo": "\u5DF2\u505C\u7528",
19420
19269
  "dialog.cancelTask": "\u53D6\u6D88\u4EFB\u52A1 #{id}\uFF1F",
19421
- "dialog.cancelTaskBody": "\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u4F1A\u5728\u4E0B\u4E00\u4E2A\u8F6E\u8BE2\u5468\u671F\u7EC8\u6B62\u5BF9\u5E94\u8FDB\u7A0B\u6811\u3002",
19270
+ "dialog.cancelTaskBody": "\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u4F1A\u5728\u4E0B\u4E00\u4E2A\u8F6E\u8BE2\u5468\u671F\u7EC8\u6B62\u5BF9\u5E94\u7684\u53D7\u7BA1\u8FDB\u7A0B\u7EC4\u3002",
19422
19271
  "dialog.retryTask": "\u91CD\u8BD5\u4EFB\u52A1 #{id}\uFF1F",
19423
19272
  "dialog.retryTaskBody": "\u4EFB\u52A1\u5C06\u56DE\u5230\u5F85\u6267\u884C\u72B6\u6001\uFF0C\u5E76\u91CD\u7F6E\u81EA\u52A8\u91CD\u8BD5\u9884\u7B97\u3002",
19424
19273
  "dialog.deleteTask": "\u5220\u9664\u4EFB\u52A1 #{id}\uFF1F",
@@ -19617,6 +19466,7 @@ var EN = {
19617
19466
  "template.cwdHint": "OpenCode runs the task in this directory.",
19618
19467
  "template.agent": "Agent",
19619
19468
  "template.model": "Model",
19469
+ "template.variant": "Model variant",
19620
19470
  "template.prompt": "Prompt",
19621
19471
  "template.scheduleType": "Schedule",
19622
19472
  "template.cronExpr": "Cron expression",
@@ -19654,9 +19504,11 @@ var EN = {
19654
19504
  "catalog.chooseProject": "Choose a project directory first",
19655
19505
  "catalog.defaultModel": "Use the Agent / OpenCode default model",
19656
19506
  "catalog.defaultProvider": "Default model",
19507
+ "catalog.defaultVariant": "Use the Agent / model default",
19657
19508
  "catalog.provider": "Model provider",
19658
19509
  "catalog.model": "Model",
19659
- "catalog.modelHint": "Choose a provider, then a model returned by opencode models for this project. Default does not pass -m.",
19510
+ "catalog.modelHint": "Choose a provider, then a model returned by opencode models --verbose for this project. Default does not pass -m.",
19511
+ "catalog.variantHint": "Only variants declared by the selected model are shown. Default does not pass --variant.",
19660
19512
  "catalog.agentHint": "Loaded from opencode agent list for this project.",
19661
19513
  "catalog.loading": "Loading Agents and models available to this project\u2026",
19662
19514
  "catalog.loaded": "Loaded {agents} Agents and {models} models from local OpenCode.",
@@ -19687,6 +19539,7 @@ var EN = {
19687
19539
  "details.copySuccess": "Raw data copied",
19688
19540
  "details.id": "ID",
19689
19541
  "details.project": "Project directory",
19542
+ "details.variant": "Model variant",
19690
19543
  "details.prompt": "Prompt",
19691
19544
  "details.result": "Result / failure reason",
19692
19545
  "details.category": "Category",
@@ -19719,7 +19572,7 @@ var EN = {
19719
19572
  "details.enabledYes": "Enabled",
19720
19573
  "details.enabledNo": "Disabled",
19721
19574
  "dialog.cancelTask": "Cancel task #{id}?",
19722
- "dialog.cancelTaskBody": "A running task will terminate its process tree on the next worker poll.",
19575
+ "dialog.cancelTaskBody": "A running task will terminate its managed process group on the next worker poll.",
19723
19576
  "dialog.retryTask": "Retry task #{id}?",
19724
19577
  "dialog.retryTaskBody": "The task returns to pending and its automatic retry budget is reset.",
19725
19578
  "dialog.deleteTask": "Delete task #{id}?",
@@ -20211,6 +20064,7 @@ function clientMessages(locale) {
20211
20064
  "details.copySuccess",
20212
20065
  "details.id",
20213
20066
  "details.project",
20067
+ "details.variant",
20214
20068
  "details.prompt",
20215
20069
  "details.result",
20216
20070
  "details.category",
@@ -20311,6 +20165,7 @@ function clientMessages(locale) {
20311
20165
  "catalog.chooseProject",
20312
20166
  "catalog.defaultModel",
20313
20167
  "catalog.defaultProvider",
20168
+ "catalog.defaultVariant",
20314
20169
  "catalog.loading",
20315
20170
  "catalog.loaded",
20316
20171
  "catalog.failed",
@@ -20435,23 +20290,23 @@ function renderLayout(options) {
20435
20290
  function detailSession(value){if(!value)return text('details.none');const session=String(value);return session.length<=10?session:session.slice(0,6)+'***'+session.slice(-4)}
20436
20291
  function detailTaskResult(data){const presentation=data._resultPresentation;if(!presentation)return text('details.none');const parts=[];if(Array.isArray(presentation.errors)&&presentation.errors.length)parts.push(presentation.errors.join('\\n'));if(presentation.text)parts.push(presentation.text);return parts.join('\\n\\n')||text('details.none')}
20437
20292
  function detailField(label,value,options={}){const item=document.createElement('div');item.className='detail-item'+(options.wide?' wide':'');const name=document.createElement('div');name.className='detail-label';name.textContent=label;const content=options.long?document.createElement('pre'):document.createElement('div');content.className='detail-value'+(options.mono?' mono':'')+(options.long?' long':'');content.textContent=value===null||value===undefined||value===''?text('details.none'):String(value);item.append(name,content);return item}
20438
- function renderDetailHistory(runs){const section=document.createElement('section');section.className='detail-history';const title=document.createElement('h3');title.textContent=text('details.history');section.appendChild(title);if(!Array.isArray(runs)||runs.length===0){const empty=document.createElement('div');empty.className='muted small';empty.textContent=text('details.noHistory');section.appendChild(empty);return section}const list=document.createElement('div');list.className='detail-history-list';for(const run of runs){const item=document.createElement('div');item.className='detail-history-item';const primary=document.createElement('strong');primary.textContent='Run #'+run.id+' \xB7 '+detailStatus('run',run.status);const secondary=document.createElement('span');secondary.className='muted';secondary.textContent=detailDate(run.startedAt)+(run.model?' \xB7 '+detailModel(run.model):'');item.append(primary,secondary);list.appendChild(item)}section.appendChild(list);return section}
20293
+ function renderDetailHistory(runs){const section=document.createElement('section');section.className='detail-history';const title=document.createElement('h3');title.textContent=text('details.history');section.appendChild(title);if(!Array.isArray(runs)||runs.length===0){const empty=document.createElement('div');empty.className='muted small';empty.textContent=text('details.noHistory');section.appendChild(empty);return section}const list=document.createElement('div');list.className='detail-history-list';for(const run of runs){const item=document.createElement('div');item.className='detail-history-item';const primary=document.createElement('strong');primary.textContent='Run #'+run.id+' \xB7 '+detailStatus('run',run.status);const secondary=document.createElement('span');secondary.className='muted';secondary.textContent=detailDate(run.startedAt)+(run.model?' \xB7 '+detailModel(run.model):'')+(run.variant?' \xB7 '+run.variant:'');item.append(primary,secondary);list.appendChild(item)}section.appendChild(list);return section}
20439
20294
  function detailFields(type,data){if(type==='task')return [
20440
20295
  [text('details.id'),'#'+data.id],[text('table.name'),data.name],[text('table.status'),detailStatus('task',data.status)],[text('details.project'),data.cwd,{wide:true,mono:true}],
20441
- [text('table.agent'),data.agent],[text('table.model'),detailModel(data.model)],[text('details.prompt'),data.prompt,{wide:true,long:true}],
20296
+ [text('table.agent'),data.agent],[text('table.model'),detailModel(data.model)],[text('details.variant'),data.variant||text('details.default')],[text('details.prompt'),data.prompt,{wide:true,long:true}],
20442
20297
  [text('details.category'),data.category],[text('details.batch'),data.batchId],[text('details.dependency'),data.dependsOn?'#'+data.dependsOn:text('details.none')],
20443
20298
  [text('details.importance'),data.importance],[text('details.urgency'),data.urgency],[text('details.retryCount'),String(data.retryCount??0)+' / '+String(data.maxRetries??0)],
20444
20299
  [text('details.retryBackoff'),detailDuration(data.retryBackoffMs)],[text('details.timeout'),detailDuration(data.timeoutMs)],[text('details.scheduledAt'),detailDate(data.scheduledAt)],
20445
20300
  [text('details.createdAt'),detailDate(data.createdAt)],[text('details.startedAt'),detailDate(data.startedAt)],[text('details.finishedAt'),detailDate(data.finishedAt)],
20446
20301
  [text('details.result'),detailTaskResult(data),{wide:true,long:true}]
20447
20302
  ];if(type==='run'){const started=data.startedAt?new Date(data.startedAt).getTime():null;const finished=data.finishedAt?new Date(data.finishedAt).getTime():null;return [
20448
- [text('details.id'),'Run #'+data.id],[text('details.taskId'),'#'+data.taskId],[text('table.status'),detailStatus('run',data.status)],[text('table.model'),detailModel(data.model)],
20303
+ [text('details.id'),'Run #'+data.id],[text('details.taskId'),'#'+data.taskId],[text('table.status'),detailStatus('run',data.status)],[text('table.model'),detailModel(data.model)],[text('details.variant'),data.variant||text('details.default')],
20449
20304
  [text('details.session'),detailSession(data.sessionId)],[text('details.startedAt'),detailDate(data.startedAt)],[text('details.finishedAt'),detailDate(data.finishedAt)],
20450
20305
  [text('table.duration'),started!==null?detailDuration((finished??Date.now())-started):text('details.none')],[text('details.heartbeat'),detailDate(data.heartbeatAt)],
20451
20306
  [text('details.process'),'Worker PID '+String(data.workerPid??'\u2014')+' \xB7 OpenCode PID '+String(data.childPid??'\u2014'),{wide:true,mono:true}]
20452
20307
  ];}const scheduleRule=data.scheduleType==='cron'?data.cronExpr:data.scheduleType==='recurring'?detailDuration(data.intervalMs):detailDate(data.runAt);return [
20453
20308
  [text('details.id'),'#'+data.id],[text('table.name'),data.name],[text('details.enabled'),data.enabled?text('details.enabledYes'):text('details.enabledNo')],[text('details.project'),data.cwd,{wide:true,mono:true}],
20454
- [text('table.agent'),data.agent],[text('table.model'),detailModel(data.model)],[text('details.prompt'),data.prompt,{wide:true,long:true}],
20309
+ [text('table.agent'),data.agent],[text('table.model'),detailModel(data.model)],[text('details.variant'),data.variant||text('details.default')],[text('details.prompt'),data.prompt,{wide:true,long:true}],
20455
20310
  [text('template.scheduleType'),detailScheduleType(data.scheduleType)],[text('details.scheduleRule'),scheduleRule],[text('details.category'),data.category],[text('details.batch'),data.batchId],
20456
20311
  [text('details.importance'),data.importance],[text('details.urgency'),data.urgency],[text('details.maxInstances'),data.maxInstances],[text('details.maxRetries'),data.maxRetries??0],
20457
20312
  [text('details.retryBackoff'),detailDuration(data.retryBackoffMs)],[text('details.timeout'),detailDuration(data.timeoutMs)],[text('details.lastRun'),detailDate(data.lastRunAt)],[text('details.nextRun'),detailDate(data.nextRunAt)],
@@ -20468,12 +20323,20 @@ function renderLayout(options) {
20468
20323
  function updateTaskProjectStatus(){const node=taskField('project-status');if(!node)return;const cwd=taskField('cwd').value.trim();if(!cwd){node.textContent='';return}const project=taskProjects()[cwd];node.textContent=project?text('task.projectExisting',project):text('task.projectNew')}
20469
20324
  const catalogTimers={};const catalogRequests={};const catalogModels={};
20470
20325
  function catalogField(prefix,name){return document.getElementById(prefix+'-'+name)}
20471
- function resetCatalog(prefix){const agent=catalogField(prefix,'agent');const provider=catalogField(prefix,'model-provider');const model=catalogField(prefix,'model');if(!agent||!provider||!model)return;catalogModels[prefix]=[];agent.replaceChildren(new Option(text('catalog.chooseProject'),''));provider.replaceChildren(new Option(text('catalog.defaultProvider'),''));model.replaceChildren(new Option(text('catalog.defaultModel'),'default'));agent.disabled=false;provider.disabled=true;model.disabled=true;catalogField(prefix,'catalog-status').textContent='';}
20326
+ function resetCatalog(prefix){const agent=catalogField(prefix,'agent');const provider=catalogField(prefix,'model-provider');const model=catalogField(prefix,'model');const variant=catalogField(prefix,'variant');if(!agent||!provider||!model||!variant)return;const preferredVariant=variant.dataset.preferred||'';catalogModels[prefix]=[];agent.replaceChildren(new Option(text('catalog.chooseProject'),''));provider.replaceChildren(new Option(text('catalog.defaultProvider'),''));model.replaceChildren(new Option(text('catalog.defaultModel'),'default'));variant.replaceChildren(new Option(text('catalog.defaultVariant'),''));if(preferredVariant)appendCurrentOption(variant,preferredVariant);variant.value=preferredVariant;agent.disabled=false;provider.disabled=true;model.disabled=true;variant.disabled=!preferredVariant;catalogField(prefix,'catalog-status').textContent='';}
20472
20327
  function appendCurrentOption(select,value){if(!value||[...select.options].some(option=>option.value===value))return;select.appendChild(new Option(value,value));}
20328
+ function setPreferredVariant(prefix,modelValue,variantValue){const variant=catalogField(prefix,'variant');if(!variant)return;const preferred=variantValue||'';variant.dataset.preferred=preferred;variant.dataset.preferredModel=modelValue||'default';variant.replaceChildren(new Option(text('catalog.defaultVariant'),''));if(preferred)appendCurrentOption(variant,preferred);variant.value=preferred;variant.disabled=!preferred}
20329
+ function invalidateVariantRequest(prefix){const key=prefix+'-variant';catalogRequests[key]=(catalogRequests[key]||0)+1}
20330
+ function invalidateCatalogRequests(prefix){clearTimeout(catalogTimers[prefix]);catalogRequests[prefix]=(catalogRequests[prefix]||0)+1;invalidateVariantRequest(prefix)}
20331
+ function clearVariantPreference(prefix){const variant=catalogField(prefix,'variant');if(!variant)return;invalidateVariantRequest(prefix);variant.dataset.preferred='';variant.dataset.preferredModel='';variant.replaceChildren(new Option(text('catalog.defaultVariant'),''));variant.value='';variant.disabled=true}
20332
+ function handleProviderChange(prefix){clearVariantPreference(prefix);populateModelOptions(prefix)}
20333
+ function handleModelChange(prefix){clearVariantPreference(prefix);populateVariantOptions(prefix)}
20334
+ function handleVariantChange(prefix){const variant=catalogField(prefix,'variant');if(!variant)return;invalidateVariantRequest(prefix);variant.dataset.preferred='';variant.dataset.preferredModel=''}
20473
20335
  function modelProvider(value){const slash=value.indexOf('/');return slash>0?value.slice(0,slash):''}
20474
- function populateModelOptions(prefix,preferredModel=''){const provider=catalogField(prefix,'model-provider');const model=catalogField(prefix,'model');if(!provider||!model)return;const selectedProvider=provider.value;model.replaceChildren();if(!selectedProvider){model.appendChild(new Option(text('catalog.defaultModel'),'default'));model.disabled=true;return}const available=(catalogModels[prefix]||[]).filter(value=>modelProvider(value)===selectedProvider);for(const value of available)model.appendChild(new Option(value,value));if(preferredModel&&available.includes(preferredModel))model.value=preferredModel;model.disabled=available.length===0}
20336
+ function populateModelOptions(prefix,preferredModel=''){const provider=catalogField(prefix,'model-provider');const model=catalogField(prefix,'model');if(!provider||!model)return;const selectedProvider=provider.value;model.replaceChildren();if(!selectedProvider){model.appendChild(new Option(text('catalog.defaultModel'),'default'));model.disabled=true;populateVariantOptions(prefix);return}const available=(catalogModels[prefix]||[]).filter(value=>modelProvider(value)===selectedProvider);for(const value of available)model.appendChild(new Option(value,value));if(preferredModel&&available.includes(preferredModel))model.value=preferredModel;model.disabled=available.length===0;populateVariantOptions(prefix)}
20337
+ async function populateVariantOptions(prefix,preferredVariant=''){const cwd=catalogField(prefix,'cwd')?.value.trim()||'';const model=catalogField(prefix,'model');const variant=catalogField(prefix,'variant');if(!model||!variant)return;const selectedModel=model.value;const preferred=preferredVariant||(variant.dataset.preferredModel===selectedModel?variant.dataset.preferred||'':'');if(!cwd||!selectedModel||selectedModel==='default'){variant.replaceChildren(new Option(text('catalog.defaultVariant'),''));if(preferred)appendCurrentOption(variant,preferred);variant.value=preferred;variant.dataset.preferred='';variant.dataset.preferredModel='';variant.disabled=!preferred;return}if(!preferred){variant.replaceChildren(new Option(text('catalog.defaultVariant'),''));variant.value=''}const requestKey=prefix+'-variant';const request=(catalogRequests[requestKey]||0)+1;catalogRequests[requestKey]=request;variant.disabled=true;try{const data=await readJson(await fetch('/api/opencode/catalog?cwd='+encodeURIComponent(cwd)));if(catalogRequests[requestKey]!==request||catalogField(prefix,'cwd')?.value.trim()!==cwd||model.value!==selectedModel)return;const available=Array.isArray(data.variantsByModel?.[selectedModel])?data.variantsByModel[selectedModel]:[];variant.replaceChildren(new Option(text('catalog.defaultVariant'),''));for(const value of available)variant.appendChild(new Option(value,value));if(preferred)appendCurrentOption(variant,preferred);variant.value=[...variant.options].some(option=>option.value===preferred)?preferred:'';variant.dataset.preferred='';variant.dataset.preferredModel='';variant.disabled=available.length===0&&!preferred}catch(error){if(catalogRequests[requestKey]!==request||catalogField(prefix,'cwd')?.value.trim()!==cwd||model.value!==selectedModel)return;if(preferred)appendCurrentOption(variant,preferred);variant.value=preferred;variant.dataset.preferred=preferred;variant.dataset.preferredModel=selectedModel;variant.disabled=!preferred}}
20475
20338
  async function loadCatalog(prefix,preferredAgent='',preferredModel='default',preserveUnavailable=false){const cwd=catalogField(prefix,'cwd')?.value.trim()||'';const status=catalogField(prefix,'catalog-status');const agent=catalogField(prefix,'agent');const provider=catalogField(prefix,'model-provider');const model=catalogField(prefix,'model');if(!cwd||!status||!agent||!provider||!model){if(!cwd)resetCatalog(prefix);return}const request=(catalogRequests[prefix]||0)+1;catalogRequests[prefix]=request;status.dataset.state='loading';status.textContent=text('catalog.loading');agent.disabled=true;provider.disabled=true;model.disabled=true;try{const data=await readJson(await fetch('/api/opencode/catalog?cwd='+encodeURIComponent(cwd)));if(catalogRequests[prefix]!==request||catalogField(prefix,'cwd').value.trim()!==cwd)return;agent.replaceChildren();for(const item of data.agents){const label=item.name+' \u2014 '+text('catalog.'+item.mode);agent.appendChild(new Option(label,item.name))}if(preserveUnavailable)appendCurrentOption(agent,preferredAgent);const defaultAgent=preferredAgent||data.agents.find(item=>item.name==='build')?.name||data.agents.find(item=>item.mode==='primary')?.name||data.agents[0]?.name||'';agent.value=[...agent.options].some(option=>option.value===defaultAgent)?defaultAgent:(agent.options[0]?.value||'');catalogModels[prefix]=[...data.models];if(preserveUnavailable&&preferredModel&&preferredModel!=='default'&&!catalogModels[prefix].includes(preferredModel))catalogModels[prefix].push(preferredModel);provider.replaceChildren(new Option(text('catalog.defaultProvider'),''));for(const name of [...new Set(catalogModels[prefix].map(modelProvider).filter(Boolean))].sort())provider.appendChild(new Option(name,name));const preferredProvider=preferredModel==='default'?'':modelProvider(preferredModel);provider.value=[...provider.options].some(option=>option.value===preferredProvider)?preferredProvider:'';populateModelOptions(prefix,preferredModel);status.dataset.state='ready';status.textContent=text('catalog.loaded',{agents:data.agents.length,models:data.models.length})}catch(error){if(catalogRequests[prefix]!==request)return;resetCatalog(prefix);if(preserveUnavailable){appendCurrentOption(agent,preferredAgent);if(preferredModel&&preferredModel!=='default'){catalogModels[prefix]=[preferredModel];appendCurrentOption(provider,modelProvider(preferredModel));provider.value=modelProvider(preferredModel);populateModelOptions(prefix,preferredModel)}}status.dataset.state='error';status.textContent=text('catalog.failed',{error:error.message})}finally{if(catalogRequests[prefix]===request){agent.disabled=false;provider.disabled=false;if(provider.value)model.disabled=false}}}
20476
- function scheduleCatalogLoad(prefix){clearTimeout(catalogTimers[prefix]);catalogTimers[prefix]=setTimeout(()=>loadCatalog(prefix),450)}
20339
+ function scheduleCatalogLoad(prefix){invalidateCatalogRequests(prefix);resetCatalog(prefix);catalogTimers[prefix]=setTimeout(()=>loadCatalog(prefix),450)}
20477
20340
  let directoryTargetId='';let directoryCurrent='';let directoryEntries=[];let directoryShowHidden=false;
20478
20341
  function renderDirectoryEntries(){const list=document.getElementById('directory-list');list.replaceChildren();const entries=directoryEntries.filter(entry=>directoryShowHidden||!entry.hidden);const hidden=document.getElementById('directory-hidden');hidden.textContent=text(directoryShowHidden?'action.hideHidden':'action.showHidden');if(entries.length===0){const empty=document.createElement('div');empty.className='directory-empty';empty.textContent=text('directory.empty');list.appendChild(empty);return}for(const entry of entries){const button=document.createElement('button');button.type='button';button.className='directory-item';button.innerHTML='${icon("folder")}<span></span>';button.querySelector('span').textContent=entry.name;button.onclick=()=>browseDirectory(entry.path);list.appendChild(button)}}
20479
20342
  async function browseDirectory(path=''){const choose=document.getElementById('directory-choose');choose.disabled=true;try{const suffix=path?'?path='+encodeURIComponent(path):'';const data=await readJson(await fetch('/api/filesystem/directories'+suffix));directoryCurrent=data.path;directoryEntries=data.directories;document.getElementById('directory-path').textContent=data.path;document.getElementById('directory-up').disabled=data.parent===data.path;document.getElementById('directory-up').onclick=()=>browseDirectory(data.parent);document.getElementById('directory-home').onclick=()=>browseDirectory(data.home);renderDirectoryEntries();choose.disabled=false;return true}catch(error){directoryCurrent='';directoryEntries=[];document.getElementById('directory-path').textContent='';renderDirectoryEntries();showToast(error.message,'error');return false}}
@@ -20483,16 +20346,16 @@ function renderLayout(options) {
20483
20346
  function updateDurationControl(id){const preset=document.getElementById(id+'-preset');const custom=document.getElementById(id+'-custom');const input=document.getElementById(id+'-value');const visible=preset.value==='custom';custom.hidden=!visible;input.required=visible;if(visible&&!input.value)input.value='1'}
20484
20347
  function readDuration(id){const preset=document.getElementById(id+'-preset').value;if(preset==='')return '';if(preset!=='custom')return preset==='0'?'0':preset+'ms';const value=document.getElementById(id+'-value').value.trim();return value===''?'':value+document.getElementById(id+'-unit').value}
20485
20348
  function setDuration(id,milliseconds){const preset=document.getElementById(id+'-preset');const exact=milliseconds==null?'':String(milliseconds);if([...preset.options].some(option=>option.value===exact)){preset.value=exact;updateDurationControl(id);return}preset.value='custom';const input=document.getElementById(id+'-value');const unit=document.getElementById(id+'-unit');const units=[['d',86400000],['h',3600000],['min',60000],['s',1000]];let matched=false;for(const [name,factor] of units){if(milliseconds!=null&&(milliseconds===0||milliseconds%factor===0)){input.value=String(milliseconds/factor);unit.value=name;matched=true;break}}if(!matched){input.value=String((milliseconds??60000)/1000);unit.value='s'}updateDurationControl(id)}
20486
- function openTaskCreator(){const form=document.getElementById('task-form');form.reset();taskField('id').value='';taskField('cwd').readOnly=false;taskField('cwd-picker').hidden=false;taskField('cwd').value=form.dataset.defaultCwd||'';setDuration('task-retry-backoff',30000);setDuration('task-timeout',null);taskField('dialog-title').textContent=text('task.createTitle');taskField('save').textContent=text('action.saveTask');updateTaskProjectStatus();resetCatalog('task');document.getElementById('task-dialog').showModal();if(taskField('cwd').value)loadCatalog('task');setTimeout(()=>taskField('name').focus(),50)}
20487
- async function openTaskEditor(id){try{const data=await readJson(await fetch('/api/tasks/'+id));taskField('id').value=String(id);taskField('cwd').value=data.cwd||'';taskField('cwd').readOnly=true;taskField('cwd-picker').hidden=true;taskField('name').value=data.name||'';taskField('prompt').value=data.prompt||'';taskField('category').value=data.category||'general';taskField('batch').value=data.batchId||'';taskField('importance').value=String(data.importance??3);taskField('urgency').value=String(data.urgency??3);taskField('max-retries').value=String(data.maxRetries??3);setDuration('task-retry-backoff',data.retryBackoffMs??30000);setDuration('task-timeout',data.timeoutMs);taskField('dialog-title').textContent=text('task.editTitle');taskField('save').textContent=text('action.updateTask');updateTaskProjectStatus();resetCatalog('task');document.getElementById('task-dialog').showModal();loadCatalog('task',data.agent||'',data.model||'default',true);setTimeout(()=>taskField('name').focus(),50)}catch(error){showToast(error.message,'error')}}
20488
- async function saveTask(event){event.preventDefault();const form=document.getElementById('task-form');if(!form.reportValidity())return;const id=taskField('id').value;const body={name:taskField('name').value,cwd:taskField('cwd').value,agent:taskField('agent').value,model:taskField('model').value,prompt:taskField('prompt').value,category:taskField('category').value,batchId:taskField('batch').value,importance:Number(taskField('importance').value),urgency:Number(taskField('urgency').value),maxRetries:Number(taskField('max-retries').value),retryBackoff:readDuration('task-retry-backoff'),timeout:readDuration('task-timeout')};const button=taskField('save');button.disabled=true;try{const data=await readJson(await fetch(id?'/api/tasks/'+id:'/api/tasks',{method:id?'PUT':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}));showToast(text(id?'feedback.taskUpdated':'feedback.taskCreated',{id:data.task.id}));document.getElementById('task-dialog').close();setTimeout(()=>location.assign(id?location.href:'/?cwd='+encodeURIComponent(data.task.cwd||'')),450)}catch(error){showToast(error.message,'error')}finally{button.disabled=false}}
20349
+ function openTaskCreator(){invalidateCatalogRequests('task');const form=document.getElementById('task-form');form.reset();taskField('id').value='';setPreferredVariant('task','default','');taskField('cwd').readOnly=false;taskField('cwd-picker').hidden=false;taskField('cwd').value=form.dataset.defaultCwd||'';setDuration('task-retry-backoff',30000);setDuration('task-timeout',null);taskField('dialog-title').textContent=text('task.createTitle');taskField('save').textContent=text('action.saveTask');updateTaskProjectStatus();resetCatalog('task');document.getElementById('task-dialog').showModal();if(taskField('cwd').value)loadCatalog('task');setTimeout(()=>taskField('name').focus(),50)}
20350
+ async function openTaskEditor(id){invalidateCatalogRequests('task');try{const data=await readJson(await fetch('/api/tasks/'+id));document.getElementById('task-form').reset();taskField('id').value=String(id);taskField('cwd').value=data.cwd||'';taskField('cwd').readOnly=true;taskField('cwd-picker').hidden=true;taskField('name').value=data.name||'';taskField('prompt').value=data.prompt||'';taskField('category').value=data.category||'general';taskField('batch').value=data.batchId||'';taskField('importance').value=String(data.importance??3);taskField('urgency').value=String(data.urgency??3);taskField('max-retries').value=String(data.maxRetries??3);setDuration('task-retry-backoff',data.retryBackoffMs??30000);setDuration('task-timeout',data.timeoutMs);taskField('dialog-title').textContent=text('task.editTitle');taskField('save').textContent=text('action.updateTask');updateTaskProjectStatus();setPreferredVariant('task',data.model||'default',data.variant||'');resetCatalog('task');document.getElementById('task-dialog').showModal();loadCatalog('task',data.agent||'',data.model||'default',true);setTimeout(()=>taskField('name').focus(),50)}catch(error){showToast(error.message,'error')}}
20351
+ async function saveTask(event){event.preventDefault();const form=document.getElementById('task-form');if(!form.reportValidity())return;const id=taskField('id').value;const body={name:taskField('name').value,cwd:taskField('cwd').value,agent:taskField('agent').value,model:taskField('model').value,variant:taskField('variant').value,prompt:taskField('prompt').value,category:taskField('category').value,batchId:taskField('batch').value,importance:Number(taskField('importance').value),urgency:Number(taskField('urgency').value),maxRetries:Number(taskField('max-retries').value),retryBackoff:readDuration('task-retry-backoff'),timeout:readDuration('task-timeout')};const button=taskField('save');button.disabled=true;try{const data=await readJson(await fetch(id?'/api/tasks/'+id:'/api/tasks',{method:id?'PUT':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}));showToast(text(id?'feedback.taskUpdated':'feedback.taskCreated',{id:data.task.id}));document.getElementById('task-dialog').close();setTimeout(()=>location.assign(id?location.href:'/?cwd='+encodeURIComponent(data.task.cwd||'')),450)}catch(error){showToast(error.message,'error')}finally{button.disabled=false}}
20489
20352
  function localDateTime(milliseconds){const date=new Date(milliseconds);const local=new Date(milliseconds-date.getTimezoneOffset()*60000);return local.toISOString().slice(0,23)}
20490
20353
  function updateTemplateScheduleFields(){const type=templateField('schedule-type').value;const fields={cron:templateField('cron-field'),recurring:templateField('interval-field'),delayed:templateField('run-at-field')};for(const [name,node] of Object.entries(fields)){node.hidden=name!==type;for(const control of node.querySelectorAll('input,select'))control.required=false;const required=name==='recurring'?node.querySelector('[id$="-preset"]'):node.querySelector('input');if(required)required.required=name===type;if(name==='recurring'&&name===type)updateDurationControl('template-interval')}}
20491
20354
  function setOriginalRunAt(epoch){const input=templateField('run-at');const local=epoch?localDateTime(epoch):'';input.value=local;input.dataset.originalEpoch=epoch?String(epoch):'';input.dataset.originalLocal=local}
20492
20355
  function selectedRunAt(){const input=templateField('run-at');return resolveEditedRunAt(input.dataset.originalEpoch?Number(input.dataset.originalEpoch):null,input.dataset.originalLocal||'',input.value)}
20493
- function openTemplateCreator(){const form=document.getElementById('template-form');form.reset();templateField('id').value='';templateField('dialog-title').textContent=text('template.createTitle');setDuration('template-interval',3600000);setDuration('template-retry-backoff',30000);setDuration('template-timeout',null);setOriginalRunAt(null);templateField('run-at').value=localDateTime(Date.now()+3600000);updateTemplateScheduleFields();resetCatalog('template');document.getElementById('template-dialog').showModal();if(templateField('cwd').value)loadCatalog('template');setTimeout(()=>templateField('name').focus(),50)}
20494
- async function openTemplateEditor(id){try{const data=await readJson(await fetch('/api/templates/'+id));templateField('id').value=String(id);templateField('dialog-title').textContent=text('template.editTitle');templateField('name').value=data.name||'';templateField('cwd').value=data.cwd||'';templateField('prompt').value=data.prompt||'';templateField('schedule-type').value=data.scheduleType;templateField('cron').value=data.cronExpr||'';setDuration('template-interval',data.intervalMs);setOriginalRunAt(data.runAt||null);if(!data.runAt)templateField('run-at').value=localDateTime(Date.now()+3600000);templateField('category').value=data.category||'general';templateField('batch').value=data.batchId||'';templateField('importance').value=String(data.importance??3);templateField('urgency').value=String(data.urgency??3);templateField('max-instances').value=String(data.maxInstances??1);templateField('max-retries').value=String(data.maxRetries??3);setDuration('template-retry-backoff',data.retryBackoffMs??30000);setDuration('template-timeout',data.timeoutMs);updateTemplateScheduleFields();resetCatalog('template');document.getElementById('template-dialog').showModal();loadCatalog('template',data.agent||'',data.model||'default',true);setTimeout(()=>templateField('name').focus(),50)}catch(error){showToast(error.message,'error')}}
20495
- async function saveTemplate(event){event.preventDefault();const form=document.getElementById('template-form');if(!form.reportValidity())return;const id=templateField('id').value;const type=templateField('schedule-type').value;const body={name:templateField('name').value,cwd:templateField('cwd').value,agent:templateField('agent').value,model:templateField('model').value,prompt:templateField('prompt').value,scheduleType:type,cronExpr:templateField('cron').value,interval:readDuration('template-interval'),runAt:type==='delayed'?selectedRunAt():null,category:templateField('category').value,batchId:templateField('batch').value,importance:Number(templateField('importance').value),urgency:Number(templateField('urgency').value),maxInstances:Number(templateField('max-instances').value),maxRetries:Number(templateField('max-retries').value),retryBackoff:readDuration('template-retry-backoff'),timeout:readDuration('template-timeout')};const button=templateField('save');button.disabled=true;try{await readJson(await fetch(id?'/api/templates/'+id:'/api/templates',{method:id?'PUT':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}));showToast(text(id?'feedback.templateUpdated':'feedback.templateCreated'));document.getElementById('template-dialog').close();setTimeout(()=>location.assign(id?location.href:'/templates'),450)}catch(error){showToast(error.message,'error')}finally{button.disabled=false}}
20356
+ function openTemplateCreator(){invalidateCatalogRequests('template');const form=document.getElementById('template-form');form.reset();templateField('id').value='';setPreferredVariant('template','default','');templateField('dialog-title').textContent=text('template.createTitle');setDuration('template-interval',3600000);setDuration('template-retry-backoff',30000);setDuration('template-timeout',null);setOriginalRunAt(null);templateField('run-at').value=localDateTime(Date.now()+3600000);updateTemplateScheduleFields();resetCatalog('template');document.getElementById('template-dialog').showModal();if(templateField('cwd').value)loadCatalog('template');setTimeout(()=>templateField('name').focus(),50)}
20357
+ async function openTemplateEditor(id){invalidateCatalogRequests('template');try{const data=await readJson(await fetch('/api/templates/'+id));document.getElementById('template-form').reset();templateField('id').value=String(id);templateField('dialog-title').textContent=text('template.editTitle');templateField('name').value=data.name||'';templateField('cwd').value=data.cwd||'';templateField('prompt').value=data.prompt||'';templateField('schedule-type').value=data.scheduleType;templateField('cron').value=data.cronExpr||'';setDuration('template-interval',data.intervalMs);setOriginalRunAt(data.runAt||null);if(!data.runAt)templateField('run-at').value=localDateTime(Date.now()+3600000);templateField('category').value=data.category||'general';templateField('batch').value=data.batchId||'';templateField('importance').value=String(data.importance??3);templateField('urgency').value=String(data.urgency??3);templateField('max-instances').value=String(data.maxInstances??1);templateField('max-retries').value=String(data.maxRetries??3);setDuration('template-retry-backoff',data.retryBackoffMs??30000);setDuration('template-timeout',data.timeoutMs);updateTemplateScheduleFields();setPreferredVariant('template',data.model||'default',data.variant||'');resetCatalog('template');document.getElementById('template-dialog').showModal();loadCatalog('template',data.agent||'',data.model||'default',true);setTimeout(()=>templateField('name').focus(),50)}catch(error){showToast(error.message,'error')}}
20358
+ async function saveTemplate(event){event.preventDefault();const form=document.getElementById('template-form');if(!form.reportValidity())return;const id=templateField('id').value;const type=templateField('schedule-type').value;const body={name:templateField('name').value,cwd:templateField('cwd').value,agent:templateField('agent').value,model:templateField('model').value,variant:templateField('variant').value,prompt:templateField('prompt').value,scheduleType:type,cronExpr:templateField('cron').value,interval:readDuration('template-interval'),runAt:type==='delayed'?selectedRunAt():null,category:templateField('category').value,batchId:templateField('batch').value,importance:Number(templateField('importance').value),urgency:Number(templateField('urgency').value),maxInstances:Number(templateField('max-instances').value),maxRetries:Number(templateField('max-retries').value),retryBackoff:readDuration('template-retry-backoff'),timeout:readDuration('template-timeout')};const button=templateField('save');button.disabled=true;try{await readJson(await fetch(id?'/api/templates/'+id:'/api/templates',{method:id?'PUT':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}));showToast(text(id?'feedback.templateUpdated':'feedback.templateCreated'));document.getElementById('template-dialog').close();setTimeout(()=>location.assign(id?location.href:'/templates'),450)}catch(error){showToast(error.message,'error')}finally{button.disabled=false}}
20496
20359
  async function enableTmpl(id){try{await readJson(await fetch('/api/templates/'+id+'/enable',{method:'POST'}));location.reload()}catch(error){showToast(error.message,'error')}}
20497
20360
  async function disableTmpl(id){if(!await ask(text('dialog.disableTemplate'),text('dialog.disableTemplateBody')))return;try{await readJson(await fetch('/api/templates/'+id+'/disable',{method:'POST'}));location.reload()}catch(error){showToast(error.message,'error')}}
20498
20361
  async function deleteTmpl(id){if(!await ask(text('dialog.deleteTemplate'),text('dialog.deleteTemplateBody'),true))return;try{await readJson(await fetch('/api/templates/'+id,{method:'DELETE'}));location.reload()}catch(error){showToast(error.message,'error')}}
@@ -20520,6 +20383,7 @@ var TASK_STATUSES = /* @__PURE__ */ new Set([
20520
20383
  "cancelled"
20521
20384
  ]);
20522
20385
  var SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_]+$/;
20386
+ var LOOPBACK_HOST_PATTERN = /^(?:localhost|127\.0\.0\.1|\[::1\])(?::([1-9]\d{0,4}))?$/i;
20523
20387
  var runtimeConfig = null;
20524
20388
  var restartScheduled = false;
20525
20389
  function setDashboardRuntimeConfig(config) {
@@ -20546,10 +20410,13 @@ function resolveDashboardConfigState(runtimeAvailable, restartRequired, managedR
20546
20410
  function isSafeDashboardRestartTarget(diagnostic, currentPid) {
20547
20411
  return diagnostic.pm2Installed && diagnostic.processFound && diagnostic.status === "online" && diagnostic.pid === currentPid && diagnostic.ready && diagnostic.scopeMatches;
20548
20412
  }
20549
- function canRestartCurrentGateway() {
20413
+ async function canRestartCurrentGateway(fresh = false) {
20550
20414
  if (runtimeConfig === null) return false;
20551
20415
  try {
20552
- return isSafeDashboardRestartTarget(getGatewayDiagnostic(), process.pid);
20416
+ return isSafeDashboardRestartTarget(
20417
+ await getDashboardGatewayDiagnostic({ fresh }),
20418
+ process.pid
20419
+ );
20553
20420
  } catch {
20554
20421
  return false;
20555
20422
  }
@@ -20568,7 +20435,7 @@ function listChildDirectories(path) {
20568
20435
  if (entry.isDirectory()) return [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }];
20569
20436
  if (!entry.isSymbolicLink()) return [];
20570
20437
  try {
20571
- return statSync4(entryPath).isDirectory() ? [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }] : [];
20438
+ return statSync3(entryPath).isDirectory() ? [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }] : [];
20572
20439
  } catch {
20573
20440
  return [];
20574
20441
  }
@@ -20621,6 +20488,7 @@ function parseTaskPayload(value) {
20621
20488
  cwd: requiredString("cwd"),
20622
20489
  agent: requiredString("agent"),
20623
20490
  model: optionalString("model", "default"),
20491
+ variant: Object.hasOwn(input, "variant") ? optionalString("variant") : void 0,
20624
20492
  prompt: requiredString("prompt"),
20625
20493
  category: optionalString("category", "general"),
20626
20494
  batchId: optionalString("batchId"),
@@ -20636,6 +20504,7 @@ function editableTaskPayload(input) {
20636
20504
  name: input.name,
20637
20505
  agent: input.agent,
20638
20506
  model: input.model ?? "default",
20507
+ ...input.variant === void 0 ? {} : { variant: input.variant },
20639
20508
  prompt: input.prompt,
20640
20509
  category: input.category ?? "general",
20641
20510
  batchId: input.batchId ?? null,
@@ -20691,6 +20560,7 @@ function parseTemplatePayload(value) {
20691
20560
  name: requiredString("name"),
20692
20561
  agent: requiredString("agent"),
20693
20562
  model: optionalString("model", "default"),
20563
+ variant: Object.hasOwn(input, "variant") ? optionalString("variant") : void 0,
20694
20564
  prompt: requiredString("prompt"),
20695
20565
  cwd: requiredString("cwd"),
20696
20566
  category: optionalString("category", "general"),
@@ -20726,6 +20596,14 @@ function resolveLocale(c) {
20726
20596
  return accepted.startsWith("en") ? "en" : "zh-CN";
20727
20597
  }
20728
20598
  app.use("*", async (c, next) => {
20599
+ const requestHostname = new URL(c.req.url).hostname;
20600
+ const hostHeader = c.req.header("Host");
20601
+ const loopbackHosts = ["localhost", "127.0.0.1", "[::1]"];
20602
+ const hostMatch = hostHeader?.match(LOOPBACK_HOST_PATTERN) ?? null;
20603
+ const hostPort = hostMatch?.[1] === void 0 ? null : Number(hostMatch[1]);
20604
+ if (!loopbackHosts.includes(requestHostname) || hostHeader !== void 0 && (!hostMatch || hostPort !== null && hostPort > 65535)) {
20605
+ return c.json({ error: "invalid dashboard host" }, 421);
20606
+ }
20729
20607
  await next();
20730
20608
  c.header("X-Content-Type-Options", "nosniff");
20731
20609
  c.header("X-Frame-Options", "DENY");
@@ -20757,13 +20635,13 @@ app.get("/health", (c) => {
20757
20635
  return c.json(health, health.status === "ok" ? 200 : 503);
20758
20636
  });
20759
20637
  app.get("/api/filesystem/directories", (c) => {
20760
- const requestedPath = c.req.query("path")?.trim() || homedir4();
20638
+ const requestedPath = c.req.query("path")?.trim() || homedir3();
20761
20639
  try {
20762
20640
  validateTaskWorkingDirectory(requestedPath);
20763
20641
  return c.json({
20764
20642
  path: requestedPath,
20765
20643
  parent: dirname4(requestedPath),
20766
- home: homedir4(),
20644
+ home: homedir3(),
20767
20645
  directories: listChildDirectories(requestedPath)
20768
20646
  });
20769
20647
  } catch (error) {
@@ -20861,7 +20739,7 @@ function readCurrentConfig() {
20861
20739
  const configPath = getConfigPath();
20862
20740
  if (!existsSync5(configPath)) return {};
20863
20741
  try {
20864
- return JSON.parse(readFileSync3(configPath, "utf-8"));
20742
+ return JSON.parse(readFileSync2(configPath, "utf-8"));
20865
20743
  } catch {
20866
20744
  return {};
20867
20745
  }
@@ -20869,9 +20747,9 @@ function readCurrentConfig() {
20869
20747
  function writeConfig(cfg) {
20870
20748
  const configPath = getConfigPath();
20871
20749
  const dir = dirname4(configPath);
20872
- if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true });
20750
+ if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
20873
20751
  const tempPath = `${configPath}.${process.pid}.tmp`;
20874
- writeFileSync3(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
20752
+ writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
20875
20753
  renameSync2(tempPath, configPath);
20876
20754
  }
20877
20755
  function statCard(value, label, tone, cardIcon, delay = "") {
@@ -20992,7 +20870,7 @@ app.get("/", async (c) => {
20992
20870
  const latestRun = latestRuns.get(task.id);
20993
20871
  const executionActive = latestRun?.status === "running";
20994
20872
  const batchId = task.batchId?.trim() || null;
20995
- const searchable = esc(`${task.name} ${task.agent} ${task.prompt} ${task.cwd ?? ""} ${task.batchId ?? ""} ${task.category ?? ""}`);
20873
+ const searchable = esc(`${task.name} ${task.agent} ${task.model ?? ""} ${task.variant ?? ""} ${task.prompt} ${task.cwd ?? ""} ${task.batchId ?? ""} ${task.category ?? ""}`);
20996
20874
  return `<tr data-task-row data-search="${searchable}">
20997
20875
  <td class="faint" data-label="${t(locale, "table.id")}">#${task.id}</td>
20998
20876
  <td data-primary data-label="${t(locale, "table.task")}"><div class="task-name">${esc(task.name)}</div><div class="task-prompt" title="${esc(task.prompt)}">${esc(task.prompt.substring(0, 160))}</div>
@@ -21070,11 +20948,12 @@ app.get("/", async (c) => {
21070
20948
  <label class="form-field"><span>${t(locale, "template.cwd")}</span><div class="field-action"><input id="task-cwd" required autocomplete="off" list="task-cwd-options" placeholder="/path/to/project" oninput="updateTaskProjectStatus();scheduleCatalogLoad('task')"><button id="task-cwd-picker" type="button" class="btn" onclick="openDirectoryPicker('task-cwd')">${icon("folder")}${t(locale, "action.chooseFolder")}</button></div><small>${t(locale, "template.cwdHint")}</small></label>
21071
20949
  <datalist id="task-cwd-options">${projects.map((project) => `<option value="${esc(project.cwd)}"></option>`).join("")}</datalist>
21072
20950
  <label class="form-field"><span>${t(locale, "template.agent")}</span><select id="task-agent" required><option value="">${t(locale, "catalog.chooseProject")}</option></select><small>${t(locale, "catalog.agentHint")}</small></label>
21073
- <label class="form-field"><span>${t(locale, "template.model")}</span><div class="model-selector"><select id="task-model-provider" aria-label="${t(locale, "catalog.provider")}" onchange="populateModelOptions('task')"><option value="">${t(locale, "catalog.defaultProvider")}</option></select><select id="task-model" required aria-label="${t(locale, "catalog.model")}" disabled><option value="default">${t(locale, "catalog.defaultModel")}</option></select></div><small>${t(locale, "catalog.modelHint")}</small></label>
20951
+ <label class="form-field"><span>${t(locale, "template.model")}</span><div class="model-selector"><select id="task-model-provider" aria-label="${t(locale, "catalog.provider")}" onchange="handleProviderChange('task')"><option value="">${t(locale, "catalog.defaultProvider")}</option></select><select id="task-model" required aria-label="${t(locale, "catalog.model")}" onchange="handleModelChange('task')" disabled><option value="default">${t(locale, "catalog.defaultModel")}</option></select></div><small>${t(locale, "catalog.modelHint")}</small></label>
20952
+ <label class="form-field"><span>${t(locale, "template.variant")}</span><select id="task-variant" onchange="handleVariantChange('task')" disabled><option value="">${t(locale, "catalog.defaultVariant")}</option></select><small>${t(locale, "catalog.variantHint")}</small></label>
21074
20953
  <label class="form-field form-field-wide"><span>${t(locale, "template.prompt")}</span><textarea id="task-prompt" rows="6" required></textarea></label>
21075
20954
  </div>
21076
20955
  <p id="task-project-status" class="form-note"></p>
21077
- <p id="task-catalog-status" class="form-note catalog-status"></p>
20956
+ <p id="task-catalog-status" class="form-note catalog-status" role="status" aria-live="polite"></p>
21078
20957
  <details class="advanced-fields">
21079
20958
  <summary>${t(locale, "template.advanced")}</summary>
21080
20959
  <div class="template-form-grid">
@@ -21116,7 +20995,7 @@ app.get("/templates", async (c) => {
21116
20995
  return `<tr>
21117
20996
  <td class="faint" data-label="${t(locale, "table.id")}">#${template.id}</td>
21118
20997
  <td data-primary data-label="${t(locale, "table.name")}"><div class="task-name">${esc(template.name)}</div><div class="task-prompt" title="${esc(template.prompt)}">${esc(template.prompt.substring(0, 140))}</div>
21119
- <div class="actions" style="margin-top:5px"><span class="tag">${esc(template.agent)}</span>${template.model && template.model !== "default" ? `<span class="tag">${esc(template.model)}</span>` : ""}</div></td>
20998
+ <div class="actions" style="margin-top:5px"><span class="tag">${esc(template.agent)}</span>${template.model && template.model !== "default" ? `<span class="tag">${esc(template.model)}</span>` : ""}${template.variant ? `<span class="tag">${esc(template.variant)}</span>` : ""}</div></td>
21120
20999
  <td data-label="${t(locale, "table.type")}"><span class="tag t-${scheduleType}">${typeLabel}</span></td>
21121
21000
  <td data-label="${t(locale, "table.rule")}" class="m small">${esc(rule)}</td>
21122
21001
  <td data-label="${t(locale, "table.status")}"><span class="badge ${template.enabled ? "b-done" : "b-cancelled"}">${t(locale, template.enabled ? "schedule.enabled" : "schedule.disabled")}</span></td>
@@ -21146,14 +21025,15 @@ app.get("/templates", async (c) => {
21146
21025
  <label class="form-field"><span>${t(locale, "template.name")}</span><input id="template-name" required maxlength="200" autocomplete="off"></label>
21147
21026
  <label class="form-field"><span>${t(locale, "template.cwd")}</span><div class="field-action"><input id="template-cwd" required autocomplete="off" placeholder="/path/to/project" oninput="scheduleCatalogLoad('template')"><button type="button" class="btn" onclick="openDirectoryPicker('template-cwd')">${icon("folder")}${t(locale, "action.chooseFolder")}</button></div><small>${t(locale, "template.cwdHint")}</small></label>
21148
21027
  <label class="form-field"><span>${t(locale, "template.agent")}</span><select id="template-agent" required><option value="">${t(locale, "catalog.chooseProject")}</option></select><small>${t(locale, "catalog.agentHint")}</small></label>
21149
- <label class="form-field"><span>${t(locale, "template.model")}</span><div class="model-selector"><select id="template-model-provider" aria-label="${t(locale, "catalog.provider")}" onchange="populateModelOptions('template')"><option value="">${t(locale, "catalog.defaultProvider")}</option></select><select id="template-model" required aria-label="${t(locale, "catalog.model")}" disabled><option value="default">${t(locale, "catalog.defaultModel")}</option></select></div><small>${t(locale, "catalog.modelHint")}</small></label>
21028
+ <label class="form-field"><span>${t(locale, "template.model")}</span><div class="model-selector"><select id="template-model-provider" aria-label="${t(locale, "catalog.provider")}" onchange="handleProviderChange('template')"><option value="">${t(locale, "catalog.defaultProvider")}</option></select><select id="template-model" required aria-label="${t(locale, "catalog.model")}" onchange="handleModelChange('template')" disabled><option value="default">${t(locale, "catalog.defaultModel")}</option></select></div><small>${t(locale, "catalog.modelHint")}</small></label>
21029
+ <label class="form-field"><span>${t(locale, "template.variant")}</span><select id="template-variant" onchange="handleVariantChange('template')" disabled><option value="">${t(locale, "catalog.defaultVariant")}</option></select><small>${t(locale, "catalog.variantHint")}</small></label>
21150
21030
  <label class="form-field form-field-wide"><span>${t(locale, "template.prompt")}</span><textarea id="template-prompt" rows="6" required></textarea></label>
21151
21031
  <label class="form-field"><span>${t(locale, "template.scheduleType")}</span><select id="template-schedule-type" onchange="updateTemplateScheduleFields()"><option value="recurring">${t(locale, "schedule.recurring")}</option><option value="delayed">${t(locale, "schedule.delayed")}</option><option value="cron">${t(locale, "schedule.cron")}</option></select></label>
21152
21032
  <label id="template-cron-field" class="form-field" hidden><span>${t(locale, "template.cronExpr")}</span><input id="template-cron" autocomplete="off" placeholder="0 9 * * *"><small>${t(locale, "template.cronHint")}</small></label>
21153
21033
  <label id="template-interval-field" class="form-field"><span>${t(locale, "template.interval")}</span>${durationControl(locale, "template-interval", 36e5, "interval")}<small>${t(locale, "template.intervalHint")}</small></label>
21154
21034
  <label id="template-run-at-field" class="form-field" hidden><span>${t(locale, "template.runAt")}</span><input id="template-run-at" type="datetime-local" step="0.001"></label>
21155
21035
  </div>
21156
- <p id="template-catalog-status" class="form-note catalog-status"></p>
21036
+ <p id="template-catalog-status" class="form-note catalog-status" role="status" aria-live="polite"></p>
21157
21037
  <details class="advanced-fields">
21158
21038
  <summary>${t(locale, "template.advanced")}</summary>
21159
21039
  <div class="template-form-grid">
@@ -21187,6 +21067,7 @@ app.get("/runs", async (c) => {
21187
21067
  taskId: taskRuns4.taskId,
21188
21068
  sessionId: taskRuns4.sessionId,
21189
21069
  model: taskRuns4.model,
21070
+ variant: taskRuns4.variant,
21190
21071
  status: taskRuns4.status,
21191
21072
  startedAt: taskRuns4.startedAt,
21192
21073
  finishedAt: taskRuns4.finishedAt,
@@ -21207,7 +21088,7 @@ app.get("/runs", async (c) => {
21207
21088
  const log = run.log ? renderRunLog(run.id, run.taskName, run.log, locale, run.status !== "done") : "";
21208
21089
  return `<tr class="run-summary-row">
21209
21090
  <td class="faint" data-label="${t(locale, "table.run")}">#${run.id}</td>
21210
- <td data-primary data-label="${t(locale, "table.task")}"><div class="task-name">${esc(run.taskName)} <span class="faint">#${run.taskId}</span></div>${run.model ? `<div style="margin-top:4px"><span class="tag">${esc(run.model)}</span></div>` : ""}</td>
21091
+ <td data-primary data-label="${t(locale, "table.task")}"><div class="task-name">${esc(run.taskName)} <span class="faint">#${run.taskId}</span></div>${run.model || run.variant ? `<div class="actions" style="margin-top:4px">${run.model ? `<span class="tag">${esc(run.model)}</span>` : ""}${run.variant ? `<span class="tag">${esc(run.variant)}</span>` : ""}</div>` : ""}</td>
21211
21092
  <td data-label="${t(locale, "table.agent")}"><span class="tag">${esc(run.taskAgent)}</span></td>
21212
21093
  <td data-label="${t(locale, "table.session")}" class="m small">${esc(maskSessionId(run.sessionId))}</td>
21213
21094
  <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${runStatusText(locale, run.status ?? "unknown")}</span></td>
@@ -21234,7 +21115,7 @@ app.get("/system", async (c) => {
21234
21115
  const config = loadConfig();
21235
21116
  const activeConfig = runtimeConfig ?? config;
21236
21117
  const restartRequired = runtimeConfig !== null && !configsEqual(config, runtimeConfig);
21237
- const managedRestart = canRestartCurrentGateway();
21118
+ const managedRestart = await canRestartCurrentGateway();
21238
21119
  const configState = resolveDashboardConfigState(
21239
21120
  runtimeConfig !== null,
21240
21121
  restartRequired,
@@ -21257,7 +21138,7 @@ app.get("/system", async (c) => {
21257
21138
  const runRows = runningRuns.map((run) => {
21258
21139
  const session = maskSessionId(run.sessionId);
21259
21140
  return `<tr><td class="faint" data-label="${t(locale, "table.run")}">#${run.id}</td><td data-primary data-label="${t(locale, "table.task")}">#${run.taskId}</td><td data-label="${t(locale, "table.session")}" class="m small">${esc(session)}</td>
21260
- <td data-label="${t(locale, "table.model")}" class="small">${esc(run.model) || "\u2014"}</td><td data-label="${t(locale, "table.startedAt")}" class="small">${formatDateTime(run.startedAt, locale)}</td>
21141
+ <td data-label="${t(locale, "table.model")}" class="small">${esc(run.model) || "\u2014"}${run.variant ? ` \xB7 ${esc(run.variant)}` : ""}</td><td data-label="${t(locale, "table.startedAt")}" class="small">${formatDateTime(run.startedAt, locale)}</td>
21261
21142
  <td data-label="${t(locale, "table.heartbeat")}" class="small muted">${formatRelative(run.heartbeatAt, locale)}</td><td data-label="${t(locale, "table.pid")}" class="m small">W:${run.workerPid ?? "\u2014"} C:${run.childPid ?? "\u2014"}</td>
21262
21143
  <td data-label="${t(locale, "table.duration")}" class="small">${formatDuration(run.startedAt, null)}</td></tr>`;
21263
21144
  }).join("");
@@ -21354,9 +21235,9 @@ app.get("/api/runs/:id/session-command", async (c) => {
21354
21235
  if (!isValidSessionId(run.sessionId)) return c.json({ error: "session unavailable" }, 409);
21355
21236
  return c.json({ command: sessionCommand(run.sessionId) });
21356
21237
  });
21357
- app.get("/api/gateway/status", (c) => {
21238
+ app.get("/api/gateway/status", async (c) => {
21358
21239
  const savedConfig = loadConfig();
21359
- const managed = canRestartCurrentGateway();
21240
+ const managed = await canRestartCurrentGateway();
21360
21241
  return c.json({
21361
21242
  pid: process.pid,
21362
21243
  managed,
@@ -21481,7 +21362,7 @@ app.put("/api/config", async (c) => {
21481
21362
  return c.json({
21482
21363
  success: true,
21483
21364
  restartRequired: runtimeConfig === null || !configsEqual(savedConfig, runtimeConfig),
21484
- managed: canRestartCurrentGateway()
21365
+ managed: await canRestartCurrentGateway()
21485
21366
  });
21486
21367
  } catch (error) {
21487
21368
  return c.json({
@@ -21497,9 +21378,10 @@ app.post("/api/gateway/restart", async (c) => {
21497
21378
  return c.json({ error: "confirmation must be RESTART" }, 400);
21498
21379
  }
21499
21380
  if (restartScheduled) return c.json({ error: "restart already scheduled" }, 409);
21500
- if (!canRestartCurrentGateway()) {
21381
+ if (!await canRestartCurrentGateway(true)) {
21501
21382
  return c.json({ error: "\u5F53\u524D Gateway \u4E0D\u662F\u7531\u5339\u914D\u8FD0\u884C\u4F5C\u7528\u57DF\u7684 PM2 \u8FDB\u7A0B\u6258\u7BA1\uFF0C\u65E0\u6CD5\u4ECE\u7F51\u9875\u5B89\u5168\u91CD\u542F" }, 409);
21502
21383
  }
21384
+ if (restartScheduled) return c.json({ error: "restart already scheduled" }, 409);
21503
21385
  restartScheduled = true;
21504
21386
  const previousPid = process.pid;
21505
21387
  setTimeout(() => {