opencode-supertask 0.1.31 → 0.1.32-rc.1

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: readFileSync3 } = __require("fs");
9104
- const data = readFileSync3(filePath, "utf8");
9103
+ const { readFileSync: readFileSync4 } = __require("fs");
9104
+ const data = readFileSync4(filePath, "utf8");
9105
9105
  return _CronFileParser.#parseContent(data);
9106
9106
  }
9107
9107
  /**
@@ -12224,7 +12224,7 @@ function sql(strings, ...params) {
12224
12224
  return new SQL([new StringChunk(str)]);
12225
12225
  }
12226
12226
  sql2.raw = raw2;
12227
- function join3(chunks, separator) {
12227
+ function join4(chunks, separator) {
12228
12228
  const result = [];
12229
12229
  for (const [i, chunk] of chunks.entries()) {
12230
12230
  if (i > 0 && separator !== void 0) {
@@ -12234,7 +12234,7 @@ function sql(strings, ...params) {
12234
12234
  }
12235
12235
  return new SQL(result);
12236
12236
  }
12237
- sql2.join = join3;
12237
+ sql2.join = join4;
12238
12238
  function identifier(value) {
12239
12239
  return new Name(value);
12240
12240
  }
@@ -13083,8 +13083,8 @@ function mapRelationalRow(tablesConfig, tableConfig, row, buildQueryResultSelect
13083
13083
  }
13084
13084
 
13085
13085
  // src/web/index.tsx
13086
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
13087
- import { dirname as dirname3 } from "path";
13086
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
13087
+ import { basename as basename2, dirname as dirname4 } from "path";
13088
13088
 
13089
13089
  // src/core/db/index.ts
13090
13090
  import { Database as Database2 } from "bun:sqlite";
@@ -14571,7 +14571,7 @@ var SQLiteSelectQueryBuilderBase = class extends TypedQueryBuilder {
14571
14571
  return (table, on) => {
14572
14572
  const baseTableName = this.tableName;
14573
14573
  const tableName = getTableLikeName(table);
14574
- if (typeof tableName === "string" && this.config.joins?.some((join3) => join3.alias === tableName)) {
14574
+ if (typeof tableName === "string" && this.config.joins?.some((join4) => join4.alias === tableName)) {
14575
14575
  throw new Error(`Alias "${tableName}" is already used in this query`);
14576
14576
  }
14577
14577
  if (!this.isPartialSelect) {
@@ -15384,7 +15384,7 @@ var SQLiteUpdateBase = class extends QueryPromise {
15384
15384
  createJoin(joinType) {
15385
15385
  return (table, on) => {
15386
15386
  const tableName = getTableLikeName(table);
15387
- if (typeof tableName === "string" && this.config.joins.some((join3) => join3.alias === tableName)) {
15387
+ if (typeof tableName === "string" && this.config.joins.some((join4) => join4.alias === tableName)) {
15388
15388
  throw new Error(`Alias "${tableName}" is already used in this query`);
15389
15389
  }
15390
15390
  if (typeof on === "function") {
@@ -16490,8 +16490,8 @@ var _sqlite = null;
16490
16490
  var _db = null;
16491
16491
  var _migrationRan = false;
16492
16492
  function getMigrationsFolder() {
16493
- const __dirname = dirname(fileURLToPath(import.meta.url));
16494
- let dir = __dirname;
16493
+ const __dirname2 = dirname(fileURLToPath(import.meta.url));
16494
+ let dir = __dirname2;
16495
16495
  for (let i = 0; i < 5; i++) {
16496
16496
  const candidate = join(dir, "drizzle", "meta", "_journal.json");
16497
16497
  if (existsSync(candidate)) {
@@ -16499,7 +16499,7 @@ function getMigrationsFolder() {
16499
16499
  }
16500
16500
  dir = dirname(dir);
16501
16501
  }
16502
- return join(__dirname, "../../drizzle");
16502
+ return join(__dirname2, "../../drizzle");
16503
16503
  }
16504
16504
  function ensureGatewayLock(sqliteDb) {
16505
16505
  sqliteDb.exec(`
@@ -16582,6 +16582,35 @@ var sqlite = new Proxy({}, {
16582
16582
  }
16583
16583
  });
16584
16584
 
16585
+ // src/core/duration.ts
16586
+ var DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|sec|seconds?|min|minutes?|m|h|hours?|d|days?|w|weeks?)$/i;
16587
+ var ISO8601_REGEX = /^P(?:([.\d]+)D)?(?:T(?:([.\d]+)H)?(?:([.\d]+)M)?(?:([.\d]+)S)?)?$/i;
16588
+ function parseDuration(input) {
16589
+ const trimmed = input.trim();
16590
+ const simple = DURATION_REGEX.exec(trimmed);
16591
+ if (simple) {
16592
+ const value = parseFloat(simple[1]);
16593
+ const unit = simple[2].toLowerCase();
16594
+ if (unit === "ms") return value;
16595
+ if (unit === "s" || unit === "sec" || unit === "second" || unit === "seconds") return value * 1e3;
16596
+ if (unit === "min" || unit === "minute" || unit === "minutes" || unit === "m") return value * 6e4;
16597
+ if (unit === "h" || unit === "hour" || unit === "hours") return value * 36e5;
16598
+ if (unit === "d" || unit === "day" || unit === "days") return value * 864e5;
16599
+ if (unit === "w" || unit === "week" || unit === "weeks") return value * 6048e5;
16600
+ }
16601
+ const iso = ISO8601_REGEX.exec(trimmed);
16602
+ if (iso) {
16603
+ const days = parseFloat(iso[1] ?? "0");
16604
+ const hours = parseFloat(iso[2] ?? "0");
16605
+ const minutes = parseFloat(iso[3] ?? "0");
16606
+ const seconds = parseFloat(iso[4] ?? "0");
16607
+ return (days * 86400 + hours * 3600 + minutes * 60 + seconds) * 1e3;
16608
+ }
16609
+ const asNumber = Number(trimmed);
16610
+ if (!isNaN(asNumber) && asNumber > 0) return asNumber;
16611
+ return null;
16612
+ }
16613
+
16585
16614
  // src/core/services/database-maintenance.service.ts
16586
16615
  import { Database as Database3 } from "bun:sqlite";
16587
16616
  import { randomUUID } from "crypto";
@@ -17215,6 +17244,42 @@ function computeBackoff(retryCount, baseMs = 3e4, maxMs = MAX_BACKOFF_MS) {
17215
17244
  return Math.min(baseMs * Math.pow(2, retryCount - 1), maxMs);
17216
17245
  }
17217
17246
 
17247
+ // src/core/task-working-directory.ts
17248
+ import { statSync as statSync2 } from "fs";
17249
+ import { isAbsolute } from "path";
17250
+ var InvalidTaskWorkingDirectoryError = class extends Error {
17251
+ constructor(message) {
17252
+ super(message);
17253
+ this.name = "InvalidTaskWorkingDirectoryError";
17254
+ }
17255
+ };
17256
+ function validateTaskWorkingDirectory(cwd) {
17257
+ if (cwd == null) return;
17258
+ if (!cwd.trim()) {
17259
+ throw new InvalidTaskWorkingDirectoryError("\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
17260
+ }
17261
+ if (!isAbsolute(cwd)) {
17262
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF1A${cwd}`);
17263
+ }
17264
+ let stat;
17265
+ try {
17266
+ stat = statSync2(cwd);
17267
+ } catch (error) {
17268
+ const detail = error instanceof Error ? error.message : String(error);
17269
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BBF\u95EE\uFF1A${cwd}\uFF08${detail}\uFF09`);
17270
+ }
17271
+ if (!stat.isDirectory()) {
17272
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u76EE\u5F55\uFF1A${cwd}`);
17273
+ }
17274
+ }
17275
+
17276
+ // src/core/task-batch.ts
17277
+ var TASK_BATCH_TRIM_CHARACTERS = " \n\v\f\r\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF";
17278
+ function normalizeTaskBatchId(batchId) {
17279
+ if (batchId == null) return batchId;
17280
+ return batchId.trim() || null;
17281
+ }
17282
+
17218
17283
  // src/core/services/task.service.ts
17219
17284
  var { tasks: tasks3, taskRuns: taskRuns3 } = schema_exports;
17220
17285
  var cleanupInvocation = 0;
@@ -17278,34 +17343,75 @@ var TaskService = class {
17278
17343
  return conditions;
17279
17344
  }
17280
17345
  static async add(data) {
17281
- this.validateNewTask(data);
17346
+ const normalizedData = {
17347
+ ...data,
17348
+ batchId: normalizeTaskBatchId(data.batchId)
17349
+ };
17350
+ this.validateNewTask(normalizedData);
17282
17351
  return db.transaction((tx) => {
17283
- if (data.dependsOn != null) {
17352
+ if (normalizedData.dependsOn != null) {
17284
17353
  const dependency = tx.select({
17285
17354
  id: tasks3.id,
17286
17355
  cwd: tasks3.cwd,
17287
17356
  status: tasks3.status,
17288
17357
  retryCount: tasks3.retryCount,
17289
17358
  maxRetries: tasks3.maxRetries
17290
- }).from(tasks3).where(eq(tasks3.id, data.dependsOn)).get();
17359
+ }).from(tasks3).where(eq(tasks3.id, normalizedData.dependsOn)).get();
17291
17360
  if (!dependency) {
17292
- throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${data.dependsOn} \u4E0D\u5B58\u5728`);
17361
+ throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${normalizedData.dependsOn} \u4E0D\u5B58\u5728`);
17293
17362
  }
17294
- if ((dependency.cwd ?? null) !== (data.cwd ?? null)) {
17363
+ if ((dependency.cwd ?? null) !== (normalizedData.cwd ?? null)) {
17295
17364
  throw new Error("dependsOn \u5FC5\u987B\u6307\u5411\u540C\u4E00 cwd \u7684\u4EFB\u52A1");
17296
17365
  }
17297
17366
  const dependencyIsRecoverable = dependency.status === "pending" || dependency.status === "running" || dependency.status === "done" || dependency.status === "failed" && (dependency.retryCount ?? 0) <= (dependency.maxRetries ?? 3);
17298
17367
  if (!dependencyIsRecoverable) {
17299
- throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${data.dependsOn} \u5DF2\u8FDB\u5165\u4E0D\u53EF\u6062\u590D\u7EC8\u6001`);
17368
+ throw new Error(`dependsOn \u6307\u5411\u7684\u4EFB\u52A1 #${normalizedData.dependsOn} \u5DF2\u8FDB\u5165\u4E0D\u53EF\u6062\u590D\u7EC8\u6001`);
17300
17369
  }
17301
17370
  }
17302
- return tx.insert(tasks3).values(data).returning().get();
17371
+ return tx.insert(tasks3).values(normalizedData).returning().get();
17372
+ }, { behavior: "immediate" });
17373
+ }
17374
+ static async update(id, data, scope = {}) {
17375
+ if (Object.keys(data).length === 0) throw new Error("\u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A\u8981\u4FEE\u6539\u7684\u5B57\u6BB5");
17376
+ const normalizedData = data.batchId === void 0 ? data : { ...data, batchId: normalizeTaskBatchId(data.batchId) ?? null };
17377
+ return db.transaction((tx) => {
17378
+ const task = tx.select().from(tasks3).where(and(
17379
+ eq(tasks3.id, id),
17380
+ sql`${tasks3.status} IN ('pending', 'failed', 'dead_letter')`,
17381
+ ...this.buildScopeWhere(scope)
17382
+ )).get();
17383
+ if (!task) return null;
17384
+ this.validateNewTask({
17385
+ name: normalizedData.name ?? task.name,
17386
+ agent: normalizedData.agent ?? task.agent,
17387
+ model: normalizedData.model ?? task.model,
17388
+ prompt: normalizedData.prompt ?? task.prompt,
17389
+ cwd: task.cwd,
17390
+ category: normalizedData.category ?? task.category,
17391
+ importance: normalizedData.importance ?? task.importance,
17392
+ urgency: normalizedData.urgency ?? task.urgency,
17393
+ batchId: normalizedData.batchId === void 0 ? task.batchId : normalizedData.batchId,
17394
+ maxRetries: normalizedData.maxRetries ?? task.maxRetries,
17395
+ retryBackoffMs: normalizedData.retryBackoffMs ?? task.retryBackoffMs,
17396
+ timeoutMs: normalizedData.timeoutMs === void 0 ? task.timeoutMs : normalizedData.timeoutMs,
17397
+ dependsOn: task.dependsOn
17398
+ });
17399
+ const maxRetries = normalizedData.maxRetries ?? task.maxRetries ?? 3;
17400
+ const exhausted = task.status === "failed" && (task.retryCount ?? 0) > maxRetries;
17401
+ return tx.update(tasks3).set({
17402
+ ...normalizedData,
17403
+ ...exhausted ? {
17404
+ status: "dead_letter",
17405
+ retryAfter: null
17406
+ } : {}
17407
+ }).where(eq(tasks3.id, id)).returning().get() ?? null;
17303
17408
  }, { behavior: "immediate" });
17304
17409
  }
17305
17410
  static validateNewTask(data) {
17306
17411
  if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
17307
17412
  if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
17308
17413
  if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
17414
+ validateTaskWorkingDirectory(data.cwd);
17309
17415
  this.validateInteger("importance", data.importance, 1, 5);
17310
17416
  this.validateInteger("urgency", data.urgency, 1, 5);
17311
17417
  this.validateInteger("maxRetries", data.maxRetries, 0, 1e3);
@@ -17326,12 +17432,14 @@ var TaskService = class {
17326
17432
  isNull(tasks3.retryAfter),
17327
17433
  sql`${tasks3.retryAfter} <= ${nowMs}`
17328
17434
  );
17329
- const hasExcludedBatches = scope.excludedBatchIds && scope.excludedBatchIds.length > 0;
17435
+ const excludedBatchIds = [...new Set((scope.excludedBatchIds ?? []).map((batchId) => normalizeTaskBatchId(batchId)).filter((batchId) => Boolean(batchId)))];
17436
+ const hasExcludedBatches = excludedBatchIds.length > 0;
17330
17437
  let batchFilter;
17331
17438
  if (hasExcludedBatches) {
17332
17439
  batchFilter = or(
17333
17440
  isNull(tasks3.batchId),
17334
- sql`${tasks3.batchId} NOT IN ${scope.excludedBatchIds}`
17441
+ sql`trim(${tasks3.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
17442
+ sql`trim(${tasks3.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) NOT IN ${excludedBatchIds}`
17335
17443
  );
17336
17444
  }
17337
17445
  const statusConditions = or(
@@ -17365,9 +17473,11 @@ var TaskService = class {
17365
17473
  ),
17366
17474
  or(
17367
17475
  isNull(tasks3.batchId),
17476
+ sql`trim(${tasks3.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
17368
17477
  sql`NOT EXISTS (
17369
17478
  SELECT 1 FROM tasks AS running_batch_task
17370
- WHERE running_batch_task.batch_id = ${tasks3.batchId}
17479
+ WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
17480
+ = trim(${tasks3.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
17371
17481
  AND (
17372
17482
  running_batch_task.status = 'running'
17373
17483
  OR EXISTS (
@@ -17391,12 +17501,18 @@ var TaskService = class {
17391
17501
  ).limit(1);
17392
17502
  return result[0] ?? null;
17393
17503
  }
17394
- static async countRunning() {
17504
+ static async countRunning(scope = {}) {
17505
+ const scopeConditions = scope.legacyCwd ? [sql`${tasks3.cwd} IS NULL OR trim(${tasks3.cwd}) = ''`] : this.buildScopeWhere(scope);
17506
+ if (scope.batchId !== void 0) {
17507
+ const batchId = normalizeTaskBatchId(scope.batchId);
17508
+ scopeConditions.push(batchId ? sql`trim(${tasks3.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ${batchId}` : sql`0`);
17509
+ }
17395
17510
  return db.transaction((tx) => {
17396
- const runningTasks = tx.select({ count: sql`count(*)` }).from(tasks3).where(eq(tasks3.status, "running")).get();
17511
+ const runningTasks = tx.select({ count: sql`count(*)` }).from(tasks3).where(and(eq(tasks3.status, "running"), ...scopeConditions)).get();
17397
17512
  const runsWithoutRunningTask = tx.select({ count: sql`count(DISTINCT ${taskRuns3.taskId})` }).from(taskRuns3).innerJoin(tasks3, eq(tasks3.id, taskRuns3.taskId)).where(and(
17398
17513
  eq(taskRuns3.status, "running"),
17399
- sql`${tasks3.status} <> 'running'`
17514
+ sql`${tasks3.status} <> 'running'`,
17515
+ ...scopeConditions
17400
17516
  )).get();
17401
17517
  return Number(runningTasks?.count ?? 0) + Number(runsWithoutRunningTask?.count ?? 0);
17402
17518
  }, { behavior: "deferred" });
@@ -17729,15 +17845,17 @@ var TaskService = class {
17729
17845
  }).where(and(...conditions, hasViableDependency())).returning().get() ?? null, { behavior: "immediate" });
17730
17846
  }
17731
17847
  static async retryBatch(batchId, scope = {}) {
17848
+ const normalizedBatchId = normalizeTaskBatchId(batchId);
17849
+ if (!normalizedBatchId) return 0;
17732
17850
  const sqlite2 = getSqlite();
17733
17851
  const scopeFilter = scope.cwd === void 0 ? "" : "AND candidate.cwd = ?";
17734
- const parameters = scope.cwd === void 0 ? [batchId] : [batchId, scope.cwd];
17852
+ const parameters = scope.cwd === void 0 ? [TASK_BATCH_TRIM_CHARACTERS, normalizedBatchId] : [TASK_BATCH_TRIM_CHARACTERS, normalizedBatchId, scope.cwd];
17735
17853
  return db.transaction(() => sqlite2.query(`
17736
17854
  WITH RECURSIVE
17737
17855
  candidate(id, cwd, depends_on) AS MATERIALIZED (
17738
17856
  SELECT candidate.id, candidate.cwd, candidate.depends_on
17739
17857
  FROM tasks AS candidate
17740
- WHERE candidate.batch_id = ?
17858
+ WHERE trim(candidate.batch_id, ?) = ?
17741
17859
  AND candidate.status IN ('failed', 'dead_letter')
17742
17860
  ${scopeFilter}
17743
17861
  ),
@@ -17791,16 +17909,28 @@ var TaskService = class {
17791
17909
  static async list(options = {}) {
17792
17910
  let query = db.select().from(tasks3).$dynamic();
17793
17911
  const conditions = [];
17794
- if (options.status) {
17912
+ if (options.activeExecution) {
17913
+ conditions.push(or(
17914
+ eq(tasks3.status, "running"),
17915
+ sql`EXISTS (
17916
+ SELECT 1 FROM task_runs AS active_list_run
17917
+ WHERE active_list_run.task_id = ${tasks3.id}
17918
+ AND active_list_run.status = 'running'
17919
+ )`
17920
+ ));
17921
+ } else if (options.status) {
17795
17922
  conditions.push(eq(tasks3.status, options.status));
17796
17923
  }
17797
- if (options.batchId) {
17798
- conditions.push(eq(tasks3.batchId, options.batchId));
17924
+ if (options.batchId !== void 0) {
17925
+ const batchId = normalizeTaskBatchId(options.batchId);
17926
+ conditions.push(batchId ? sql`trim(${tasks3.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ${batchId}` : sql`0`);
17799
17927
  }
17800
17928
  if (options.category) {
17801
17929
  conditions.push(eq(tasks3.category, options.category));
17802
17930
  }
17803
- if (options.cwd !== void 0) {
17931
+ if (options.legacyCwd) {
17932
+ conditions.push(sql`${tasks3.cwd} IS NULL OR trim(${tasks3.cwd}) = ''`);
17933
+ } else if (options.cwd !== void 0) {
17804
17934
  conditions.push(eq(tasks3.cwd, options.cwd));
17805
17935
  }
17806
17936
  if (conditions.length > 0) {
@@ -17818,9 +17948,12 @@ var TaskService = class {
17818
17948
  static async stats(options = {}) {
17819
17949
  const conditions = [];
17820
17950
  if (options.batchId !== void 0) {
17821
- conditions.push(eq(tasks3.batchId, options.batchId));
17951
+ const batchId = normalizeTaskBatchId(options.batchId);
17952
+ conditions.push(batchId ? sql`trim(${tasks3.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ${batchId}` : sql`0`);
17822
17953
  }
17823
- if (options.cwd !== void 0) {
17954
+ if (options.legacyCwd) {
17955
+ conditions.push(sql`${tasks3.cwd} IS NULL OR trim(${tasks3.cwd}) = ''`);
17956
+ } else if (options.cwd !== void 0) {
17824
17957
  conditions.push(eq(tasks3.cwd, options.cwd));
17825
17958
  }
17826
17959
  const whereCondition = conditions.length > 0 ? and(...conditions) : void 0;
@@ -17845,6 +17978,33 @@ var TaskService = class {
17845
17978
  }
17846
17979
  return stats;
17847
17980
  }
17981
+ static async projectSummaries(limit = 100) {
17982
+ this.validateInteger("limit", limit, 1, 1e3);
17983
+ const lastCreatedAt = sql`max(${tasks3.createdAt})`;
17984
+ const lastTaskId = sql`max(${tasks3.id})`;
17985
+ const rows = await db.select({
17986
+ cwd: tasks3.cwd,
17987
+ total: sql`count(*)`,
17988
+ pending: sql`sum(CASE WHEN ${tasks3.status} = 'pending' THEN 1 ELSE 0 END)`,
17989
+ running: sql`sum(CASE WHEN ${tasks3.status} = 'running' OR EXISTS (
17990
+ SELECT 1 FROM task_runs AS active_project_run
17991
+ WHERE active_project_run.task_id = ${tasks3.id}
17992
+ AND active_project_run.status = 'running'
17993
+ ) THEN 1 ELSE 0 END)`,
17994
+ failed: sql`sum(CASE WHEN ${tasks3.status} IN ('failed', 'dead_letter') THEN 1 ELSE 0 END)`,
17995
+ done: sql`sum(CASE WHEN ${tasks3.status} = 'done' THEN 1 ELSE 0 END)`,
17996
+ lastCreatedAt
17997
+ }).from(tasks3).where(sql`${tasks3.cwd} IS NOT NULL AND trim(${tasks3.cwd}) <> ''`).groupBy(tasks3.cwd).orderBy(desc(lastCreatedAt), desc(lastTaskId)).limit(limit);
17998
+ return rows.flatMap((row) => row.cwd === null ? [] : [{
17999
+ cwd: row.cwd,
18000
+ total: Number(row.total),
18001
+ pending: Number(row.pending),
18002
+ running: Number(row.running),
18003
+ failed: Number(row.failed),
18004
+ done: Number(row.done),
18005
+ lastCreatedAt: row.lastCreatedAt === null ? null : Number(row.lastCreatedAt) * 1e3
18006
+ }]);
18007
+ }
17848
18008
  static async delete(id, scope = {}) {
17849
18009
  const conditions = [
17850
18010
  eq(tasks3.id, id),
@@ -17997,24 +18157,29 @@ function isValidCronExpr(expr) {
17997
18157
  var { taskTemplates: taskTemplates2 } = schema_exports;
17998
18158
  var TaskTemplateService = class {
17999
18159
  static async create(data) {
18000
- this.validate(data);
18160
+ const normalizedData = {
18161
+ ...data,
18162
+ batchId: normalizeTaskBatchId(data.batchId)
18163
+ };
18164
+ this.validate(normalizedData);
18001
18165
  const now = Date.now();
18002
- const nextRunAt = data.nextRunAt ?? this.calculateNextRunAt(
18003
- data.scheduleType,
18166
+ const nextRunAt = normalizedData.nextRunAt ?? this.calculateNextRunAt(
18167
+ normalizedData.scheduleType,
18004
18168
  {
18005
- cronExpr: data.cronExpr ?? null,
18006
- intervalMs: data.intervalMs ?? null,
18007
- runAt: data.runAt ?? null
18169
+ cronExpr: normalizedData.cronExpr ?? null,
18170
+ intervalMs: normalizedData.intervalMs ?? null,
18171
+ runAt: normalizedData.runAt ?? null
18008
18172
  },
18009
18173
  now
18010
18174
  );
18011
- const result = await db.insert(taskTemplates2).values({ ...data, nextRunAt, createdAt: now, updatedAt: now }).returning();
18175
+ const result = await db.insert(taskTemplates2).values({ ...normalizedData, nextRunAt, createdAt: now, updatedAt: now }).returning();
18012
18176
  return result[0];
18013
18177
  }
18014
18178
  static validate(data) {
18015
18179
  if (!data.name.trim()) throw new Error("name \u4E0D\u80FD\u4E3A\u7A7A");
18016
18180
  if (!data.agent.trim()) throw new Error("agent \u4E0D\u80FD\u4E3A\u7A7A");
18017
18181
  if (!data.prompt.trim()) throw new Error("prompt \u4E0D\u80FD\u4E3A\u7A7A");
18182
+ validateTaskWorkingDirectory(data.cwd);
18018
18183
  const scheduleType = data.scheduleType;
18019
18184
  if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
18020
18185
  throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
@@ -18041,13 +18206,40 @@ var TaskTemplateService = class {
18041
18206
  throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
18042
18207
  }
18043
18208
  }
18044
- static async list(limit = 50) {
18045
- return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit);
18209
+ static async list(limit = 50, offset = 0) {
18210
+ return await db.select().from(taskTemplates2).orderBy(desc(taskTemplates2.createdAt), desc(taskTemplates2.id)).limit(limit).offset(offset);
18211
+ }
18212
+ static async stats() {
18213
+ const result = await db.select({
18214
+ total: sql`count(*)`,
18215
+ enabled: sql`sum(case when ${taskTemplates2.enabled} = 1 then 1 else 0 end)`
18216
+ }).from(taskTemplates2);
18217
+ const total = Number(result[0]?.total ?? 0);
18218
+ const enabled = Number(result[0]?.enabled ?? 0);
18219
+ return { total, enabled, disabled: total - enabled };
18046
18220
  }
18047
18221
  static async getById(id) {
18048
18222
  const result = await db.select().from(taskTemplates2).where(eq(taskTemplates2.id, id));
18049
18223
  return result[0] || null;
18050
18224
  }
18225
+ static async update(id, data) {
18226
+ const normalizedData = {
18227
+ ...data,
18228
+ batchId: normalizeTaskBatchId(data.batchId) ?? null
18229
+ };
18230
+ this.validate(normalizedData);
18231
+ const now = Date.now();
18232
+ const nextRunAt = this.calculateNextRunAt(
18233
+ normalizedData.scheduleType,
18234
+ normalizedData,
18235
+ now
18236
+ );
18237
+ return db.transaction((tx) => {
18238
+ const existing = tx.select({ id: taskTemplates2.id }).from(taskTemplates2).where(eq(taskTemplates2.id, id)).limit(1).get();
18239
+ if (!existing) return null;
18240
+ return tx.update(taskTemplates2).set({ ...normalizedData, nextRunAt, updatedAt: now }).where(eq(taskTemplates2.id, id)).returning().get() ?? null;
18241
+ }, { behavior: "immediate" });
18242
+ }
18051
18243
  static async enable(id) {
18052
18244
  return db.transaction((tx) => {
18053
18245
  const template = tx.select().from(taskTemplates2).where(eq(taskTemplates2.id, id)).limit(1).get();
@@ -18329,37 +18521,40 @@ function createTaskFromTemplate(templateId, options) {
18329
18521
  return db.transaction((tx) => {
18330
18522
  const tmpl = tx.select().from(taskTemplates3).where(eq(taskTemplates3.id, templateId)).limit(1).get();
18331
18523
  if (!tmpl || options.advanceSchedule && !tmpl.enabled) return null;
18332
- const activeTasks = tx.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(and(
18333
- eq(schema_exports.tasks.templateId, templateId),
18334
- sql`${schema_exports.tasks.status} IN ('pending', 'running')`
18335
- )).get();
18336
- const retryableTasks = tx.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(and(
18337
- eq(schema_exports.tasks.templateId, templateId),
18338
- eq(schema_exports.tasks.status, "failed"),
18339
- sql`${schema_exports.tasks.retryCount} <= ${schema_exports.tasks.maxRetries}`
18340
- )).get();
18341
- const detachedRuns = tx.select({ count: sql`count(DISTINCT ${schema_exports.taskRuns.taskId})` }).from(schema_exports.taskRuns).innerJoin(schema_exports.tasks, eq(schema_exports.tasks.id, schema_exports.taskRuns.taskId)).where(and(
18342
- eq(schema_exports.taskRuns.status, "running"),
18343
- eq(schema_exports.tasks.templateId, templateId),
18344
- sql`NOT (
18345
- ${schema_exports.tasks.status} IN ('pending', 'running')
18346
- OR (
18347
- ${schema_exports.tasks.status} = 'failed'
18348
- AND ${schema_exports.tasks.retryCount} <= ${schema_exports.tasks.maxRetries}
18349
- )
18350
- )`
18351
- )).get();
18352
- const activeCount = Number(activeTasks?.count ?? 0) + Number(retryableTasks?.count ?? 0) + Number(detachedRuns?.count ?? 0);
18353
- if (activeCount >= (tmpl.maxInstances ?? 1)) {
18354
- if (options.advanceSchedule && tmpl.scheduleType !== "delayed") {
18355
- const nextRunAt2 = TaskTemplateService.calculateNextRunAt(
18356
- tmpl.scheduleType,
18357
- tmpl,
18358
- nowMs
18359
- );
18360
- tx.update(taskTemplates3).set({ nextRunAt: nextRunAt2, updatedAt: nowMs }).where(and(eq(taskTemplates3.id, templateId), eq(taskTemplates3.enabled, true))).run();
18524
+ if (options.advanceSchedule && options.expectedNextRunAt !== void 0 && tmpl.nextRunAt !== options.expectedNextRunAt) return null;
18525
+ if (options.advanceSchedule) {
18526
+ const activeTasks = tx.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(and(
18527
+ eq(schema_exports.tasks.templateId, templateId),
18528
+ sql`${schema_exports.tasks.status} IN ('pending', 'running')`
18529
+ )).get();
18530
+ const retryableTasks = tx.select({ count: sql`count(*)` }).from(schema_exports.tasks).where(and(
18531
+ eq(schema_exports.tasks.templateId, templateId),
18532
+ eq(schema_exports.tasks.status, "failed"),
18533
+ sql`${schema_exports.tasks.retryCount} <= ${schema_exports.tasks.maxRetries}`
18534
+ )).get();
18535
+ const detachedRuns = tx.select({ count: sql`count(DISTINCT ${schema_exports.taskRuns.taskId})` }).from(schema_exports.taskRuns).innerJoin(schema_exports.tasks, eq(schema_exports.tasks.id, schema_exports.taskRuns.taskId)).where(and(
18536
+ eq(schema_exports.taskRuns.status, "running"),
18537
+ eq(schema_exports.tasks.templateId, templateId),
18538
+ sql`NOT (
18539
+ ${schema_exports.tasks.status} IN ('pending', 'running')
18540
+ OR (
18541
+ ${schema_exports.tasks.status} = 'failed'
18542
+ AND ${schema_exports.tasks.retryCount} <= ${schema_exports.tasks.maxRetries}
18543
+ )
18544
+ )`
18545
+ )).get();
18546
+ const activeCount = Number(activeTasks?.count ?? 0) + Number(retryableTasks?.count ?? 0) + Number(detachedRuns?.count ?? 0);
18547
+ if (activeCount >= (tmpl.maxInstances ?? 1)) {
18548
+ if (tmpl.scheduleType !== "delayed") {
18549
+ const nextRunAt2 = TaskTemplateService.calculateNextRunAt(
18550
+ tmpl.scheduleType,
18551
+ tmpl,
18552
+ nowMs
18553
+ );
18554
+ tx.update(taskTemplates3).set({ nextRunAt: nextRunAt2, updatedAt: nowMs }).where(and(eq(taskTemplates3.id, templateId), eq(taskTemplates3.enabled, true))).run();
18555
+ }
18556
+ return null;
18361
18557
  }
18362
- return null;
18363
18558
  }
18364
18559
  const isDelayed = tmpl.scheduleType === "delayed";
18365
18560
  const nextRunAt = isDelayed ? null : TaskTemplateService.calculateNextRunAt(
@@ -18376,7 +18571,7 @@ function createTaskFromTemplate(templateId, options) {
18376
18571
  category: tmpl.category ?? "general",
18377
18572
  importance: tmpl.importance ?? 3,
18378
18573
  urgency: tmpl.urgency ?? 3,
18379
- batchId: tmpl.batchId,
18574
+ batchId: normalizeTaskBatchId(tmpl.batchId),
18380
18575
  maxRetries: tmpl.maxRetries ?? 3,
18381
18576
  retryBackoffMs: tmpl.retryBackoffMs ?? 3e4,
18382
18577
  timeoutMs: tmpl.timeoutMs,
@@ -18395,21 +18590,447 @@ function createTaskFromTemplate(templateId, options) {
18395
18590
  }, { behavior: "immediate" });
18396
18591
  }
18397
18592
 
18593
+ // src/daemon/pm2.ts
18594
+ import { execSync, spawnSync } from "child_process";
18595
+ import {
18596
+ accessSync,
18597
+ chmodSync as chmodSync2,
18598
+ constants,
18599
+ existsSync as existsSync4,
18600
+ mkdirSync as mkdirSync3,
18601
+ readFileSync as readFileSync2,
18602
+ rmSync,
18603
+ statSync as statSync3,
18604
+ writeFileSync as writeFileSync2
18605
+ } from "fs";
18606
+ import { homedir as homedir3, userInfo } from "os";
18607
+ import { delimiter, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "path";
18608
+ import { fileURLToPath as fileURLToPath2 } from "url";
18609
+ import { Database as Database5 } from "bun:sqlite";
18610
+
18611
+ // src/daemon/management-lock.ts
18612
+ import { Database as Database4 } from "bun:sqlite";
18613
+
18614
+ // src/daemon/pm2.ts
18615
+ var __dirname = dirname3(fileURLToPath2(import.meta.url));
18616
+ var PROCESS_NAME = "supertask-gateway";
18617
+ var MAC_LAUNCH_AGENT_LABEL = "com.supertask.pm2-resurrect";
18618
+ var GATEWAY_LOCK_STALE_MS = 3e4;
18619
+ function runtimeHome(env) {
18620
+ return resolve2(env.HOME || homedir3());
18621
+ }
18622
+ function runtimePath(value, cwd) {
18623
+ return resolve2(cwd, value);
18624
+ }
18625
+ function versionFile(env = process.env, cwd = process.cwd()) {
18626
+ return env.SUPERTASK_VERSION_FILE ? runtimePath(env.SUPERTASK_VERSION_FILE, cwd) : join3(runtimeHome(env), ".local/share/opencode/supertask-gateway-version");
18627
+ }
18628
+ function getRunningVersion(env = process.env, cwd = process.cwd()) {
18629
+ try {
18630
+ const path = versionFile(env, cwd);
18631
+ if (!existsSync4(path)) return null;
18632
+ return readFileSync2(path, "utf-8").trim() || null;
18633
+ } catch {
18634
+ return null;
18635
+ }
18636
+ }
18637
+ function packageVersionFromGatewayEntry(gatewayEntry) {
18638
+ if (gatewayEntry === null) return null;
18639
+ let directory = dirname3(gatewayEntry);
18640
+ for (let depth = 0; depth < 6; depth += 1) {
18641
+ const packagePath = join3(directory, "package.json");
18642
+ if (existsSync4(packagePath)) {
18643
+ try {
18644
+ const pkg = JSON.parse(readFileSync2(packagePath, "utf8"));
18645
+ if (pkg.name === "opencode-supertask" && typeof pkg.version === "string") {
18646
+ return pkg.version;
18647
+ }
18648
+ } catch {
18649
+ }
18650
+ }
18651
+ const parent = dirname3(directory);
18652
+ if (parent === directory) break;
18653
+ directory = parent;
18654
+ }
18655
+ return null;
18656
+ }
18657
+ function resolveRuntimeExecutable(command, env, cwd) {
18658
+ if (isAbsolute2(command) || command.includes("/")) return runtimePath(command, cwd);
18659
+ for (const entry of (env.PATH ?? "").split(delimiter)) {
18660
+ if (!entry) continue;
18661
+ const candidate = resolve2(cwd, entry, command);
18662
+ try {
18663
+ accessSync(candidate, constants.X_OK);
18664
+ return candidate;
18665
+ } catch {
18666
+ }
18667
+ }
18668
+ return command;
18669
+ }
18670
+ function runtimeScope(runtime) {
18671
+ const { cwd, env } = runtime;
18672
+ const home = runtimeHome(env);
18673
+ return {
18674
+ cwd: resolve2(cwd),
18675
+ databasePath: env.SUPERTASK_DB_PATH ? runtimePath(env.SUPERTASK_DB_PATH, cwd) : join3(home, ".local/share/opencode/tasks.db"),
18676
+ configPath: env.SUPERTASK_CONFIG_PATH ? runtimePath(env.SUPERTASK_CONFIG_PATH, cwd) : join3(home, ".config/opencode/supertask.json"),
18677
+ opencodePath: resolveRuntimeExecutable(env.SUPERTASK_OPENCODE_BIN ?? "opencode", env, cwd),
18678
+ home,
18679
+ pm2Home: env.PM2_HOME ? runtimePath(env.PM2_HOME, cwd) : join3(home, ".pm2"),
18680
+ managementLockPath: canonicalManagementLockPath(env, cwd)
18681
+ };
18682
+ }
18683
+ function scopesMatch(left, right) {
18684
+ 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;
18685
+ }
18686
+ function currentScope() {
18687
+ return runtimeScope({ cwd: process.cwd(), env: process.env });
18688
+ }
18689
+ function getGatewayDiagnostic() {
18690
+ const producerScope = currentScope();
18691
+ if (!isPm2Installed()) {
18692
+ return {
18693
+ pm2Installed: false,
18694
+ processFound: false,
18695
+ status: null,
18696
+ pid: null,
18697
+ ready: false,
18698
+ runningVersion: getRunningVersion(),
18699
+ gatewayEntry: null,
18700
+ gatewayPackageVersion: null,
18701
+ logRotationInstalled: false,
18702
+ startupConfigured: process.platform === "darwin" || process.platform === "linux" ? false : null,
18703
+ currentScope: producerScope,
18704
+ gatewayScope: null,
18705
+ scopeMatches: false
18706
+ };
18707
+ }
18708
+ const processes = pm2JsonList();
18709
+ const gateway = processes.find((item) => item.name === PROCESS_NAME);
18710
+ const runtime = gatewayRuntimeFromProcess(gateway);
18711
+ const gatewayEnv = runtime?.env ?? process.env;
18712
+ const managedScope = runtime ? runtimeScope(runtime) : null;
18713
+ const pid = typeof gateway?.pid === "number" ? gateway.pid : null;
18714
+ const readyPath = managedScope?.databasePath ?? databasePath();
18715
+ const lockedVersion = pid == null ? void 0 : gatewayVersionFromLock(pid, readyPath);
18716
+ const gatewayEntry = runtime?.gatewayEntry ?? null;
18717
+ return {
18718
+ pm2Installed: true,
18719
+ processFound: gateway != null,
18720
+ status: gateway?.pm2_env?.status ?? null,
18721
+ pid,
18722
+ ready: pid != null && isGatewayReady(pid, readyPath),
18723
+ runningVersion: lockedVersion === void 0 ? getRunningVersion(gatewayEnv, runtime?.cwd) : lockedVersion,
18724
+ gatewayEntry,
18725
+ gatewayPackageVersion: packageVersionFromGatewayEntry(gatewayEntry),
18726
+ logRotationInstalled: processes.some((item) => item.name === "pm2-logrotate" && item.pm2_env?.status === "online"),
18727
+ startupConfigured: process.platform === "darwin" ? isMacLaunchAgentConfigured(
18728
+ gatewayEnv.PM2_HOME ?? join3(runtimeHome(gatewayEnv), ".pm2"),
18729
+ runtime ?? void 0
18730
+ ) : process.platform === "linux" ? isLinuxStartupConfigured(gatewayEnv, runtime?.cwd, runtime ?? void 0) : null,
18731
+ currentScope: producerScope,
18732
+ gatewayScope: managedScope,
18733
+ scopeMatches: managedScope != null && scopesMatch(producerScope, managedScope)
18734
+ };
18735
+ }
18736
+ function pm2Bin(env = process.env) {
18737
+ return env.SUPERTASK_PM2_BIN ?? (process.platform === "win32" ? "pm2.cmd" : "pm2");
18738
+ }
18739
+ function pm2CommandTimeoutMs(env = process.env) {
18740
+ const configured = Number(env.SUPERTASK_PM2_COMMAND_TIMEOUT_MS ?? 15e3);
18741
+ return Number.isFinite(configured) && configured > 0 ? configured : 15e3;
18742
+ }
18743
+ function resolvePm2Bin() {
18744
+ const configured = pm2Bin();
18745
+ if (isAbsolute2(configured)) return configured;
18746
+ const result = spawnSync("which", [configured], { encoding: "utf8" });
18747
+ const resolved = result.status === 0 ? result.stdout.trim().split("\n")[0] : "";
18748
+ if (!resolved) throw new Error(`[supertask] \u65E0\u6CD5\u89E3\u6790 pm2 \u53EF\u6267\u884C\u6587\u4EF6: ${configured}`);
18749
+ return resolved;
18750
+ }
18751
+ function launchAgentPath() {
18752
+ return process.env.SUPERTASK_LAUNCH_AGENT_PATH ?? join3(runtimeHome(process.env), "Library/LaunchAgents", `${MAC_LAUNCH_AGENT_LABEL}.plist`);
18753
+ }
18754
+ function launchctlBin() {
18755
+ return process.env.SUPERTASK_LAUNCHCTL_BIN ?? "launchctl";
18756
+ }
18757
+ function xmlUnescape(value) {
18758
+ return value.replaceAll("&apos;", "'").replaceAll("&quot;", '"').replaceAll("&gt;", ">").replaceAll("&lt;", "<").replaceAll("&amp;", "&");
18759
+ }
18760
+ function plistValue(contents, key) {
18761
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18762
+ const match2 = contents.match(new RegExp(`<key>\\s*${escapedKey}\\s*</key>\\s*<string>([^<]*)</string>`));
18763
+ return match2?.[1] ? xmlUnescape(match2[1]) : null;
18764
+ }
18765
+ function isMacLaunchAgentConfigured(expectedPm2Home, expectedRuntime) {
18766
+ if (typeof process.getuid !== "function") return false;
18767
+ const plistPath = launchAgentPath();
18768
+ if (!existsSync4(plistPath)) return false;
18769
+ try {
18770
+ const contents = readFileSync2(plistPath, "utf8");
18771
+ const argumentsBlock = contents.match(
18772
+ /<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/
18773
+ )?.[1];
18774
+ const programArguments = argumentsBlock ? [...argumentsBlock.matchAll(/<string>([^<]*)<\/string>/g)].map((match2) => xmlUnescape(match2[1])) : [];
18775
+ const bunPath = programArguments[0];
18776
+ const supervisorEntry = programArguments[1];
18777
+ const pm2Path = programArguments[2];
18778
+ const pm2Home = plistValue(contents, "PM2_HOME");
18779
+ const managementLock = plistValue(contents, "SUPERTASK_PM2_MANAGEMENT_LOCK");
18780
+ if (!bunPath || !supervisorEntry || !pm2Path || !pm2Home || !managementLock) return false;
18781
+ if (expectedPm2Home && pm2Home !== expectedPm2Home) return false;
18782
+ const expectedManagementLock = expectedRuntime ? managementLockPath(expectedRuntime.env, expectedRuntime.cwd) : process.env.SUPERTASK_PM2_MANAGEMENT_LOCK ? managementLockPath() : join3(expectedPm2Home ?? currentScope().pm2Home, "supertask-gateway.manage.sqlite");
18783
+ if (managementLock !== expectedManagementLock) return false;
18784
+ accessSync(bunPath, constants.X_OK);
18785
+ accessSync(supervisorEntry, constants.R_OK);
18786
+ accessSync(pm2Path, constants.X_OK);
18787
+ if (spawnSync(bunPath, ["--version"], {
18788
+ stdio: "ignore",
18789
+ timeout: pm2CommandTimeoutMs(),
18790
+ killSignal: "SIGKILL"
18791
+ }).status !== 0) return false;
18792
+ if (spawnSync(pm2Path, ["--version"], {
18793
+ stdio: "ignore",
18794
+ env: { ...process.env, PM2_HOME: pm2Home },
18795
+ timeout: pm2CommandTimeoutMs(),
18796
+ killSignal: "SIGKILL"
18797
+ }).status !== 0) return false;
18798
+ const loaded = spawnSync(
18799
+ launchctlBin(),
18800
+ ["print", `gui/${process.getuid()}/${MAC_LAUNCH_AGENT_LABEL}`],
18801
+ {
18802
+ encoding: "utf8",
18803
+ stdio: ["ignore", "pipe", "ignore"],
18804
+ timeout: pm2CommandTimeoutMs(),
18805
+ killSignal: "SIGKILL"
18806
+ }
18807
+ );
18808
+ if (loaded.status !== 0) return false;
18809
+ if (!loaded.stdout.includes("state = running")) return false;
18810
+ if (!loaded.stdout.includes(`path = ${plistPath}`)) return false;
18811
+ if (!loaded.stdout.includes(`program = ${bunPath}`)) return false;
18812
+ const dumpPath = join3(pm2Home, "dump.pm2");
18813
+ const dump = JSON.parse(readFileSync2(dumpPath, "utf8"));
18814
+ if (!Array.isArray(dump)) return false;
18815
+ const gateway = dump.find((item) => item.name === PROCESS_NAME);
18816
+ const dumpRuntime = gatewayRuntimeFromProcess(gateway);
18817
+ if (!dumpRuntime || !hasRestorableSavedGatewayRuntime(gateway)) return false;
18818
+ return expectedRuntime === void 0 || dumpRuntime.gatewayEntry === expectedRuntime.gatewayEntry && dumpRuntime.bunPath === expectedRuntime.bunPath && dumpRuntime.cwd === expectedRuntime.cwd && scopesMatch(runtimeScope(dumpRuntime), runtimeScope(expectedRuntime));
18819
+ } catch {
18820
+ return false;
18821
+ }
18822
+ }
18823
+ function systemctlBin(env = process.env) {
18824
+ return env.SUPERTASK_SYSTEMCTL_BIN ?? "systemctl";
18825
+ }
18826
+ function isLinuxStartupConfigured(env = process.env, cwd = process.cwd(), expectedRuntime) {
18827
+ const unit = env.SUPERTASK_PM2_SYSTEMD_UNIT ?? `pm2-${userInfo().username}.service`;
18828
+ const enabled = spawnSync(systemctlBin(env), ["is-enabled", unit], {
18829
+ encoding: "utf8",
18830
+ env,
18831
+ stdio: ["ignore", "pipe", "ignore"],
18832
+ timeout: pm2CommandTimeoutMs(env),
18833
+ killSignal: "SIGKILL"
18834
+ });
18835
+ if (enabled.status !== 0 || enabled.stdout.trim() !== "enabled") return false;
18836
+ const contents = spawnSync(systemctlBin(env), ["cat", unit], {
18837
+ encoding: "utf8",
18838
+ env,
18839
+ stdio: ["ignore", "pipe", "ignore"],
18840
+ timeout: pm2CommandTimeoutMs(env),
18841
+ killSignal: "SIGKILL"
18842
+ });
18843
+ if (contents.status !== 0) return false;
18844
+ const expectedPm2Home = env.PM2_HOME ?? join3(runtimeHome(env), ".pm2");
18845
+ if (!contents.stdout.includes("resurrect") || !contents.stdout.includes(expectedPm2Home)) {
18846
+ return false;
18847
+ }
18848
+ try {
18849
+ const dump = JSON.parse(readFileSync2(join3(expectedPm2Home, "dump.pm2"), "utf8"));
18850
+ if (!Array.isArray(dump)) return false;
18851
+ const gateway = dump.find((item) => item.name === PROCESS_NAME);
18852
+ const runtime = gatewayRuntimeFromProcess(gateway);
18853
+ if (!runtime || !hasRestorableSavedGatewayRuntime(gateway)) return false;
18854
+ return runtime.cwd === resolve2(cwd) && scopesMatch(runtimeScope(runtime), runtimeScope({ cwd, env })) && (expectedRuntime === void 0 || runtime.gatewayEntry === expectedRuntime.gatewayEntry && runtime.bunPath === expectedRuntime.bunPath);
18855
+ } catch {
18856
+ return false;
18857
+ }
18858
+ }
18859
+ function isPm2Installed(env = process.env) {
18860
+ const result = spawnSync(pm2Bin(env), ["--version"], {
18861
+ stdio: "ignore",
18862
+ env,
18863
+ shell: process.platform === "win32",
18864
+ timeout: pm2CommandTimeoutMs(env),
18865
+ killSignal: "SIGKILL"
18866
+ });
18867
+ return result.status === 0;
18868
+ }
18869
+ function resolveCommand(command) {
18870
+ const lookup = process.platform === "win32" ? "where" : "which";
18871
+ const result = spawnSync(lookup, [command], { encoding: "utf8" });
18872
+ return result.status === 0 ? result.stdout.trim().split("\n")[0] || null : null;
18873
+ }
18874
+ function npmPreferredEnvironment() {
18875
+ if (process.platform === "win32") return null;
18876
+ const npm = resolveCommand("npm");
18877
+ const node = resolveCommand("node");
18878
+ if (!npm || !node) return null;
18879
+ const command = resolvePm2Bin();
18880
+ const path = [.../* @__PURE__ */ new Set([
18881
+ dirname3(command),
18882
+ dirname3(npm),
18883
+ dirname3(node),
18884
+ "/usr/local/bin",
18885
+ "/usr/bin",
18886
+ "/bin"
18887
+ ])].join(delimiter);
18888
+ return { command, env: { ...process.env, PATH: path } };
18889
+ }
18890
+ function pm2Exec(args, options = {}) {
18891
+ const npmEnvironment = options.preferNpm ? npmPreferredEnvironment() : null;
18892
+ const effectiveEnv = options.env ?? npmEnvironment?.env ?? process.env;
18893
+ const result = spawnSync(npmEnvironment?.command ?? pm2Bin(effectiveEnv), args, {
18894
+ stdio: ["pipe", "pipe", "pipe"],
18895
+ encoding: "utf-8",
18896
+ env: effectiveEnv,
18897
+ shell: process.platform === "win32",
18898
+ timeout: options.timeoutMs ?? pm2CommandTimeoutMs(effectiveEnv),
18899
+ killSignal: "SIGKILL"
18900
+ });
18901
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
18902
+ if (result.error) return { ok: false, output: result.error.message };
18903
+ return { ok: result.status === 0, output };
18904
+ }
18905
+ function requirePm2(args, action, options = {}) {
18906
+ const result = pm2Exec(args, options);
18907
+ if (!result.ok) throw new Error(`[supertask] ${action} failed: ${result.output || "unknown pm2 error"}`);
18908
+ return result.output;
18909
+ }
18910
+ function pm2JsonList(env = process.env) {
18911
+ const output = requirePm2(["jlist"], "pm2 jlist", { env });
18912
+ try {
18913
+ const parsed = JSON.parse(output);
18914
+ if (!Array.isArray(parsed)) throw new Error("result is not an array");
18915
+ return parsed;
18916
+ } catch (error) {
18917
+ const message = error instanceof Error ? error.message : String(error);
18918
+ throw new Error(`[supertask] Invalid pm2 jlist output: ${message}`);
18919
+ }
18920
+ }
18921
+ function gatewayEntryFromProcess(processInfo) {
18922
+ const args = processInfo?.pm2_env?.args ?? processInfo?.args;
18923
+ const candidates = Array.isArray(args) ? [...args].reverse() : typeof args === "string" ? [args] : [];
18924
+ const savedCwd = processInfo?.pm2_env?.pm_cwd ?? processInfo?.pm_cwd;
18925
+ for (const candidate of candidates) {
18926
+ const path = typeof savedCwd === "string" ? runtimePath(candidate, savedCwd) : candidate;
18927
+ if (existsSync4(path)) return resolve2(path);
18928
+ }
18929
+ return null;
18930
+ }
18931
+ function gatewayEnvironmentFromProcess(processInfo) {
18932
+ const saved = processInfo?.pm2_env?.env ?? processInfo?.env;
18933
+ if (!saved) return { ...process.env };
18934
+ const env = {};
18935
+ for (const [key, value] of Object.entries(saved)) {
18936
+ if (typeof value === "string") env[key] = value;
18937
+ }
18938
+ return env;
18939
+ }
18940
+ function gatewayRuntimeFromProcess(processInfo) {
18941
+ const gatewayEntry = gatewayEntryFromProcess(processInfo);
18942
+ if (!gatewayEntry) return null;
18943
+ const savedBunPath = processInfo?.pm2_env?.pm_exec_path ?? processInfo?.pm_exec_path;
18944
+ const savedCwd = processInfo?.pm2_env?.pm_cwd ?? processInfo?.pm_cwd;
18945
+ const savedEnv = gatewayEnvironmentFromProcess(processInfo);
18946
+ if (typeof savedBunPath !== "string" || typeof savedCwd !== "string") return null;
18947
+ try {
18948
+ accessSync(savedBunPath, constants.X_OK);
18949
+ if (!statSync3(savedCwd).isDirectory()) return null;
18950
+ if (spawnSync(savedBunPath, ["--version"], {
18951
+ stdio: "ignore",
18952
+ env: savedEnv,
18953
+ timeout: pm2CommandTimeoutMs(savedEnv),
18954
+ killSignal: "SIGKILL"
18955
+ }).status !== 0) return null;
18956
+ } catch {
18957
+ return null;
18958
+ }
18959
+ const killTimeout = processInfo?.pm2_env?.kill_timeout ?? processInfo?.kill_timeout;
18960
+ return {
18961
+ gatewayEntry,
18962
+ bunPath: savedBunPath,
18963
+ cwd: resolve2(savedCwd),
18964
+ env: savedEnv,
18965
+ killTimeoutMs: Number.isInteger(killTimeout) && killTimeout >= 5e3 ? killTimeout : void 0
18966
+ };
18967
+ }
18968
+ function hasRestorableSavedGatewayRuntime(processInfo) {
18969
+ return gatewayRuntimeFromProcess(processInfo) !== null;
18970
+ }
18971
+ function databasePath(env = process.env, cwd = process.cwd()) {
18972
+ return env.SUPERTASK_DB_PATH ? runtimePath(env.SUPERTASK_DB_PATH, cwd) : join3(runtimeHome(env), ".local/share/opencode/tasks.db");
18973
+ }
18974
+ function isGatewayReady(expectedPid, path = databasePath()) {
18975
+ if (!existsSync4(path)) return false;
18976
+ let database = null;
18977
+ try {
18978
+ database = new Database5(path, { readonly: true });
18979
+ const row = database.query(
18980
+ "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
18981
+ ).get();
18982
+ if (!row || row.ready_at == null) return false;
18983
+ if (expectedPid !== void 0 && row.pid !== expectedPid) return false;
18984
+ const ageMs = Date.now() - row.heartbeat_at;
18985
+ return ageMs >= -5e3 && ageMs < GATEWAY_LOCK_STALE_MS;
18986
+ } catch {
18987
+ return false;
18988
+ } finally {
18989
+ database?.close();
18990
+ }
18991
+ }
18992
+ function gatewayVersionFromLock(expectedPid, path) {
18993
+ if (!existsSync4(path)) return void 0;
18994
+ let database = null;
18995
+ try {
18996
+ database = new Database5(path, { readonly: true });
18997
+ const row = database.query(
18998
+ "SELECT pid, version FROM gateway_lock WHERE id = 1"
18999
+ ).get();
19000
+ if (!row || row.pid !== expectedPid) return void 0;
19001
+ return row.version;
19002
+ } catch {
19003
+ return void 0;
19004
+ } finally {
19005
+ database?.close();
19006
+ }
19007
+ }
19008
+ function managementLockPath(env = process.env, cwd = process.cwd()) {
19009
+ const override = env.SUPERTASK_PM2_MANAGEMENT_LOCK;
19010
+ if (override) return runtimePath(override, cwd);
19011
+ return join3(runtimeScope({ env, cwd }).pm2Home, "supertask-gateway.manage.sqlite");
19012
+ }
19013
+ function canonicalManagementLockPath(env = process.env, cwd = process.cwd()) {
19014
+ const home = runtimeHome(env);
19015
+ const pm2Home = env.PM2_HOME ? runtimePath(env.PM2_HOME, cwd) : join3(home, ".pm2");
19016
+ return join3(pm2Home, "supertask-gateway.manage.sqlite");
19017
+ }
19018
+
18398
19019
  // src/web/ui.ts
18399
19020
  var ZH = {
18400
19021
  "app.name": "SuperTask",
18401
19022
  "app.dashboard": "\u63A7\u5236\u53F0",
18402
- "app.tagline": "\u53EF\u9760\u7684\u672C\u5730 Agent \u8C03\u5EA6\u4E2D\u5FC3",
19023
+ "app.tagline": "\u53EF\u9760\u7684\u672C\u5730 Agent \u4EFB\u52A1\u4E2D\u5FC3",
18403
19024
  "app.local": "\u672C\u5730\u8FD0\u884C",
18404
19025
  "app.footer": "Local-first \xB7 \u6570\u636E\u7559\u5728\u4F60\u7684\u8BBE\u5907\u4E0A",
18405
19026
  "nav.tasks": "\u4EFB\u52A1",
18406
- "nav.templates": "\u8C03\u5EA6",
19027
+ "nav.templates": "\u5B9A\u65F6\u4EFB\u52A1",
18407
19028
  "nav.runs": "\u6267\u884C\u8BB0\u5F55",
18408
19029
  "nav.system": "\u7CFB\u7EDF",
18409
19030
  "page.tasks.title": "\u4EFB\u52A1\u961F\u5217",
18410
19031
  "page.tasks.description": "\u67E5\u770B\u4F18\u5148\u7EA7\u3001\u6267\u884C\u72B6\u6001\u4E0E\u91CD\u8BD5\u60C5\u51B5\uFF0C\u5FEB\u901F\u5904\u7406\u9700\u8981\u5173\u6CE8\u7684\u4EFB\u52A1\u3002",
18411
- "page.templates.title": "\u8C03\u5EA6\u6A21\u677F",
18412
- "page.templates.description": "\u7BA1\u7406 Cron\u3001\u5EF6\u8FDF\u548C\u5FAA\u73AF\u4EFB\u52A1\uFF0C\u8BA9\u91CD\u590D\u5DE5\u4F5C\u6309\u8BA1\u5212\u81EA\u52A8\u8FD0\u884C\u3002",
19032
+ "page.templates.title": "\u5B9A\u65F6\u4EFB\u52A1",
19033
+ "page.templates.description": "\u5728\u7F51\u9875\u521B\u5EFA\u548C\u7F16\u8F91 Cron\u3001\u56FA\u5B9A\u95F4\u9694\u53CA\u4E00\u6B21\u6027\u4EFB\u52A1\uFF0C\u5305\u62EC\u6A21\u578B\u3001Agent \u548C\u63D0\u793A\u8BCD\u3002",
18413
19034
  "page.runs.title": "\u6267\u884C\u8BB0\u5F55",
18414
19035
  "page.runs.description": "\u8FFD\u8E2A\u6BCF\u6B21 Agent \u6267\u884C\u7684\u72B6\u6001\u3001\u8017\u65F6\u3001\u5FC3\u8DF3\u4E0E\u8F93\u51FA\u3002",
18415
19036
  "page.system.title": "\u7CFB\u7EDF\u8BBE\u7F6E",
@@ -18419,29 +19040,43 @@ var ZH = {
18419
19040
  "action.retry": "\u91CD\u8BD5",
18420
19041
  "action.cancel": "\u53D6\u6D88",
18421
19042
  "action.delete": "\u5220\u9664",
19043
+ "action.edit": "\u7F16\u8F91",
18422
19044
  "action.enable": "\u542F\u7528",
18423
19045
  "action.disable": "\u7981\u7528",
18424
- "action.trigger": "\u7ACB\u5373\u89E6\u53D1",
19046
+ "action.trigger": "\u7ACB\u5373\u8FD0\u884C\u4E00\u6B21",
19047
+ "action.continueSession": "\u7EE7\u7EED\u4F1A\u8BDD",
18425
19048
  "action.logs": "\u67E5\u770B\u65E5\u5FD7",
18426
19049
  "action.hideLogs": "\u6536\u8D77\u65E5\u5FD7",
18427
19050
  "action.save": "\u4FDD\u5B58\u8BBE\u7F6E",
19051
+ "action.saveAndRestart": "\u4FDD\u5B58\u5E76\u91CD\u542F",
18428
19052
  "action.copy": "\u590D\u5236 JSON",
18429
19053
  "action.close": "\u5173\u95ED",
18430
19054
  "action.confirm": "\u786E\u8BA4",
18431
19055
  "action.clearDatabase": "\u6E05\u7A7A\u6570\u636E\u5E93",
19056
+ "action.createTask": "\u65B0\u5EFA\u4EFB\u52A1",
19057
+ "action.saveTask": "\u52A0\u5165\u961F\u5217",
19058
+ "action.updateTask": "\u4FDD\u5B58\u4FEE\u6539",
19059
+ "action.createTemplate": "\u65B0\u5EFA\u5B9A\u65F6\u4EFB\u52A1",
19060
+ "action.saveTemplate": "\u4FDD\u5B58\u5B9A\u65F6\u4EFB\u52A1",
18432
19061
  "status.pending": "\u5F85\u6267\u884C",
18433
19062
  "status.running": "\u8FD0\u884C\u4E2D",
18434
19063
  "status.done": "\u5DF2\u5B8C\u6210",
18435
- "status.failed": "\u5931\u8D25",
18436
- "status.dead_letter": "\u6B7B\u4FE1",
19064
+ "status.failed": "\u7B49\u5F85\u91CD\u8BD5",
19065
+ "status.dead_letter": "\u5DF2\u505C\u6B62",
19066
+ "status.deadLetterHint": "\u7CFB\u7EDF\u4E0D\u4F1A\u518D\u81EA\u52A8\u8FD0\u884C\u3002\u53EF\u80FD\u662F\u91CD\u8BD5\u6B21\u6570\u5DF2\u7528\u5B8C\uFF0C\u6216\u4F9D\u8D56\u4EFB\u52A1\u65E0\u6CD5\u7EE7\u7EED\uFF1B\u8BF7\u67E5\u770B\u5931\u8D25\u539F\u56E0\uFF0C\u786E\u8BA4\u540E\u624B\u52A8\u91CD\u8BD5\u3002",
19067
+ "status.deadLetterAction": "\u9700\u68C0\u67E5\u539F\u56E0\u540E\u624B\u52A8\u91CD\u8BD5",
18437
19068
  "status.cancelled": "\u5DF2\u53D6\u6D88",
19069
+ "status.executionStillActive": "\u6267\u884C\u8FDB\u7A0B\u4ECD\u5728\u9000\u51FA\uFF0C\u6682\u65F6\u5360\u7528\u5E76\u53D1",
18438
19070
  "status.unknown": "\u672A\u77E5",
19071
+ "runStatus.running": "\u8FD0\u884C\u4E2D",
19072
+ "runStatus.done": "\u6210\u529F",
19073
+ "runStatus.failed": "\u5931\u8D25",
18439
19074
  "stats.total": "\u603B\u4EFB\u52A1",
18440
19075
  "stats.pending": "\u5F85\u6267\u884C",
18441
19076
  "stats.running": "\u8FD0\u884C\u4E2D",
18442
19077
  "stats.done": "\u5DF2\u5B8C\u6210",
18443
- "stats.failedDead": "\u5931\u8D25\u4E0E\u6B7B\u4FE1",
18444
- "stats.templates": "\u6A21\u677F\u603B\u6570",
19078
+ "stats.failedDead": "\u7B49\u5F85\u91CD\u8BD5\u4E0E\u5DF2\u505C\u6B62",
19079
+ "stats.templates": "\u5B9A\u65F6\u4EFB\u52A1\u603B\u6570",
18445
19080
  "stats.enabled": "\u5DF2\u542F\u7528",
18446
19081
  "stats.disabled": "\u5DF2\u7981\u7528",
18447
19082
  "stats.records": "\u6267\u884C\u603B\u6570",
@@ -18449,7 +19084,7 @@ var ZH = {
18449
19084
  "stats.pageFailed": "\u672C\u9875\u5931\u8D25",
18450
19085
  "stats.pageRunning": "\u672C\u9875\u8FD0\u884C\u4E2D",
18451
19086
  "filter.all": "\u5168\u90E8",
18452
- "filter.searchTasks": "\u641C\u7D22\u5F53\u524D\u9875\u7684\u4EFB\u52A1\u3001Agent \u6216\u63D0\u793A\u8BCD",
19087
+ "filter.searchTasks": "\u641C\u7D22\u5F53\u524D\u9875\u7684\u4EFB\u52A1\u3001\u9879\u76EE\u3001\u6279\u6B21\u3001Agent \u6216\u63D0\u793A\u8BCD",
18453
19088
  "filter.noResults": "\u6CA1\u6709\u7B26\u5408\u5F53\u524D\u641C\u7D22\u6761\u4EF6\u7684\u4EFB\u52A1",
18454
19089
  "table.id": "ID",
18455
19090
  "table.task": "\u4EFB\u52A1",
@@ -18465,7 +19100,7 @@ var ZH = {
18465
19100
  "table.nextRun": "\u4E0B\u6B21\u6267\u884C",
18466
19101
  "table.run": "Run",
18467
19102
  "table.heartbeat": "\u5FC3\u8DF3",
18468
- "table.session": "Session",
19103
+ "table.session": "\u4F1A\u8BDD",
18469
19104
  "table.model": "\u6A21\u578B",
18470
19105
  "table.startedAt": "\u542F\u52A8\u65F6\u95F4",
18471
19106
  "table.pid": "PID",
@@ -18473,14 +19108,14 @@ var ZH = {
18473
19108
  "pagination.next": "\u4E0B\u4E00\u9875",
18474
19109
  "pagination.summary": "\u7B2C {page} \u9875\uFF0C\u5171 {pages} \u9875 \xB7 {total} \u6761",
18475
19110
  "empty.tasks": "\u961F\u5217\u91CC\u8FD8\u6CA1\u6709\u4EFB\u52A1",
18476
- "empty.tasksHint": "\u901A\u8FC7 OpenCode \u63D2\u4EF6\u6216 supertask add \u521B\u5EFA\u7B2C\u4E00\u4E2A\u4EFB\u52A1\u3002",
18477
- "empty.templates": "\u8FD8\u6CA1\u6709\u8C03\u5EA6\u6A21\u677F",
18478
- "empty.templatesHint": "\u4F7F\u7528 CLI \u521B\u5EFA\uFF1Asupertask template add",
19111
+ "empty.tasksHint": "\u70B9\u51FB\u201C\u65B0\u5EFA\u4EFB\u52A1\u201D\uFF0C\u6216\u901A\u8FC7 OpenCode \u63D2\u4EF6\u548C supertask add \u521B\u5EFA\u7B2C\u4E00\u4E2A\u4EFB\u52A1\u3002",
19112
+ "empty.templates": "\u8FD8\u6CA1\u6709\u5B9A\u65F6\u4EFB\u52A1",
19113
+ "empty.templatesHint": "\u70B9\u51FB\u53F3\u4E0A\u89D2\u201C\u65B0\u5EFA\u5B9A\u65F6\u4EFB\u52A1\u201D\u5F00\u59CB\u521B\u5EFA\u3002",
18479
19114
  "empty.runs": "\u8FD8\u6CA1\u6709\u6267\u884C\u8BB0\u5F55",
18480
19115
  "empty.running": "\u5F53\u524D\u6CA1\u6709\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1",
18481
19116
  "schedule.cron": "Cron",
18482
- "schedule.recurring": "\u5FAA\u73AF",
18483
- "schedule.delayed": "\u5EF6\u8FDF",
19117
+ "schedule.recurring": "\u56FA\u5B9A\u95F4\u9694",
19118
+ "schedule.delayed": "\u4E00\u6B21\u6027",
18484
19119
  "schedule.unknown": "\u672A\u77E5",
18485
19120
  "schedule.enabled": "\u5DF2\u542F\u7528",
18486
19121
  "schedule.disabled": "\u5DF2\u7981\u7528",
@@ -18489,14 +19124,14 @@ var ZH = {
18489
19124
  "schedule.hours": "{count} \u5C0F\u65F6",
18490
19125
  "schedule.days": "{count} \u5929",
18491
19126
  "schedule.overdue": "\u5DF2\u5230\u671F",
18492
- "system.worker": "Worker",
18493
- "system.scheduler": "Scheduler",
18494
- "system.watchdog": "Watchdog",
19127
+ "system.worker": "\u4EFB\u52A1\u6267\u884C",
19128
+ "system.scheduler": "\u5B9A\u65F6\u4EFB\u52A1\u670D\u52A1",
19129
+ "system.watchdog": "\u8FD0\u884C\u76D1\u63A7",
18495
19130
  "system.maxConcurrency": "\u6700\u5927\u5E76\u53D1",
18496
19131
  "system.pollInterval": "\u8F6E\u8BE2\u95F4\u9694",
18497
19132
  "system.heartbeatInterval": "\u5FC3\u8DF3\u95F4\u9694",
18498
19133
  "system.taskTimeout": "\u4EFB\u52A1\u8D85\u65F6",
18499
- "system.schedulerEnabled": "\u542F\u7528\u8C03\u5EA6",
19134
+ "system.schedulerEnabled": "\u542F\u7528\u5B9A\u65F6\u4EFB\u52A1",
18500
19135
  "system.checkInterval": "\u68C0\u67E5\u95F4\u9694",
18501
19136
  "system.heartbeatTimeout": "\u5FC3\u8DF3\u8D85\u65F6",
18502
19137
  "system.cleanupInterval": "\u6E05\u7406\u95F4\u9694",
@@ -18506,8 +19141,12 @@ var ZH = {
18506
19141
  "system.minutes": "\u5206\u949F",
18507
19142
  "system.hours": "\u5C0F\u65F6",
18508
19143
  "system.days": "\u5929",
18509
- "system.activeTemplates": "\u6D3B\u8DC3\u6A21\u677F",
18510
- "system.saveHint": "\u4FDD\u5B58\u540E\u9700\u91CD\u542F Gateway \u751F\u6548",
19144
+ "system.activeTemplates": "\u5DF2\u542F\u7528\u5B9A\u65F6\u4EFB\u52A1",
19145
+ "system.saveHint": "\u8BBE\u7F6E\u4FDD\u5B58\u540E\u9700\u8981\u91CD\u542F Gateway \u624D\u80FD\u751F\u6548\u3002",
19146
+ "system.configApplied": "\u9875\u9762\u4E2D\u7684\u8BBE\u7F6E\u6B63\u5728\u751F\u6548\u3002",
19147
+ "system.configPending": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u4F46\u5F53\u524D Gateway \u4ECD\u5728\u4F7F\u7528\u4E0A\u6B21\u542F\u52A8\u65F6\u7684\u8BBE\u7F6E\u3002",
19148
+ "system.configForeground": "\u6B64\u9875\u9762\u672A\u8FDE\u63A5\u5230 Gateway \u7684\u8FD0\u884C\u914D\u7F6E\uFF1B\u4FDD\u5B58\u540E\u8BF7\u91CD\u542F Gateway\u3002",
19149
+ "system.configRestartManually": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u4F46\u5C1A\u672A\u751F\u6548\u3002\u8BF7\u56DE\u5230\u542F\u52A8 Gateway \u7684\u7EC8\u7AEF\uFF0C\u6309 Ctrl-C \u540E\u91CD\u65B0\u8FD0\u884C supertask gateway\u3002",
18511
19150
  "system.runningTasks": "\u5F53\u524D\u8FD0\u884C\u4EFB\u52A1\uFF08{running} / {limit} \u5E76\u53D1\uFF09",
18512
19151
  "system.taskStats": "\u4EFB\u52A1\u6982\u89C8",
18513
19152
  "system.configFile": "\u914D\u7F6E\u6587\u4EF6",
@@ -18516,7 +19155,46 @@ var ZH = {
18516
19155
  "system.yes": "\u662F",
18517
19156
  "system.noDefault": "\u5426\uFF0C\u5F53\u524D\u4F7F\u7528\u9ED8\u8BA4\u503C",
18518
19157
  "system.danger": "\u5371\u9669\u64CD\u4F5C",
18519
- "system.dangerDescription": "\u7CFB\u7EDF\u4F1A\u5148\u521B\u5EFA\u53EF\u6821\u9A8C\u5907\u4EFD\uFF0C\u518D\u4E8B\u52A1\u6027\u6E05\u7A7A\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\uFF1B\u5B58\u5728\u8FD0\u884C\u4EFB\u52A1\u65F6\u4F1A\u62D2\u7EDD\u64CD\u4F5C\u3002",
19158
+ "system.dangerDescription": "\u7CFB\u7EDF\u4F1A\u5148\u521B\u5EFA\u53EF\u6821\u9A8C\u5907\u4EFD\uFF0C\u518D\u4E8B\u52A1\u6027\u6E05\u7A7A\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u5B9A\u65F6\u4EFB\u52A1\uFF1B\u5B58\u5728\u8FD0\u884C\u4EFB\u52A1\u65F6\u4F1A\u62D2\u7EDD\u64CD\u4F5C\u3002",
19159
+ "template.createTitle": "\u65B0\u5EFA\u5B9A\u65F6\u4EFB\u52A1",
19160
+ "template.editTitle": "\u7F16\u8F91\u5B9A\u65F6\u4EFB\u52A1",
19161
+ "template.formSubtitle": "\u8BBE\u7F6E\u4F55\u65F6\u8FD0\u884C\uFF0C\u4EE5\u53CA OpenCode \u8981\u4F7F\u7528\u7684\u9879\u76EE\u3001\u6A21\u578B\u548C\u63D0\u793A\u8BCD\u3002",
19162
+ "template.name": "\u540D\u79F0",
19163
+ "template.cwd": "\u9879\u76EE\u76EE\u5F55",
19164
+ "template.cwdHint": "OpenCode \u4F1A\u5728\u8FD9\u4E2A\u76EE\u5F55\u4E2D\u6267\u884C\u4EFB\u52A1\u3002",
19165
+ "template.agent": "Agent",
19166
+ "template.model": "\u6A21\u578B",
19167
+ "template.prompt": "\u63D0\u793A\u8BCD",
19168
+ "template.scheduleType": "\u6267\u884C\u65B9\u5F0F",
19169
+ "template.cronExpr": "Cron \u8868\u8FBE\u5F0F",
19170
+ "template.cronHint": "\u4F8B\u5982\uFF1A0 9 * * *\uFF08\u6BCF\u5929 09:00\uFF09",
19171
+ "template.interval": "\u6267\u884C\u95F4\u9694",
19172
+ "template.runAt": "\u6267\u884C\u65F6\u95F4",
19173
+ "template.durationHint": "\u652F\u6301 30s\u30015min\u30011h\u30012d",
19174
+ "template.advanced": "\u66F4\u591A\u6267\u884C\u8BBE\u7F6E",
19175
+ "template.category": "\u5206\u7C7B",
19176
+ "template.batchId": "\u6279\u6B21 ID",
19177
+ "template.importance": "\u91CD\u8981\u7A0B\u5EA6\uFF081-5\uFF09",
19178
+ "template.urgency": "\u7D27\u6025\u7A0B\u5EA6\uFF081-5\uFF09",
19179
+ "template.maxInstances": "\u81EA\u52A8\u8C03\u5EA6\u4E0A\u9650",
19180
+ "template.maxInstancesHint": "\u4EC5\u9650\u5236\u81EA\u52A8\u8C03\u5EA6\uFF1B\u624B\u52A8\u201C\u7ACB\u5373\u8FD0\u884C\u4E00\u6B21\u201D\u59CB\u7EC8\u52A0\u5165\u961F\u5217\u3002\u6D3B\u8DC3\u4EFB\u52A1\u5305\u542B\u5F85\u6267\u884C\u3001\u8FD0\u884C\u4E2D\u548C\u7B49\u5F85\u91CD\u8BD5\u3002",
19181
+ "template.maxRetries": "\u5931\u8D25\u91CD\u8BD5\u6B21\u6570",
19182
+ "template.retryBackoff": "\u91CD\u8BD5\u7B49\u5F85",
19183
+ "template.timeout": "\u5355\u6B21\u8D85\u65F6",
19184
+ "template.optional": "\u53EF\u9009",
19185
+ "template.futureOnly": "\u4FDD\u5B58\u540E\u7684\u8BBE\u7F6E\u7528\u4E8E\u4EE5\u540E\u751F\u6210\u7684\u4EFB\u52A1\uFF1B\u7F16\u8F91\u4E0D\u4F1A\u6539\u53D8\u5DF2\u7ECF\u8FDB\u5165\u961F\u5217\u6216\u6B63\u5728\u6267\u884C\u7684\u4EFB\u52A1\u3002",
19186
+ "projects.title": "\u9879\u76EE\u5206\u7EC4",
19187
+ "projects.description": "\u9879\u76EE\u76EE\u5F55\u662F\u9694\u79BB\u952E\uFF1B\u5148\u770B\u6BCF\u4E2A\u9879\u76EE\u6709\u6CA1\u6709\u8FD0\u884C\u4E2D\u6216\u6392\u961F\u4EFB\u52A1\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u7EE7\u7EED\u6DFB\u52A0\u3002",
19188
+ "projects.all": "\u5168\u90E8\u9879\u76EE",
19189
+ "projects.legacy": "\u672A\u5206\u7EC4",
19190
+ "projects.legacyHint": "\u65E7\u7248\u672C\u4E2D\u6CA1\u6709\u9879\u76EE\u76EE\u5F55\u7684\u4EFB\u52A1\uFF1B\u53EF\u67E5\u770B\u3001\u53D6\u6D88\u6216\u5220\u9664\uFF0C\u9700\u8981\u4FEE\u6539\u65F6\u8BF7\u5728\u6B63\u786E\u9879\u76EE\u76EE\u5F55\u4E0B\u91CD\u65B0\u521B\u5EFA\u3002",
19191
+ "projects.counts": "\u8FD0\u884C {running} \xB7 \u6392\u961F {pending} \xB7 \u5F02\u5E38 {failed}",
19192
+ "task.createTitle": "\u65B0\u5EFA\u4EFB\u52A1",
19193
+ "task.editTitle": "\u7F16\u8F91\u4EFB\u52A1",
19194
+ "task.formSubtitle": "\u4EFB\u52A1\u4F1A\u7ACB\u5373\u52A0\u5165\u6301\u4E45\u961F\u5217\uFF1B\u5E76\u53D1\u5DF2\u6EE1\u65F6\u4F1A\u81EA\u52A8\u7B49\u5F85\u3002",
19195
+ "task.batchHint": "\u76F8\u540C\u975E\u7A7A\u6279\u6B21 ID \u7684\u4EFB\u52A1\u4E25\u683C\u4E32\u884C\uFF1B\u7559\u7A7A\u5219\u4E0D\u53D7\u6279\u6B21\u4E32\u884C\u9650\u5236\uFF0C\u4F46\u4ECD\u53D7\u5168\u5C40\u5E76\u53D1\u548C\u4F9D\u8D56\u7EA6\u675F\u3002",
19196
+ "task.projectExisting": "\u6B64\u9879\u76EE\u73B0\u6709 {total} \u4E2A\u4EFB\u52A1\uFF1A\u8FD0\u884C {running}\uFF0C\u6392\u961F {pending}\uFF0C\u5F02\u5E38 {failed}\u3002",
19197
+ "task.projectNew": "\u8FD9\u662F\u4E00\u4E2A\u65B0\u9879\u76EE\u5206\u7EC4\uFF1B\u521B\u5EFA\u540E\u4F1A\u51FA\u73B0\u5728\u9879\u76EE\u5217\u8868\u4E2D\u3002",
18520
19198
  "theme.label": "\u4E3B\u9898",
18521
19199
  "theme.system": "\u8DDF\u968F\u7CFB\u7EDF",
18522
19200
  "theme.light": "\u6D45\u8272",
@@ -18531,21 +19209,31 @@ var ZH = {
18531
19209
  "dialog.retryTaskBody": "\u4EFB\u52A1\u5C06\u56DE\u5230\u5F85\u6267\u884C\u72B6\u6001\uFF0C\u5E76\u91CD\u7F6E\u81EA\u52A8\u91CD\u8BD5\u9884\u7B97\u3002",
18532
19210
  "dialog.deleteTask": "\u5220\u9664\u4EFB\u52A1 #{id}\uFF1F",
18533
19211
  "dialog.deleteTaskBody": "\u4EFB\u52A1\u53CA\u5173\u8054\u6267\u884C\u8BB0\u5F55\u5C06\u6C38\u4E45\u5220\u9664\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
18534
- "dialog.disableTemplate": "\u7981\u7528\u8FD9\u4E2A\u8C03\u5EA6\u6A21\u677F\uFF1F",
18535
- "dialog.disableTemplateBody": "\u6A21\u677F\u5C06\u505C\u6B62\u81EA\u52A8\u521B\u5EFA\u65B0\u4EFB\u52A1\uFF0C\u5DF2\u6709\u4EFB\u52A1\u4E0D\u53D7\u5F71\u54CD\u3002",
18536
- "dialog.deleteTemplate": "\u5220\u9664\u8FD9\u4E2A\u8C03\u5EA6\u6A21\u677F\uFF1F",
18537
- "dialog.deleteTemplateBody": "\u6A21\u677F\u914D\u7F6E\u5C06\u6C38\u4E45\u5220\u9664\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
18538
- "dialog.triggerTemplate": "\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21\uFF1F",
18539
- "dialog.triggerTemplateBody": "\u7CFB\u7EDF\u4F1A\u6309\u5F53\u524D\u6A21\u677F\u521B\u5EFA\u4E00\u4E2A\u65B0\u4EFB\u52A1\uFF0C\u5E76\u9075\u5B88\u6700\u5927\u5B9E\u4F8B\u9650\u5236\u3002",
19212
+ "dialog.disableTemplate": "\u7981\u7528\u8FD9\u4E2A\u5B9A\u65F6\u4EFB\u52A1\uFF1F",
19213
+ "dialog.disableTemplateBody": "\u5B83\u5C06\u505C\u6B62\u81EA\u52A8\u521B\u5EFA\u65B0\u4EFB\u52A1\uFF0C\u5DF2\u6709\u4EFB\u52A1\u4E0D\u53D7\u5F71\u54CD\u3002",
19214
+ "dialog.deleteTemplate": "\u5220\u9664\u8FD9\u4E2A\u5B9A\u65F6\u4EFB\u52A1\uFF1F",
19215
+ "dialog.deleteTemplateBody": "\u5B9A\u65F6\u4EFB\u52A1\u914D\u7F6E\u5C06\u6C38\u4E45\u5220\u9664\uFF0C\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
19216
+ "dialog.triggerTemplate": "\u7ACB\u5373\u8FD0\u884C\u4E00\u6B21\uFF1F",
19217
+ "dialog.triggerTemplateBody": "\u7CFB\u7EDF\u4F1A\u6309\u5F53\u524D\u8BBE\u7F6E\u7ACB\u5373\u521B\u5EFA\u4E00\u4E2A\u4EFB\u52A1\u5E76\u52A0\u5165\u961F\u5217\uFF1B\u82E5\u5168\u5C40\u5E76\u53D1\u5DF2\u6EE1\uFF0C\u4EFB\u52A1\u4F1A\u7B49\u5F85\u6267\u884C\u3002",
18540
19218
  "dialog.clearTitle": "\u786E\u8BA4\u6E05\u7A7A\u6570\u636E\u5E93",
18541
- "dialog.clearBody": "\u8FD9\u4F1A\u5220\u9664\u5168\u90E8\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\u3002\u7CFB\u7EDF\u4F1A\u5148\u81EA\u52A8\u5907\u4EFD\u3002",
19219
+ "dialog.clearBody": "\u8FD9\u4F1A\u5220\u9664\u5168\u90E8\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u5B9A\u65F6\u4EFB\u52A1\u3002\u7CFB\u7EDF\u4F1A\u5148\u81EA\u52A8\u5907\u4EFD\u3002",
18542
19220
  "dialog.clearInstruction": "\u8F93\u5165 CLEAR \u4EE5\u786E\u8BA4",
19221
+ "dialog.restartGateway": "\u91CD\u542F\u5E76\u5E94\u7528\u8BBE\u7F6E\uFF1F",
19222
+ "dialog.restartGatewayBody": "Gateway \u4F1A\u77ED\u6682\u79BB\u7EBF\uFF0C\u901A\u5E38\u6570\u79D2\u540E\u7531 PM2 \u81EA\u52A8\u6062\u590D\u3002",
19223
+ "dialog.restartGatewayRunningBody": "\u5F53\u524D\u6709 {count} \u4E2A\u4EFB\u52A1\u5728\u8FD0\u884C\u3002\u7CFB\u7EDF\u4F1A\u5148\u7B49\u5F85\u5B83\u4EEC\u5B8C\u6210\uFF1B\u8D85\u65F6\u540E\uFF0C\u672A\u5B8C\u6210\u4EFB\u52A1\u4F1A\u88AB\u5B89\u5168\u505C\u6B62\u5E76\u653E\u56DE\u961F\u5217\uFF0C\u7136\u540E Gateway \u7531 PM2 \u81EA\u52A8\u6062\u590D\u3002",
18543
19224
  "feedback.retryFailed": "\u91CD\u8BD5\u5931\u8D25",
18544
19225
  "feedback.cancelFailed": "\u53D6\u6D88\u5931\u8D25",
18545
19226
  "feedback.deleteFailed": "\u5220\u9664\u5931\u8D25",
18546
19227
  "feedback.requestFailed": "\u8BF7\u6C42\u5931\u8D25",
18547
19228
  "feedback.triggered": "\u5DF2\u521B\u5EFA\u4EFB\u52A1 #{id}",
18548
- "feedback.configSaved": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u91CD\u542F Gateway \u540E\u751F\u6548",
19229
+ "feedback.taskCreated": "\u4EFB\u52A1 #{id} \u5DF2\u52A0\u5165\u961F\u5217",
19230
+ "feedback.taskUpdated": "\u4EFB\u52A1 #{id} \u5DF2\u66F4\u65B0",
19231
+ "feedback.templateCreated": "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u521B\u5EFA",
19232
+ "feedback.templateUpdated": "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u66F4\u65B0",
19233
+ "feedback.configSaved": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58",
19234
+ "feedback.sessionCommandCopied": "\u4F1A\u8BDD\u547D\u4EE4\u5DF2\u590D\u5236\uFF0C\u53EF\u76F4\u63A5\u7C98\u8D34\u5230\u7EC8\u7AEF\u8FD0\u884C",
19235
+ "feedback.restarting": "Gateway \u6B63\u5728\u91CD\u542F\uFF0C\u8BF7\u7A0D\u5019\u2026",
19236
+ "feedback.restartTimeout": "Gateway \u5C1A\u672A\u6062\u590D\uFF0C\u8BF7\u7A0D\u540E\u5237\u65B0\u6216\u68C0\u67E5 PM2 \u72B6\u6001",
18549
19237
  "feedback.databaseCleared": "\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u5907\u4EFD\u4F4D\u4E8E\uFF1A{path}",
18550
19238
  "feedback.copyFailed": "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9\u5185\u5BB9",
18551
19239
  "a11y.skip": "\u8DF3\u5230\u4E3B\u8981\u5185\u5BB9",
@@ -18558,13 +19246,13 @@ var EN = {
18558
19246
  "app.local": "Running locally",
18559
19247
  "app.footer": "Local-first \xB7 Your data stays on this device",
18560
19248
  "nav.tasks": "Tasks",
18561
- "nav.templates": "Schedules",
19249
+ "nav.templates": "Scheduled tasks",
18562
19250
  "nav.runs": "Runs",
18563
19251
  "nav.system": "System",
18564
19252
  "page.tasks.title": "Task queue",
18565
19253
  "page.tasks.description": "Track priority, execution state, and retries, then act on tasks that need attention.",
18566
- "page.templates.title": "Schedule templates",
18567
- "page.templates.description": "Manage cron, delayed, and recurring work so repeated tasks run on time.",
19254
+ "page.templates.title": "Scheduled tasks",
19255
+ "page.templates.description": "Create and edit cron, fixed-interval, and one-time tasks, including their model, agent, and prompt.",
18568
19256
  "page.runs.title": "Execution history",
18569
19257
  "page.runs.description": "Inspect the status, duration, heartbeat, and output of every agent run.",
18570
19258
  "page.system.title": "System settings",
@@ -18574,29 +19262,43 @@ var EN = {
18574
19262
  "action.retry": "Retry",
18575
19263
  "action.cancel": "Cancel",
18576
19264
  "action.delete": "Delete",
19265
+ "action.edit": "Edit",
18577
19266
  "action.enable": "Enable",
18578
19267
  "action.disable": "Disable",
18579
19268
  "action.trigger": "Run now",
19269
+ "action.continueSession": "Continue session",
18580
19270
  "action.logs": "View log",
18581
19271
  "action.hideLogs": "Hide log",
18582
19272
  "action.save": "Save settings",
19273
+ "action.saveAndRestart": "Save and restart",
18583
19274
  "action.copy": "Copy JSON",
18584
19275
  "action.close": "Close",
18585
19276
  "action.confirm": "Confirm",
18586
19277
  "action.clearDatabase": "Clear database",
19278
+ "action.createTask": "New task",
19279
+ "action.saveTask": "Add to queue",
19280
+ "action.updateTask": "Save changes",
19281
+ "action.createTemplate": "New scheduled task",
19282
+ "action.saveTemplate": "Save scheduled task",
18587
19283
  "status.pending": "Pending",
18588
19284
  "status.running": "Running",
18589
19285
  "status.done": "Done",
18590
- "status.failed": "Failed",
18591
- "status.dead_letter": "Dead letter",
19286
+ "status.failed": "Waiting to retry",
19287
+ "status.dead_letter": "Stopped",
19288
+ "status.deadLetterHint": "The system will not run this task automatically again. Its retries may be exhausted or a dependency cannot continue; inspect the error, then retry manually if appropriate.",
19289
+ "status.deadLetterAction": "Inspect the error, then retry manually",
18592
19290
  "status.cancelled": "Cancelled",
19291
+ "status.executionStillActive": "The execution process is still stopping and temporarily occupies a slot",
18593
19292
  "status.unknown": "Unknown",
19293
+ "runStatus.running": "Running",
19294
+ "runStatus.done": "Succeeded",
19295
+ "runStatus.failed": "Failed",
18594
19296
  "stats.total": "Total tasks",
18595
19297
  "stats.pending": "Pending",
18596
19298
  "stats.running": "Running",
18597
19299
  "stats.done": "Completed",
18598
- "stats.failedDead": "Failed & dead",
18599
- "stats.templates": "Templates",
19300
+ "stats.failedDead": "Waiting to retry & stopped",
19301
+ "stats.templates": "Scheduled tasks",
18600
19302
  "stats.enabled": "Enabled",
18601
19303
  "stats.disabled": "Disabled",
18602
19304
  "stats.records": "Total runs",
@@ -18604,7 +19306,7 @@ var EN = {
18604
19306
  "stats.pageFailed": "Failed here",
18605
19307
  "stats.pageRunning": "Running here",
18606
19308
  "filter.all": "All",
18607
- "filter.searchTasks": "Search tasks, agents, or prompts on this page",
19309
+ "filter.searchTasks": "Search tasks, projects, batches, agents, or prompts on this page",
18608
19310
  "filter.noResults": "No tasks match this search",
18609
19311
  "table.id": "ID",
18610
19312
  "table.task": "Task",
@@ -18628,14 +19330,14 @@ var EN = {
18628
19330
  "pagination.next": "Next",
18629
19331
  "pagination.summary": "Page {page} of {pages} \xB7 {total} items",
18630
19332
  "empty.tasks": "Your queue is empty",
18631
- "empty.tasksHint": "Create the first task with the OpenCode plugin or supertask add.",
18632
- "empty.templates": "No schedule templates yet",
18633
- "empty.templatesHint": "Create one with: supertask template add",
19333
+ "empty.tasksHint": "Select \u201CNew task\u201D, or use the OpenCode plugin or supertask add.",
19334
+ "empty.templates": "No scheduled tasks yet",
19335
+ "empty.templatesHint": "Select \u201CNew scheduled task\u201D to create one.",
18634
19336
  "empty.runs": "No execution history yet",
18635
19337
  "empty.running": "No tasks are running right now",
18636
19338
  "schedule.cron": "Cron",
18637
- "schedule.recurring": "Recurring",
18638
- "schedule.delayed": "Delayed",
19339
+ "schedule.recurring": "Fixed interval",
19340
+ "schedule.delayed": "One-time",
18639
19341
  "schedule.unknown": "Unknown",
18640
19342
  "schedule.enabled": "Enabled",
18641
19343
  "schedule.disabled": "Disabled",
@@ -18644,14 +19346,14 @@ var EN = {
18644
19346
  "schedule.hours": "{count} hr",
18645
19347
  "schedule.days": "{count} days",
18646
19348
  "schedule.overdue": "Overdue",
18647
- "system.worker": "Worker",
18648
- "system.scheduler": "Scheduler",
18649
- "system.watchdog": "Watchdog",
19349
+ "system.worker": "Task execution",
19350
+ "system.scheduler": "Scheduled task service",
19351
+ "system.watchdog": "Runtime monitor",
18650
19352
  "system.maxConcurrency": "Max concurrency",
18651
19353
  "system.pollInterval": "Poll interval",
18652
19354
  "system.heartbeatInterval": "Heartbeat interval",
18653
19355
  "system.taskTimeout": "Task timeout",
18654
- "system.schedulerEnabled": "Enable scheduler",
19356
+ "system.schedulerEnabled": "Enable scheduled tasks",
18655
19357
  "system.checkInterval": "Check interval",
18656
19358
  "system.heartbeatTimeout": "Heartbeat timeout",
18657
19359
  "system.cleanupInterval": "Cleanup interval",
@@ -18661,8 +19363,12 @@ var EN = {
18661
19363
  "system.minutes": "minutes",
18662
19364
  "system.hours": "hours",
18663
19365
  "system.days": "days",
18664
- "system.activeTemplates": "Active templates",
18665
- "system.saveHint": "Restart Gateway to apply saved settings",
19366
+ "system.activeTemplates": "Enabled scheduled tasks",
19367
+ "system.saveHint": "Restart Gateway to apply saved settings.",
19368
+ "system.configApplied": "The settings shown here are active.",
19369
+ "system.configPending": "Settings are saved, but this Gateway is still using the values from its last start.",
19370
+ "system.configForeground": "This page is not connected to Gateway runtime settings. Restart Gateway after saving.",
19371
+ "system.configRestartManually": "Settings are saved but not active. Return to the terminal that launched Gateway, press Ctrl-C, then run supertask gateway again.",
18666
19372
  "system.runningTasks": "Active tasks ({running} / {limit} concurrent)",
18667
19373
  "system.taskStats": "Task overview",
18668
19374
  "system.configFile": "Configuration file",
@@ -18672,6 +19378,45 @@ var EN = {
18672
19378
  "system.noDefault": "No, using defaults",
18673
19379
  "system.danger": "Danger zone",
18674
19380
  "system.dangerDescription": "A verified backup is created first, then tasks, runs, and templates are cleared transactionally. Active work blocks this operation.",
19381
+ "template.createTitle": "New scheduled task",
19382
+ "template.editTitle": "Edit scheduled task",
19383
+ "template.formSubtitle": "Choose when to run and which project, model, and prompt OpenCode should use.",
19384
+ "template.name": "Name",
19385
+ "template.cwd": "Project directory",
19386
+ "template.cwdHint": "OpenCode runs the task in this directory.",
19387
+ "template.agent": "Agent",
19388
+ "template.model": "Model",
19389
+ "template.prompt": "Prompt",
19390
+ "template.scheduleType": "Schedule",
19391
+ "template.cronExpr": "Cron expression",
19392
+ "template.cronHint": "Example: 0 9 * * * (daily at 09:00)",
19393
+ "template.interval": "Interval",
19394
+ "template.runAt": "Run at",
19395
+ "template.durationHint": "Supports 30s, 5min, 1h, 2d",
19396
+ "template.advanced": "More execution settings",
19397
+ "template.category": "Category",
19398
+ "template.batchId": "Batch ID",
19399
+ "template.importance": "Importance (1-5)",
19400
+ "template.urgency": "Urgency (1-5)",
19401
+ "template.maxInstances": "Automatic scheduling limit",
19402
+ "template.maxInstancesHint": "Limits automatic scheduling only. \u201CRun now\u201D always queues a task. Active instances include pending, running, and retry-waiting tasks.",
19403
+ "template.maxRetries": "Failure retries",
19404
+ "template.retryBackoff": "Retry delay",
19405
+ "template.timeout": "Run timeout",
19406
+ "template.optional": "Optional",
19407
+ "template.futureOnly": "Saved settings apply to tasks created in the future. Editing does not change queued or running tasks.",
19408
+ "projects.title": "Projects",
19409
+ "projects.description": "The project directory is the isolation key. Check active and queued work before adding more.",
19410
+ "projects.all": "All projects",
19411
+ "projects.legacy": "Ungrouped",
19412
+ "projects.legacyHint": "Legacy tasks without a project directory. View, cancel, or delete them; recreate under the correct project to make changes.",
19413
+ "projects.counts": "Running {running} \xB7 queued {pending} \xB7 issues {failed}",
19414
+ "task.createTitle": "New task",
19415
+ "task.editTitle": "Edit task",
19416
+ "task.formSubtitle": "The task enters the durable queue immediately and waits automatically when concurrency is full.",
19417
+ "task.batchHint": "Tasks with the same non-empty batch ID run serially. Blank removes the batch constraint; global concurrency and dependencies still apply.",
19418
+ "task.projectExisting": "This project has {total} tasks: {running} running, {pending} queued, and {failed} with issues.",
19419
+ "task.projectNew": "This is a new project group. It appears in the project list after creation.",
18675
19420
  "theme.label": "Theme",
18676
19421
  "theme.system": "System",
18677
19422
  "theme.light": "Light",
@@ -18689,18 +19434,28 @@ var EN = {
18689
19434
  "dialog.disableTemplate": "Disable this schedule?",
18690
19435
  "dialog.disableTemplateBody": "It will stop creating new tasks automatically. Existing tasks are unchanged.",
18691
19436
  "dialog.deleteTemplate": "Delete this schedule?",
18692
- "dialog.deleteTemplateBody": "This template configuration will be permanently deleted.",
19437
+ "dialog.deleteTemplateBody": "This scheduled task configuration will be permanently deleted.",
18693
19438
  "dialog.triggerTemplate": "Run this schedule now?",
18694
- "dialog.triggerTemplateBody": "A new task is created from the template, subject to its instance limit.",
19439
+ "dialog.triggerTemplateBody": "A task is queued immediately using the current settings. If global concurrency is full, it waits to run.",
18695
19440
  "dialog.clearTitle": "Confirm database clear",
18696
- "dialog.clearBody": "This deletes every task, run, and schedule template after creating a backup.",
19441
+ "dialog.clearBody": "This deletes every task, run, and scheduled task after creating a backup.",
18697
19442
  "dialog.clearInstruction": "Type CLEAR to confirm",
19443
+ "dialog.restartGateway": "Restart and apply settings?",
19444
+ "dialog.restartGatewayBody": "Gateway will be briefly unavailable and should recover through PM2 within a few seconds.",
19445
+ "dialog.restartGatewayRunningBody": "{count} tasks are running. The system waits for them first; after the grace period, unfinished tasks are stopped safely and returned to the queue before PM2 restores Gateway.",
18698
19446
  "feedback.retryFailed": "Retry failed",
18699
19447
  "feedback.cancelFailed": "Cancellation failed",
18700
19448
  "feedback.deleteFailed": "Delete failed",
18701
19449
  "feedback.requestFailed": "Request failed",
18702
19450
  "feedback.triggered": "Task #{id} created",
18703
- "feedback.configSaved": "Settings saved. Restart Gateway to apply them.",
19451
+ "feedback.taskCreated": "Task #{id} added to the queue",
19452
+ "feedback.taskUpdated": "Task #{id} updated",
19453
+ "feedback.templateCreated": "Scheduled task created",
19454
+ "feedback.templateUpdated": "Scheduled task updated",
19455
+ "feedback.configSaved": "Settings saved",
19456
+ "feedback.sessionCommandCopied": "Session command copied. Paste it into a terminal to continue.",
19457
+ "feedback.restarting": "Gateway is restarting\u2026",
19458
+ "feedback.restartTimeout": "Gateway has not recovered yet. Refresh later or check PM2 status.",
18704
19459
  "feedback.databaseCleared": "Database cleared. Backup: {path}",
18705
19460
  "feedback.copyFailed": "Copy failed. Select the content manually.",
18706
19461
  "a11y.skip": "Skip to main content",
@@ -18714,6 +19469,14 @@ function statusText(locale, status) {
18714
19469
  const key = `status.${status}`;
18715
19470
  return key in ZH ? t(locale, key) : t(locale, "status.unknown");
18716
19471
  }
19472
+ function runStatusText(locale, status) {
19473
+ const key = `runStatus.${status}`;
19474
+ return key in ZH ? t(locale, key) : t(locale, "status.unknown");
19475
+ }
19476
+ function resolveEditedRunAt(originalEpoch, originalLocal, currentLocal) {
19477
+ if (originalEpoch !== null && originalLocal === currentLocal) return originalEpoch;
19478
+ return new Date(currentLocal).getTime();
19479
+ }
18717
19480
  function formatRelative(timestamp2, locale) {
18718
19481
  if (!timestamp2) return "\u2014";
18719
19482
  const deltaMs = timestamp2 - Date.now();
@@ -18793,8 +19556,8 @@ var STYLES = `
18793
19556
  radial-gradient(circle at 10% -10%,var(--bg-glow),transparent 34rem),var(--bg);
18794
19557
  font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
18795
19558
  font-size:14px; line-height:1.5; -webkit-font-smoothing:antialiased; }
18796
- button,input,select { font:inherit; }
18797
- button,a,select,input { -webkit-tap-highlight-color:transparent; }
19559
+ button,input,select,textarea { font:inherit; }
19560
+ button,a,select,input,textarea { -webkit-tap-highlight-color:transparent; }
18798
19561
  a { color:inherit; }
18799
19562
  code,.mono,.m { font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace; }
18800
19563
  .skip-link { position:fixed; left:16px; top:-60px; z-index:200; padding:10px 14px; border-radius:8px;
@@ -18875,6 +19638,16 @@ var STYLES = `
18875
19638
  .panel-head { min-height:52px; display:flex; align-items:center; justify-content:space-between; gap:12px; padding:0 18px;
18876
19639
  border-bottom:1px solid var(--border); }
18877
19640
  .panel-head h2,.panel-head h3 { margin:0; font-size:14px; letter-spacing:-.01em; }
19641
+ .panel-head p { margin:3px 0 0; color:var(--text-2); font-size:11px; }
19642
+ .project-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:10px; padding:14px; }
19643
+ .project-card { min-width:0; padding:12px; border:1px solid var(--border); border-radius:10px; color:var(--text); background:var(--surface-2); text-decoration:none; }
19644
+ .project-card:hover { border-color:var(--border-strong); background:var(--surface-3); }
19645
+ .project-card.active { border-color:color-mix(in srgb,var(--primary) 45%,var(--border)); background:var(--primary-soft); }
19646
+ .project-card-head { display:flex; align-items:center; justify-content:space-between; gap:10px; }
19647
+ .project-card-head strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
19648
+ .project-card-head span { color:var(--text-3); font-size:11px; }
19649
+ .project-path { margin-top:4px; overflow:hidden; color:var(--text-3); font-family:"SFMono-Regular",Consolas,monospace; font-size:10px; text-overflow:ellipsis; white-space:nowrap; }
19650
+ .project-counts { margin-top:8px; color:var(--text-2); font-size:11px; }
18878
19651
  .table-wrap { width:100%; overflow-x:auto; }
18879
19652
  table { width:100%; border-collapse:separate; border-spacing:0; font-size:13px; }
18880
19653
  th { height:42px; padding:0 13px; color:var(--text-3); background:var(--surface-2); border-bottom:1px solid var(--border);
@@ -18885,6 +19658,7 @@ var STYLES = `
18885
19658
  tbody tr:hover { background:color-mix(in srgb,var(--primary-soft) 30%,transparent); }
18886
19659
  .task-name { font-weight:680; color:var(--text); }
18887
19660
  .task-prompt { max-width:520px; margin-top:3px; color:var(--text-2); font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
19661
+ .task-context { margin-top:6px; }
18888
19662
  .muted,.mu { color:var(--text-2); }
18889
19663
  .faint { color:var(--text-3); }
18890
19664
  .small,.sm { font-size:12px; }
@@ -18968,6 +19742,18 @@ var STYLES = `
18968
19742
  .dialog-head p { margin:3px 0 0; color:var(--text-2); font-size:11px; }
18969
19743
  .dialog-body { max-height:70vh; overflow:auto; padding:18px; }
18970
19744
  .dialog-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; padding:14px 18px; border-top:1px solid var(--border); }
19745
+ .template-dialog { width:min(880px,calc(100% - 32px)); }
19746
+ .template-form-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:15px; }
19747
+ .form-field { display:flex; min-width:0; flex-direction:column; gap:6px; color:var(--text-2); font-size:12px; font-weight:650; }
19748
+ .form-field-wide { grid-column:1 / -1; }
19749
+ .form-field input,.form-field select,.form-field textarea { width:100%; min-height:39px; padding:8px 10px; border:1px solid var(--border); border-radius:9px;
19750
+ outline:none; color:var(--text); background:var(--surface-2); font-weight:450; }
19751
+ .form-field textarea { resize:vertical; line-height:1.5; }
19752
+ .form-field input:focus,.form-field select:focus,.form-field textarea:focus { border-color:var(--primary); box-shadow:var(--focus); background:var(--surface); }
19753
+ .form-field small { color:var(--text-3); font-size:10px; font-weight:450; }
19754
+ .advanced-fields { margin-top:18px; padding-top:14px; border-top:1px solid var(--border); }
19755
+ .advanced-fields summary { margin-bottom:14px; color:var(--text-2); cursor:pointer; font-size:12px; font-weight:700; }
19756
+ .form-note { margin:16px 0 0; color:var(--text-3); font-size:11px; }
18971
19757
  .json-view { min-height:160px; margin:0; padding:15px; overflow:auto; border:1px solid var(--border); border-radius:10px; color:var(--text-2);
18972
19758
  background:var(--surface-2); font-size:12px; white-space:pre-wrap; overflow-wrap:anywhere; }
18973
19759
  .confirm-copy { color:var(--text-2); margin:0; }
@@ -19033,6 +19819,9 @@ var STYLES = `
19033
19819
  .responsive-table td[data-primary]::before { display:none; }
19034
19820
  .responsive-table .task-prompt { max-width:100%; }
19035
19821
  .responsive-table .actions { justify-content:flex-start; }
19822
+ .project-grid { grid-template-columns:1fr; }
19823
+ .template-form-grid { grid-template-columns:1fr; }
19824
+ .form-field-wide { grid-column:auto; }
19036
19825
  }
19037
19826
  @media (max-width:520px) {
19038
19827
  .stats-grid,.stats-grid.three { grid-template-columns:1fr 1fr; }
@@ -19079,13 +19868,31 @@ function clientMessages(locale) {
19079
19868
  "dialog.clearTitle",
19080
19869
  "dialog.clearBody",
19081
19870
  "dialog.clearInstruction",
19871
+ "dialog.restartGateway",
19872
+ "dialog.restartGatewayBody",
19873
+ "dialog.restartGatewayRunningBody",
19082
19874
  "feedback.retryFailed",
19083
19875
  "feedback.cancelFailed",
19084
19876
  "feedback.deleteFailed",
19085
19877
  "feedback.requestFailed",
19086
19878
  "feedback.triggered",
19879
+ "feedback.taskCreated",
19880
+ "feedback.taskUpdated",
19881
+ "task.projectExisting",
19882
+ "task.projectNew",
19883
+ "task.createTitle",
19884
+ "task.editTitle",
19885
+ "action.saveTask",
19886
+ "action.updateTask",
19087
19887
  "feedback.configSaved",
19088
19888
  "feedback.databaseCleared",
19889
+ "feedback.templateCreated",
19890
+ "feedback.templateUpdated",
19891
+ "feedback.sessionCommandCopied",
19892
+ "feedback.restarting",
19893
+ "feedback.restartTimeout",
19894
+ "template.createTitle",
19895
+ "template.editTitle",
19089
19896
  "filter.noResults"
19090
19897
  ];
19091
19898
  return Object.fromEntries(keys.map((key) => [key, t(locale, key)]));
@@ -19179,6 +19986,7 @@ function renderLayout(options) {
19179
19986
  themeMedia.addEventListener?.('change',()=>{if((localStorage.getItem('supertask-theme')||'system')==='system')applyTheme('system');});
19180
19987
  applyTheme(localStorage.getItem('supertask-theme')||'system');
19181
19988
  requestAnimationFrame(()=>document.documentElement.classList.add('ui-ready'));
19989
+ ${resolveEditedRunAt.toString()}
19182
19990
  function refreshPage(button){button.classList.add('refreshing');button.setAttribute('aria-label','${t(locale, "a11y.refreshing")}');location.reload();}
19183
19991
  function showToast(message,type='ok'){const region=document.getElementById('toast-region');const node=document.createElement('div');node.className='toast '+type;node.innerHTML=(type==='error'?'${icon("alert")}':'${icon("check")}')+'<span></span>';node.querySelector('span').textContent=message;region.appendChild(node);setTimeout(()=>{node.classList.add('leaving');setTimeout(()=>node.remove(),220)},3600);}
19184
19992
  async function readJson(response){const data=await response.json().catch(()=>({}));if(!response.ok)throw new Error(data.error||text('feedback.requestFailed'));return data;}
@@ -19190,6 +19998,22 @@ function renderLayout(options) {
19190
19998
  async function showRecord(url){try{const data=await readJson(await fetch(url));document.getElementById('detail-content').textContent=JSON.stringify(data,null,2);document.getElementById('detail-dialog').showModal()}catch(error){showToast(error.message,'error')}}
19191
19999
  const showDetail=id=>showRecord('/api/tasks/'+id);const showRunDetail=id=>showRecord('/api/runs/'+id);const showTemplateDetail=id=>showRecord('/api/templates/'+id);
19192
20000
  async function copyDetails(){try{await navigator.clipboard.writeText(document.getElementById('detail-content').textContent);showToast(text('details.copySuccess'))}catch{showToast(text('feedback.copyFailed'),'error')}}
20001
+ async function copySessionCommand(id){try{const data=await readJson(await fetch('/api/runs/'+id+'/session-command'));await navigator.clipboard.writeText(data.command);showToast(text('feedback.sessionCommandCopied'))}catch(error){showToast(error.message||text('feedback.copyFailed'),'error')}}
20002
+ function taskField(name){return document.getElementById('task-'+name)}
20003
+ function taskProjects(){const node=document.getElementById('task-project-data');if(!node)return {};try{return JSON.parse(node.textContent||'{}')}catch{return {}}}
20004
+ 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')}
20005
+ function openTaskCreator(){const form=document.getElementById('task-form');form.reset();taskField('id').value='';taskField('cwd').readOnly=false;taskField('cwd').value=form.dataset.defaultCwd||'';taskField('dialog-title').textContent=text('task.createTitle');taskField('save').textContent=text('action.saveTask');updateTaskProjectStatus();document.getElementById('task-dialog').showModal();setTimeout(()=>taskField('name').focus(),50)}
20006
+ async function openTaskEditor(id){try{const data=await readJson(await fetch('/api/tasks/'+id));taskField('id').value=String(id);taskField('name').value=data.name||'';taskField('cwd').value=data.cwd||'';taskField('cwd').readOnly=true;taskField('agent').value=data.agent||'';taskField('model').value=data.model||'default';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);taskField('retry-backoff').value=durationInput(data.retryBackoffMs??30000);taskField('timeout').value=durationInput(data.timeoutMs);taskField('dialog-title').textContent=text('task.editTitle');taskField('save').textContent=text('action.updateTask');updateTaskProjectStatus();document.getElementById('task-dialog').showModal();setTimeout(()=>taskField('name').focus(),50)}catch(error){showToast(error.message,'error')}}
20007
+ 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:taskField('retry-backoff').value,timeout:taskField('timeout').value};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}}
20008
+ function templateField(name){return document.getElementById('template-'+name)}
20009
+ function durationInput(milliseconds){if(milliseconds==null)return '';if(milliseconds===0)return '0';const units=[['d',86400000],['h',3600000],['min',60000],['s',1000],['ms',1]];for(const [unit,factor] of units){if(milliseconds%factor===0)return String(milliseconds/factor)+unit}return String(milliseconds)+'ms'}
20010
+ function localDateTime(milliseconds){const date=new Date(milliseconds);const local=new Date(milliseconds-date.getTimezoneOffset()*60000);return local.toISOString().slice(0,23)}
20011
+ 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;node.querySelector('input').required=name===type}}
20012
+ 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}
20013
+ function selectedRunAt(){const input=templateField('run-at');return resolveEditedRunAt(input.dataset.originalEpoch?Number(input.dataset.originalEpoch):null,input.dataset.originalLocal||'',input.value)}
20014
+ function openTemplateCreator(){const form=document.getElementById('template-form');form.reset();templateField('id').value='';templateField('dialog-title').textContent=text('template.createTitle');setOriginalRunAt(null);templateField('run-at').value=localDateTime(Date.now()+3600000);updateTemplateScheduleFields();document.getElementById('template-dialog').showModal();setTimeout(()=>templateField('name').focus(),50)}
20015
+ 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('agent').value=data.agent||'';templateField('model').value=data.model||'default';templateField('prompt').value=data.prompt||'';templateField('schedule-type').value=data.scheduleType;templateField('cron').value=data.cronExpr||'';templateField('interval').value=durationInput(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);templateField('retry-backoff').value=durationInput(data.retryBackoffMs??30000);templateField('timeout').value=durationInput(data.timeoutMs);updateTemplateScheduleFields();document.getElementById('template-dialog').showModal();setTimeout(()=>templateField('name').focus(),50)}catch(error){showToast(error.message,'error')}}
20016
+ 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:templateField('interval').value,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:templateField('retry-backoff').value,timeout:templateField('timeout').value};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}}
19193
20017
  async function enableTmpl(id){try{await readJson(await fetch('/api/templates/'+id+'/enable',{method:'POST'}));location.reload()}catch(error){showToast(error.message,'error')}}
19194
20018
  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')}}
19195
20019
  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')}}
@@ -19197,7 +20021,9 @@ function renderLayout(options) {
19197
20021
  function toggleLog(id,button){const panel=document.getElementById('log-'+id);const hidden=!panel.hidden;panel.hidden=hidden;button.setAttribute('aria-expanded',String(!hidden));button.textContent=text(hidden?'action.logs':'action.hideLogs');}
19198
20022
  function filterTasks(value){const query=value.trim().toLocaleLowerCase();let visible=0;document.querySelectorAll('[data-task-row]').forEach(row=>{const match=!query||row.dataset.search.toLocaleLowerCase().includes(query);row.hidden=!match;if(match)visible++});const empty=document.getElementById('search-empty');if(empty)empty.hidden=visible!==0;}
19199
20023
  async function clearDatabase(){if(!await askDanger())return;try{const data=await readJson(await fetch('/api/database/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({confirmation:'CLEAR'})}));showToast(text('feedback.databaseCleared',{path:data.backupPath}));setTimeout(()=>location.reload(),1000)}catch(error){showToast(error.message,'error')}}
19200
- async function saveConfig(){const form=document.getElementById('config-form');const data={worker:{maxConcurrency:Number(form.mc.value),pollIntervalMs:Number(form.pi.value),heartbeatIntervalMs:Number(form.hi.value)*1000,taskTimeoutMs:Number(form.to.value)*60000},scheduler:{enabled:form.se.checked,checkIntervalMs:Number(form.si.value)},watchdog:{heartbeatTimeoutMs:Number(form.wt.value)*1000,checkIntervalMs:Number(form.wci.value)*1000,cleanupIntervalMs:Number(form.wcl.value)*3600000,retentionDays:Number(form.rd.value)}};try{await readJson(await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}));showToast(text('feedback.configSaved'))}catch(error){showToast(error.message,'error')}}
20024
+ async function confirmGatewayRestart(runningCount=0){const body=runningCount>0?text('dialog.restartGatewayRunningBody',{count:runningCount}):text('dialog.restartGatewayBody');return await ask(text('dialog.restartGateway'),body)}
20025
+ async function restartGateway(){try{const data=await readJson(await fetch('/api/gateway/restart',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({confirmation:'RESTART'})}));showToast(text('feedback.restarting'));for(let attempt=0;attempt<120;attempt++){await new Promise(resolve=>setTimeout(resolve,500));try{const status=await readJson(await fetch('/api/gateway/status',{cache:'no-store'}));if(status.pid!==data.previousPid&&status.managed&&status.ready&&!status.restartRequired){location.reload();return true}}catch{}}showToast(text('feedback.restartTimeout'),'error');return false}catch(error){showToast(error.message,'error');return false}}
20026
+ async function saveConfig(restartAfterSave=false,runningCount=0){if(restartAfterSave&&!await confirmGatewayRestart(runningCount))return;const form=document.getElementById('config-form');const data={worker:{maxConcurrency:Number(form.mc.value),pollIntervalMs:Number(form.pi.value),heartbeatIntervalMs:Number(form.hi.value)*1000,taskTimeoutMs:Number(form.to.value)*60000},scheduler:{enabled:form.se.checked,checkIntervalMs:Number(form.si.value)},watchdog:{heartbeatTimeoutMs:Number(form.wt.value)*1000,checkIntervalMs:Number(form.wci.value)*1000,cleanupIntervalMs:Number(form.wcl.value)*3600000,retentionDays:Number(form.rd.value)}};try{await readJson(await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}));showToast(text('feedback.configSaved'));if(restartAfterSave){const restarted=await restartGateway();if(!restarted)setTimeout(()=>location.reload(),500)}else{setTimeout(()=>location.reload(),500)}}catch(error){showToast(error.message,'error')}}
19201
20027
  </script>
19202
20028
  </body>
19203
20029
  </html>`;
@@ -19205,6 +20031,7 @@ function renderLayout(options) {
19205
20031
 
19206
20032
  // src/web/index.tsx
19207
20033
  var app = new Hono2();
20034
+ var LEGACY_PROJECT_FILTER = "__supertask_legacy__";
19208
20035
  var TASK_STATUSES = /* @__PURE__ */ new Set([
19209
20036
  "pending",
19210
20037
  "running",
@@ -19213,6 +20040,41 @@ var TASK_STATUSES = /* @__PURE__ */ new Set([
19213
20040
  "dead_letter",
19214
20041
  "cancelled"
19215
20042
  ]);
20043
+ var SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_]+$/;
20044
+ var runtimeConfig = null;
20045
+ var restartScheduled = false;
20046
+ function setDashboardRuntimeConfig(config) {
20047
+ runtimeConfig = config === null ? null : structuredClone(config);
20048
+ }
20049
+ function isValidSessionId(value) {
20050
+ return typeof value === "string" && SESSION_ID_PATTERN.test(value);
20051
+ }
20052
+ function maskSessionId(value) {
20053
+ return isValidSessionId(value) ? `${value.slice(4, 7)}***${value.slice(-3)}` : "\u2014";
20054
+ }
20055
+ function sessionCommand(value) {
20056
+ if (!isValidSessionId(value)) throw new Error("session unavailable");
20057
+ return `opencode --session ${value}`;
20058
+ }
20059
+ function configsEqual(left, right) {
20060
+ return JSON.stringify(left) === JSON.stringify(right);
20061
+ }
20062
+ function resolveDashboardConfigState(runtimeAvailable, restartRequired, managedRestart) {
20063
+ if (!runtimeAvailable) return "foreground";
20064
+ if (!restartRequired) return "applied";
20065
+ return managedRestart ? "pending" : "manual";
20066
+ }
20067
+ function isSafeDashboardRestartTarget(diagnostic, currentPid) {
20068
+ return diagnostic.pm2Installed && diagnostic.processFound && diagnostic.status === "online" && diagnostic.pid === currentPid && diagnostic.ready && diagnostic.scopeMatches;
20069
+ }
20070
+ function canRestartCurrentGateway() {
20071
+ if (runtimeConfig === null) return false;
20072
+ try {
20073
+ return isSafeDashboardRestartTarget(getGatewayDiagnostic(), process.pid);
20074
+ } catch {
20075
+ return false;
20076
+ }
20077
+ }
19216
20078
  function parsePositiveInteger(value) {
19217
20079
  if (!/^\d+$/.test(value)) return null;
19218
20080
  const parsed = Number(value);
@@ -19221,6 +20083,134 @@ function parsePositiveInteger(value) {
19221
20083
  function parseTaskStatus(value) {
19222
20084
  return TASK_STATUSES.has(value) ? value : null;
19223
20085
  }
20086
+ function parseTaskPayload(value) {
20087
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
20088
+ throw new Error("\u8BF7\u6C42\u5185\u5BB9\u5FC5\u987B\u662F\u5BF9\u8C61");
20089
+ }
20090
+ const input = value;
20091
+ const requiredString = (name) => {
20092
+ const field = input[name];
20093
+ if (typeof field !== "string" || !field.trim()) throw new Error(`${name} \u4E0D\u80FD\u4E3A\u7A7A`);
20094
+ return field.trim();
20095
+ };
20096
+ const optionalString = (name, fallback = null) => {
20097
+ const field = input[name];
20098
+ if (field === void 0 || field === null || field === "") return fallback;
20099
+ if (typeof field !== "string") throw new Error(`${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
20100
+ return field.trim() || fallback;
20101
+ };
20102
+ const integer2 = (name, fallback) => {
20103
+ const field = input[name];
20104
+ if (field === void 0 || field === null || field === "") return fallback;
20105
+ if (typeof field !== "number" || !Number.isSafeInteger(field)) {
20106
+ throw new Error(`${name} \u5FC5\u987B\u662F\u6574\u6570`);
20107
+ }
20108
+ return field;
20109
+ };
20110
+ const duration = (name, fallback, allowZero = false) => {
20111
+ const raw2 = optionalString(name, fallback);
20112
+ if (raw2 === null) return null;
20113
+ if (allowZero && raw2 === "0") return 0;
20114
+ if (Number.isFinite(Number(raw2))) {
20115
+ throw new Error(`${name} \u5FC5\u987B\u5E26\u65F6\u95F4\u5355\u4F4D\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
20116
+ }
20117
+ const milliseconds = parseDuration(raw2);
20118
+ if (milliseconds === null || !Number.isSafeInteger(milliseconds)) {
20119
+ throw new Error(`${name} \u683C\u5F0F\u65E0\u6548\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
20120
+ }
20121
+ return milliseconds;
20122
+ };
20123
+ return {
20124
+ name: requiredString("name"),
20125
+ cwd: requiredString("cwd"),
20126
+ agent: requiredString("agent"),
20127
+ model: optionalString("model", "default"),
20128
+ prompt: requiredString("prompt"),
20129
+ category: optionalString("category", "general"),
20130
+ batchId: optionalString("batchId"),
20131
+ importance: integer2("importance", 3),
20132
+ urgency: integer2("urgency", 3),
20133
+ maxRetries: integer2("maxRetries", 3),
20134
+ retryBackoffMs: duration("retryBackoff", "30s", true),
20135
+ timeoutMs: duration("timeout", null)
20136
+ };
20137
+ }
20138
+ function editableTaskPayload(input) {
20139
+ return {
20140
+ name: input.name,
20141
+ agent: input.agent,
20142
+ model: input.model ?? "default",
20143
+ prompt: input.prompt,
20144
+ category: input.category ?? "general",
20145
+ batchId: input.batchId ?? null,
20146
+ importance: input.importance ?? 3,
20147
+ urgency: input.urgency ?? 3,
20148
+ maxRetries: input.maxRetries ?? 3,
20149
+ retryBackoffMs: input.retryBackoffMs ?? 3e4,
20150
+ timeoutMs: input.timeoutMs ?? null
20151
+ };
20152
+ }
20153
+ function parseTemplatePayload(value) {
20154
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
20155
+ throw new Error("\u8BF7\u6C42\u5185\u5BB9\u5FC5\u987B\u662F\u5BF9\u8C61");
20156
+ }
20157
+ const input = value;
20158
+ const requiredString = (name) => {
20159
+ const field = input[name];
20160
+ if (typeof field !== "string" || !field.trim()) throw new Error(`${name} \u4E0D\u80FD\u4E3A\u7A7A`);
20161
+ return field.trim();
20162
+ };
20163
+ const optionalString = (name, fallback = null) => {
20164
+ const field = input[name];
20165
+ if (field === void 0 || field === null || field === "") return fallback;
20166
+ if (typeof field !== "string") throw new Error(`${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
20167
+ return field.trim() || fallback;
20168
+ };
20169
+ const integer2 = (name, fallback) => {
20170
+ const field = input[name];
20171
+ if (field === void 0 || field === null || field === "") return fallback;
20172
+ if (typeof field !== "number" || !Number.isSafeInteger(field)) {
20173
+ throw new Error(`${name} \u5FC5\u987B\u662F\u6574\u6570`);
20174
+ }
20175
+ return field;
20176
+ };
20177
+ const duration = (name, fallback, allowZero = false) => {
20178
+ const raw2 = optionalString(name, fallback);
20179
+ if (raw2 === null) return null;
20180
+ if (allowZero && raw2 === "0") return 0;
20181
+ if (Number.isFinite(Number(raw2))) {
20182
+ throw new Error(`${name} \u5FC5\u987B\u5E26\u65F6\u95F4\u5355\u4F4D\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
20183
+ }
20184
+ const milliseconds = parseDuration(raw2);
20185
+ if (milliseconds === null || !Number.isSafeInteger(milliseconds)) {
20186
+ throw new Error(`${name} \u683C\u5F0F\u65E0\u6548\uFF0C\u8BF7\u4F7F\u7528 30s\u30015min\u30011h \u6216 2d`);
20187
+ }
20188
+ return milliseconds;
20189
+ };
20190
+ const scheduleType = requiredString("scheduleType");
20191
+ if (!["cron", "delayed", "recurring"].includes(scheduleType)) {
20192
+ throw new Error("scheduleType \u5FC5\u987B\u662F cron\u3001delayed \u6216 recurring");
20193
+ }
20194
+ return {
20195
+ name: requiredString("name"),
20196
+ agent: requiredString("agent"),
20197
+ model: optionalString("model", "default"),
20198
+ prompt: requiredString("prompt"),
20199
+ cwd: requiredString("cwd"),
20200
+ category: optionalString("category", "general"),
20201
+ importance: integer2("importance", 3),
20202
+ urgency: integer2("urgency", 3),
20203
+ batchId: optionalString("batchId"),
20204
+ scheduleType,
20205
+ cronExpr: scheduleType === "cron" ? requiredString("cronExpr") : null,
20206
+ intervalMs: scheduleType === "recurring" ? duration("interval", null) : null,
20207
+ runAt: scheduleType === "delayed" ? integer2("runAt", null) : null,
20208
+ maxInstances: integer2("maxInstances", 1),
20209
+ maxRetries: integer2("maxRetries", 3),
20210
+ retryBackoffMs: duration("retryBackoff", "30s", true),
20211
+ timeoutMs: duration("timeout", null)
20212
+ };
20213
+ }
19224
20214
  function safeStatus(value) {
19225
20215
  return value && TASK_STATUSES.has(value) ? value : "unknown";
19226
20216
  }
@@ -19286,19 +20276,19 @@ function esc(value) {
19286
20276
  }
19287
20277
  function readCurrentConfig() {
19288
20278
  const configPath = getConfigPath();
19289
- if (!existsSync4(configPath)) return {};
20279
+ if (!existsSync5(configPath)) return {};
19290
20280
  try {
19291
- return JSON.parse(readFileSync2(configPath, "utf-8"));
20281
+ return JSON.parse(readFileSync3(configPath, "utf-8"));
19292
20282
  } catch {
19293
20283
  return {};
19294
20284
  }
19295
20285
  }
19296
20286
  function writeConfig(cfg) {
19297
20287
  const configPath = getConfigPath();
19298
- const dir = dirname3(configPath);
19299
- if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
20288
+ const dir = dirname4(configPath);
20289
+ if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true });
19300
20290
  const tempPath = `${configPath}.${process.pid}.tmp`;
19301
- writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
20291
+ writeFileSync3(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
19302
20292
  renameSync2(tempPath, configPath);
19303
20293
  }
19304
20294
  function statCard(value, label, tone, cardIcon, delay = "") {
@@ -19336,24 +20326,52 @@ app.get("/", async (c) => {
19336
20326
  const page = parsePositiveInteger(c.req.query("page") || "1");
19337
20327
  if (page === null) return c.text("invalid page", 400);
19338
20328
  const statusFilter = c.req.query("status") || "";
20329
+ const requestedCwd = c.req.query("cwd") || "";
20330
+ const legacyProjectFilter = requestedCwd === LEGACY_PROJECT_FILTER;
20331
+ const cwdFilter = legacyProjectFilter ? "" : requestedCwd;
20332
+ const projectFilter = legacyProjectFilter ? LEGACY_PROJECT_FILTER : cwdFilter;
19339
20333
  const parsedStatus = statusFilter ? parseTaskStatus(statusFilter) : null;
19340
20334
  if (statusFilter && !parsedStatus) return c.text("invalid status", 400);
19341
20335
  const limit = 50;
19342
20336
  const offset = (page - 1) * limit;
19343
- const [tasks4, statsData] = await Promise.all([
19344
- TaskService.list({ limit, offset, ...parsedStatus ? { status: parsedStatus } : {} }),
19345
- TaskService.stats({})
20337
+ const [tasks4, statsData, globalStats, projects, globalRunning, legacyStats, legacyRunning] = await Promise.all([
20338
+ TaskService.list({
20339
+ limit,
20340
+ offset,
20341
+ ...parsedStatus === "running" ? { activeExecution: true } : parsedStatus ? { status: parsedStatus } : {},
20342
+ ...legacyProjectFilter ? { legacyCwd: true } : cwdFilter ? { cwd: cwdFilter } : {}
20343
+ }),
20344
+ TaskService.stats(legacyProjectFilter ? { legacyCwd: true } : cwdFilter ? { cwd: cwdFilter } : {}),
20345
+ TaskService.stats(),
20346
+ TaskService.projectSummaries(1e3),
20347
+ TaskService.countRunning(),
20348
+ TaskService.stats({ legacyCwd: true }),
20349
+ TaskService.countRunning({ legacyCwd: true })
19346
20350
  ]);
19347
20351
  const latestRuns = await TaskRunService.getLatestByTaskIds(tasks4.map((task) => task.id));
20352
+ const selectedRunning = legacyProjectFilter ? legacyRunning : cwdFilter ? projects.find((project) => project.cwd === cwdFilter)?.running ?? 0 : globalRunning;
19348
20353
  const counts = {
19349
20354
  pending: statsData.pending || 0,
19350
- running: statsData.running || 0,
20355
+ running: selectedRunning,
19351
20356
  done: statsData.done || 0,
19352
20357
  failed: (statsData.failed || 0) + (statsData.dead_letter || 0),
19353
20358
  total: statsData.total || 0
19354
20359
  };
19355
- const filteredTotal = parsedStatus ? Number(statsData[parsedStatus] ?? 0) : counts.total;
20360
+ const filteredTotal = parsedStatus === "running" ? selectedRunning : parsedStatus ? Number(statsData[parsedStatus] ?? 0) : counts.total;
19356
20361
  const totalPages = Math.max(1, Math.ceil(filteredTotal / limit));
20362
+ if (page > totalPages) {
20363
+ const params = new URLSearchParams({ page: String(totalPages) });
20364
+ if (statusFilter) params.set("status", statusFilter);
20365
+ if (projectFilter) params.set("cwd", projectFilter);
20366
+ return c.redirect(`/?${params.toString()}`);
20367
+ }
20368
+ const taskListUrl = (status, cwd) => {
20369
+ const params = new URLSearchParams();
20370
+ if (status) params.set("status", status);
20371
+ if (cwd) params.set("cwd", cwd);
20372
+ const query = params.toString();
20373
+ return query ? `/?${query}` : "/";
20374
+ };
19357
20375
  const filterItems = [
19358
20376
  { status: "", label: t(locale, "filter.all") },
19359
20377
  { status: "pending", label: statusText(locale, "pending") },
@@ -19364,22 +20382,27 @@ app.get("/", async (c) => {
19364
20382
  { status: "cancelled", label: statusText(locale, "cancelled") }
19365
20383
  ];
19366
20384
  const filters = filterItems.map(({ status, label }) => {
19367
- const href = status ? `/?status=${status}` : "/";
19368
- return `<a href="${href}" class="filter-chip ${statusFilter === status ? "active" : ""}">${label}</a>`;
20385
+ const href = taskListUrl(status, projectFilter);
20386
+ return `<a href="${esc(href)}" class="filter-chip ${statusFilter === status ? "active" : ""}">${label}</a>`;
19369
20387
  }).join("");
19370
20388
  const rows = tasks4.map((task) => {
19371
20389
  const status = safeStatus(task.status);
19372
- const executionActive = latestRuns.get(task.id)?.status === "running";
19373
- const searchable = esc(`${task.name} ${task.agent} ${task.prompt}`);
20390
+ const latestRun = latestRuns.get(task.id);
20391
+ const executionActive = latestRun?.status === "running";
20392
+ const batchId = task.batchId?.trim() || null;
20393
+ const searchable = esc(`${task.name} ${task.agent} ${task.prompt} ${task.cwd ?? ""} ${task.batchId ?? ""} ${task.category ?? ""}`);
19374
20394
  return `<tr data-task-row data-search="${searchable}">
19375
20395
  <td class="faint" data-label="${t(locale, "table.id")}">#${task.id}</td>
19376
- <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></td>
20396
+ <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>
20397
+ <div class="actions task-context"><span class="tag" title="${esc(task.cwd ?? "")}">${esc(task.cwd ? basename2(task.cwd) || task.cwd : t(locale, "projects.legacy"))}</span>${batchId ? `<span class="tag" title="${esc(t(locale, "template.batchId"))}">${esc(batchId)}</span>` : ""}</div></td>
19377
20398
  <td data-label="${t(locale, "table.agent")}"><span class="tag">${esc(task.agent)}</span></td>
19378
- <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${statusText(locale, status)}</span></td>
19379
- <td data-label="${t(locale, "table.duration")}" class="small ${task.status === "running" ? "" : "muted"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
20399
+ <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}" ${status === "dead_letter" ? `title="${esc(t(locale, "status.deadLetterHint"))}"` : ""}>${statusText(locale, status)}</span>${status === "dead_letter" ? `<div class="muted small" style="margin-top:5px">${t(locale, "status.deadLetterAction")}</div>` : ""}${executionActive && status !== "running" ? `<div class="muted small" style="margin-top:5px">${t(locale, "status.executionStillActive")}</div>` : ""}</td>
20400
+ <td data-label="${t(locale, "table.duration")}" class="small ${executionActive || task.status === "running" ? "" : "muted"}">${formatDuration(task.startedAt, task.finishedAt)}</td>
19380
20401
  <td data-label="${t(locale, "table.retries")}" class="muted small">${(task.retryCount ?? 0) > 0 ? task.retryCount : "\u2014"}</td>
19381
20402
  <td data-label="${t(locale, "table.actions")}"><div class="actions">
20403
+ ${task.cwd?.trim() && ["pending", "failed", "dead_letter"].includes(task.status ?? "") ? `<button type="button" class="btn" onclick="openTaskEditor(${task.id})">${t(locale, "action.edit")}</button>` : ""}
19382
20404
  <button type="button" class="btn" onclick="showDetail(${task.id})">${t(locale, "action.details")}</button>
20405
+ ${isValidSessionId(latestRun?.sessionId) ? `<button type="button" class="btn" onclick="copySessionCommand(${latestRun.id})">${icon("copy")}${t(locale, "action.continueSession")}</button>` : ""}
19383
20406
  ${task.status === "failed" || task.status === "dead_letter" ? `<button type="button" class="btn btn-warning" onclick="retryTask(${task.id})">${t(locale, "action.retry")}</button>` : ""}
19384
20407
  ${["pending", "running", "failed"].includes(task.status ?? "") ? `<button type="button" class="btn btn-warning" onclick="cancelTask(${task.id})">${t(locale, "action.cancel")}</button>` : ""}
19385
20408
  ${task.status === "running" || executionActive ? "" : `<button type="button" class="btn btn-danger" onclick="deleteTask(${task.id})">${t(locale, "action.delete")}</button>`}
@@ -19391,7 +20414,32 @@ app.get("/", async (c) => {
19391
20414
  <tbody>${rows}</tbody>
19392
20415
  </table></div>
19393
20416
  <div id="search-empty" hidden>${emptyState(t(locale, "filter.noResults"), "")}</div>`;
19394
- const suffix = statusFilter ? `&status=${statusFilter}` : "";
20417
+ const paginationParts = [];
20418
+ if (statusFilter) paginationParts.push(`status=${encodeURIComponent(statusFilter)}`);
20419
+ if (projectFilter) paginationParts.push(`cwd=${encodeURIComponent(projectFilter)}`);
20420
+ const suffix = paginationParts.length > 0 ? `&${paginationParts.join("&")}` : "";
20421
+ const projectCards = [
20422
+ `<a class="project-card ${projectFilter ? "" : "active"}" href="${esc(taskListUrl(statusFilter, ""))}">
20423
+ <div class="project-card-head"><strong>${t(locale, "projects.all")}</strong><span>${globalStats.total || 0}</span></div>
20424
+ <div class="project-counts">${t(locale, "projects.counts", { running: globalRunning, pending: globalStats.pending || 0, failed: (globalStats.failed || 0) + (globalStats.dead_letter || 0) })}</div>
20425
+ </a>`,
20426
+ ...legacyStats.total > 0 ? [`<a class="project-card ${legacyProjectFilter ? "active" : ""}" href="${esc(taskListUrl(statusFilter, LEGACY_PROJECT_FILTER))}">
20427
+ <div class="project-card-head"><strong>${t(locale, "projects.legacy")}</strong><span>${legacyStats.total}</span></div>
20428
+ <div class="project-path">${t(locale, "projects.legacyHint")}</div>
20429
+ <div class="project-counts">${t(locale, "projects.counts", { running: legacyRunning, pending: legacyStats.pending || 0, failed: (legacyStats.failed || 0) + (legacyStats.dead_letter || 0) })}</div>
20430
+ </a>`] : [],
20431
+ ...projects.map((project) => `<a class="project-card ${cwdFilter === project.cwd ? "active" : ""}" href="${esc(taskListUrl(statusFilter, project.cwd))}" title="${esc(project.cwd)}">
20432
+ <div class="project-card-head"><strong>${esc(basename2(project.cwd) || project.cwd)}</strong><span>${project.total}</span></div>
20433
+ <div class="project-path">${esc(project.cwd)}</div>
20434
+ <div class="project-counts">${t(locale, "projects.counts", { running: project.running, pending: project.pending, failed: project.failed })}</div>
20435
+ </a>`)
20436
+ ].join("");
20437
+ const projectData = JSON.stringify(Object.fromEntries(projects.map((project) => [project.cwd, {
20438
+ total: project.total,
20439
+ pending: project.pending,
20440
+ running: project.running,
20441
+ failed: project.failed
20442
+ }]))).replace(/</g, "\\u003c");
19395
20443
  const body = `
19396
20444
  <div class="stats-grid">
19397
20445
  ${statCard(counts.pending, t(locale, "stats.pending"), "tone-neutral", icon("clock"))}
@@ -19399,19 +20447,61 @@ app.get("/", async (c) => {
19399
20447
  ${statCard(counts.done, t(locale, "stats.done"), "tone-green", icon("check"), "reveal-delay-1")}
19400
20448
  ${statCard(counts.failed, t(locale, "stats.failedDead"), "tone-red", icon("alert"), "reveal-delay-2")}
19401
20449
  </div>
20450
+ <section class="panel project-panel reveal reveal-delay-1">
20451
+ <div class="panel-head"><div><h2>${t(locale, "projects.title")}</h2><p>${t(locale, "projects.description")}</p></div><button type="button" class="btn btn-primary" onclick="openTaskCreator()">${t(locale, "action.createTask")}</button></div>
20452
+ <div class="project-grid">${projectCards}</div>
20453
+ </section>
19402
20454
  <div class="toolbar reveal reveal-delay-1">
19403
20455
  <div class="filters">${filters}</div>
19404
20456
  <label class="search-box">${icon("search")}<input type="search" oninput="filterTasks(this.value)" placeholder="${t(locale, "filter.searchTasks")}" aria-label="${t(locale, "filter.searchTasks")}"></label>
19405
20457
  </div>
19406
20458
  <section class="panel reveal reveal-delay-2">${table}</section>
19407
- ${pagination(locale, "/", page, totalPages, filteredTotal, suffix)}`;
20459
+ ${pagination(locale, "/", page, totalPages, filteredTotal, suffix)}
20460
+ <script type="application/json" id="task-project-data">${projectData}</script>
20461
+ <dialog id="task-dialog" class="template-dialog">
20462
+ <form id="task-form" data-default-cwd="${esc(cwdFilter)}" onsubmit="saveTask(event)">
20463
+ <input id="task-id" type="hidden">
20464
+ <div class="dialog-head"><div><h2 id="task-dialog-title">${t(locale, "task.createTitle")}</h2><p>${t(locale, "task.formSubtitle")}</p></div><button type="button" class="icon-button" onclick="document.getElementById('task-dialog').close()" aria-label="${t(locale, "action.close")}">${icon("close")}</button></div>
20465
+ <div class="dialog-body">
20466
+ <div class="template-form-grid">
20467
+ <label class="form-field"><span>${t(locale, "template.name")}</span><input id="task-name" required maxlength="200" autocomplete="off"></label>
20468
+ <label class="form-field"><span>${t(locale, "template.cwd")}</span><input id="task-cwd" required autocomplete="off" list="task-cwd-options" placeholder="/path/to/project" oninput="updateTaskProjectStatus()"><small>${t(locale, "template.cwdHint")}</small></label>
20469
+ <datalist id="task-cwd-options">${projects.map((project) => `<option value="${esc(project.cwd)}"></option>`).join("")}</datalist>
20470
+ <label class="form-field"><span>${t(locale, "template.agent")}</span><input id="task-agent" required autocomplete="off" value="build" placeholder="build"></label>
20471
+ <label class="form-field"><span>${t(locale, "template.model")}</span><input id="task-model" required autocomplete="off" value="default" placeholder="default"></label>
20472
+ <label class="form-field form-field-wide"><span>${t(locale, "template.prompt")}</span><textarea id="task-prompt" rows="6" required></textarea></label>
20473
+ </div>
20474
+ <p id="task-project-status" class="form-note"></p>
20475
+ <details class="advanced-fields">
20476
+ <summary>${t(locale, "template.advanced")}</summary>
20477
+ <div class="template-form-grid">
20478
+ <label class="form-field"><span>${t(locale, "template.category")}</span><input id="task-category" autocomplete="off" value="general"></label>
20479
+ <label class="form-field"><span>${t(locale, "template.batchId")}</span><input id="task-batch" autocomplete="off"><small>${t(locale, "task.batchHint")}</small></label>
20480
+ <label class="form-field"><span>${t(locale, "template.importance")}</span><input id="task-importance" type="number" min="1" max="5" step="1" value="3" required></label>
20481
+ <label class="form-field"><span>${t(locale, "template.urgency")}</span><input id="task-urgency" type="number" min="1" max="5" step="1" value="3" required></label>
20482
+ <label class="form-field"><span>${t(locale, "template.maxRetries")}</span><input id="task-max-retries" type="number" min="0" max="1000" step="1" value="3" required></label>
20483
+ <label class="form-field"><span>${t(locale, "template.retryBackoff")}</span><input id="task-retry-backoff" autocomplete="off" value="30s"><small>${t(locale, "template.durationHint")}</small></label>
20484
+ <label class="form-field"><span>${t(locale, "template.timeout")}</span><input id="task-timeout" autocomplete="off" placeholder="30min"><small>${t(locale, "template.optional")}</small></label>
20485
+ </div>
20486
+ </details>
20487
+ </div>
20488
+ <div class="dialog-actions"><button type="button" class="btn" onclick="document.getElementById('task-dialog').close()">${t(locale, "action.cancel")}</button><button id="task-save" type="submit" class="btn btn-primary">${t(locale, "action.saveTask")}</button></div>
20489
+ </form>
20490
+ </dialog>`;
19408
20491
  return c.html(renderLayout({ locale, activeTab: "tasks", body }));
19409
20492
  });
19410
20493
  app.get("/templates", async (c) => {
19411
20494
  const locale = resolveLocale(c);
19412
- const templates = await TaskTemplateService.list(100);
19413
- const enabled = templates.filter((template) => template.enabled).length;
19414
- const disabled = templates.length - enabled;
20495
+ const page = parsePositiveInteger(c.req.query("page") || "1");
20496
+ if (page === null) return c.text("invalid page", 400);
20497
+ const limit = 50;
20498
+ const offset = (page - 1) * limit;
20499
+ const [templates, templateStats] = await Promise.all([
20500
+ TaskTemplateService.list(limit, offset),
20501
+ TaskTemplateService.stats()
20502
+ ]);
20503
+ const totalPages = Math.max(1, Math.ceil(templateStats.total / limit));
20504
+ if (page > totalPages) return c.redirect(`/templates?page=${totalPages}`);
19415
20505
  const rows = templates.map((template) => {
19416
20506
  const scheduleType = ["cron", "recurring", "delayed"].includes(template.scheduleType) ? template.scheduleType : "unknown";
19417
20507
  const typeLabel = scheduleType === "cron" ? t(locale, "schedule.cron") : scheduleType === "recurring" ? t(locale, "schedule.recurring") : scheduleType === "delayed" ? t(locale, "schedule.delayed") : t(locale, "schedule.unknown");
@@ -19429,21 +20519,56 @@ app.get("/templates", async (c) => {
19429
20519
  <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>
19430
20520
  <td data-label="${t(locale, "table.lastRun")}" class="small muted">${formatRelative(template.lastRunAt, locale)}</td>
19431
20521
  <td data-label="${t(locale, "table.nextRun")}" class="small">${formatFuture(template.nextRunAt, locale)}</td>
19432
- <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="showTemplateDetail(${template.id})">${t(locale, "action.details")}</button>
20522
+ <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="openTemplateEditor(${template.id})">${t(locale, "action.edit")}</button><button type="button" class="btn" onclick="showTemplateDetail(${template.id})">${t(locale, "action.details")}</button>
19433
20523
  <button type="button" class="btn btn-primary" onclick="triggerTmpl(${template.id})">${t(locale, "action.trigger")}</button>${toggle}
19434
20524
  <button type="button" class="btn btn-danger" onclick="deleteTmpl(${template.id})">${t(locale, "action.delete")}</button></div></td>
19435
20525
  </tr>`;
19436
20526
  }).join("");
19437
20527
  const body = `
19438
20528
  <div class="stats-grid three">
19439
- ${statCard(templates.length, t(locale, "stats.templates"), "tone-purple", icon("templates"))}
19440
- ${statCard(enabled, t(locale, "stats.enabled"), "tone-green", icon("check"), "reveal-delay-1")}
19441
- ${statCard(disabled, t(locale, "stats.disabled"), "tone-neutral", icon("clock"), "reveal-delay-2")}
20529
+ ${statCard(templateStats.total, t(locale, "stats.templates"), "tone-purple", icon("templates"))}
20530
+ ${statCard(templateStats.enabled, t(locale, "stats.enabled"), "tone-green", icon("check"), "reveal-delay-1")}
20531
+ ${statCard(templateStats.disabled, t(locale, "stats.disabled"), "tone-neutral", icon("clock"), "reveal-delay-2")}
19442
20532
  </div>
19443
20533
  <section class="panel reveal reveal-delay-2">
19444
- <div class="panel-head"><h2>${t(locale, "page.templates.title")}</h2></div>
19445
- ${templates.length === 0 ? emptyState(t(locale, "empty.templates"), t(locale, "empty.templatesHint"), "supertask template add") : `<div class="table-wrap"><table class="responsive-table"><thead><tr><th>ID</th><th>${t(locale, "table.name")}</th><th>${t(locale, "table.type")}</th><th>${t(locale, "table.rule")}</th><th>${t(locale, "table.status")}</th><th>${t(locale, "table.lastRun")}</th><th>${t(locale, "table.nextRun")}</th><th>${t(locale, "table.actions")}</th></tr></thead><tbody>${rows}</tbody></table></div>`}
19446
- </section>`;
20534
+ <div class="panel-head"><h2>${t(locale, "page.templates.title")}</h2><button type="button" class="btn btn-primary" onclick="openTemplateCreator()">${t(locale, "action.createTemplate")}</button></div>
20535
+ ${templates.length === 0 ? emptyState(t(locale, "empty.templates"), t(locale, "empty.templatesHint")) : `<div class="table-wrap"><table class="responsive-table"><thead><tr><th>ID</th><th>${t(locale, "table.name")}</th><th>${t(locale, "table.type")}</th><th>${t(locale, "table.rule")}</th><th>${t(locale, "table.status")}</th><th>${t(locale, "table.lastRun")}</th><th>${t(locale, "table.nextRun")}</th><th>${t(locale, "table.actions")}</th></tr></thead><tbody>${rows}</tbody></table></div>`}
20536
+ </section>
20537
+ <dialog id="template-dialog" class="template-dialog">
20538
+ <form id="template-form" onsubmit="saveTemplate(event)">
20539
+ <input id="template-id" type="hidden">
20540
+ <div class="dialog-head"><div><h2 id="template-dialog-title">${t(locale, "template.createTitle")}</h2><p>${t(locale, "template.formSubtitle")}</p></div><button type="button" class="icon-button" onclick="document.getElementById('template-dialog').close()" aria-label="${t(locale, "action.close")}">${icon("close")}</button></div>
20541
+ <div class="dialog-body">
20542
+ <div class="template-form-grid">
20543
+ <label class="form-field"><span>${t(locale, "template.name")}</span><input id="template-name" required maxlength="200" autocomplete="off"></label>
20544
+ <label class="form-field"><span>${t(locale, "template.cwd")}</span><input id="template-cwd" required autocomplete="off" placeholder="/path/to/project"><small>${t(locale, "template.cwdHint")}</small></label>
20545
+ <label class="form-field"><span>${t(locale, "template.agent")}</span><input id="template-agent" required autocomplete="off" placeholder="build"></label>
20546
+ <label class="form-field"><span>${t(locale, "template.model")}</span><input id="template-model" required autocomplete="off" value="default" placeholder="default"></label>
20547
+ <label class="form-field form-field-wide"><span>${t(locale, "template.prompt")}</span><textarea id="template-prompt" rows="6" required></textarea></label>
20548
+ <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>
20549
+ <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>
20550
+ <label id="template-interval-field" class="form-field"><span>${t(locale, "template.interval")}</span><input id="template-interval" autocomplete="off" value="1h" placeholder="30s / 5min / 1h"><small>${t(locale, "template.durationHint")}</small></label>
20551
+ <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>
20552
+ </div>
20553
+ <details class="advanced-fields">
20554
+ <summary>${t(locale, "template.advanced")}</summary>
20555
+ <div class="template-form-grid">
20556
+ <label class="form-field"><span>${t(locale, "template.category")}</span><input id="template-category" autocomplete="off" value="general"></label>
20557
+ <label class="form-field"><span>${t(locale, "template.batchId")}</span><input id="template-batch" autocomplete="off"><small>${t(locale, "template.optional")}</small></label>
20558
+ <label class="form-field"><span>${t(locale, "template.importance")}</span><input id="template-importance" type="number" min="1" max="5" step="1" value="3" required></label>
20559
+ <label class="form-field"><span>${t(locale, "template.urgency")}</span><input id="template-urgency" type="number" min="1" max="5" step="1" value="3" required></label>
20560
+ <label class="form-field"><span>${t(locale, "template.maxInstances")}</span><input id="template-max-instances" type="number" min="1" max="1000" step="1" value="1" required><small>${t(locale, "template.maxInstancesHint")}</small></label>
20561
+ <label class="form-field"><span>${t(locale, "template.maxRetries")}</span><input id="template-max-retries" type="number" min="0" max="1000" step="1" value="3" required></label>
20562
+ <label class="form-field"><span>${t(locale, "template.retryBackoff")}</span><input id="template-retry-backoff" autocomplete="off" value="30s"><small>${t(locale, "template.durationHint")}</small></label>
20563
+ <label class="form-field"><span>${t(locale, "template.timeout")}</span><input id="template-timeout" autocomplete="off" placeholder="30min"><small>${t(locale, "template.optional")}</small></label>
20564
+ </div>
20565
+ </details>
20566
+ <p class="form-note">${t(locale, "template.futureOnly")}</p>
20567
+ </div>
20568
+ <div class="dialog-actions"><button type="button" class="btn" onclick="document.getElementById('template-dialog').close()">${t(locale, "action.cancel")}</button><button id="template-save" type="submit" class="btn btn-primary">${t(locale, "action.saveTemplate")}</button></div>
20569
+ </form>
20570
+ </dialog>
20571
+ ${pagination(locale, "/templates", page, totalPages, templateStats.total)}`;
19447
20572
  return c.html(renderLayout({ locale, activeTab: "templates", body }));
19448
20573
  });
19449
20574
  app.get("/runs", async (c) => {
@@ -19471,9 +20596,11 @@ app.get("/runs", async (c) => {
19471
20596
  const totalResult = await db.select({ count: sql`count(*)` }).from(taskRuns4);
19472
20597
  const total = Number(totalResult[0]?.count ?? 0);
19473
20598
  const totalPages = Math.max(1, Math.ceil(total / limit));
20599
+ if (page > totalPages) return c.redirect(`/runs?page=${totalPages}`);
19474
20600
  const logs = [];
19475
20601
  const rows = runs.map((run) => {
19476
20602
  const status = safeStatus(run.status);
20603
+ const resumable = isValidSessionId(run.sessionId);
19477
20604
  if (run.log) {
19478
20605
  logs.push(`<section id="log-${run.id}" class="panel log-panel" hidden><div class="panel-head"><h3>Run #${run.id} \xB7 ${esc(run.taskName)}</h3></div><div class="log-box">${esc(run.log)}</div></section>`);
19479
20606
  }
@@ -19481,10 +20608,12 @@ app.get("/runs", async (c) => {
19481
20608
  <td class="faint" data-label="${t(locale, "table.run")}">#${run.id}</td>
19482
20609
  <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>
19483
20610
  <td data-label="${t(locale, "table.agent")}"><span class="tag">${esc(run.taskAgent)}</span></td>
19484
- <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${statusText(locale, status)}</span></td>
20611
+ <td data-label="${t(locale, "table.session")}" class="m small">${esc(maskSessionId(run.sessionId))}</td>
20612
+ <td data-label="${t(locale, "table.status")}"><span class="badge b-${status}">${runStatusText(locale, run.status ?? "unknown")}</span></td>
19485
20613
  <td data-label="${t(locale, "table.duration")}" class="small">${formatDuration(run.startedAt, run.finishedAt)}</td>
19486
20614
  <td data-label="${t(locale, "table.heartbeat")}" class="small muted">${formatRelative(run.heartbeatAt, locale)}</td>
19487
20615
  <td data-label="${t(locale, "table.actions")}"><div class="actions"><button type="button" class="btn" onclick="showRunDetail(${run.id})">${t(locale, "action.details")}</button>
20616
+ ${resumable ? `<button type="button" class="btn" onclick="copySessionCommand(${run.id})">${icon("copy")}${t(locale, "action.continueSession")}</button>` : ""}
19488
20617
  ${run.log ? `<button type="button" class="btn" aria-expanded="false" onclick="toggleLog(${run.id},this)">${t(locale, "action.logs")}</button>` : ""}</div></td>
19489
20618
  </tr>`;
19490
20619
  }).join("");
@@ -19495,22 +20624,37 @@ app.get("/runs", async (c) => {
19495
20624
  ${statCard(runs.filter((run) => run.status === "failed").length, t(locale, "stats.pageFailed"), "tone-red", icon("alert"), "reveal-delay-1")}
19496
20625
  ${statCard(runs.filter((run) => run.status === "running").length, t(locale, "stats.pageRunning"), "tone-blue", icon("activity"), "reveal-delay-2")}
19497
20626
  </div>
19498
- <section class="panel reveal reveal-delay-2">${runs.length === 0 ? emptyState(t(locale, "empty.runs"), "") : `<div class="table-wrap"><table class="responsive-table"><thead><tr><th>${t(locale, "table.run")}</th><th>${t(locale, "table.task")}</th><th>${t(locale, "table.agent")}</th><th>${t(locale, "table.status")}</th><th>${t(locale, "table.duration")}</th><th>${t(locale, "table.heartbeat")}</th><th>${t(locale, "table.actions")}</th></tr></thead><tbody>${rows}</tbody></table></div>`}</section>
20627
+ <section class="panel reveal reveal-delay-2">${runs.length === 0 ? emptyState(t(locale, "empty.runs"), "") : `<div class="table-wrap"><table class="responsive-table"><thead><tr><th>${t(locale, "table.run")}</th><th>${t(locale, "table.task")}</th><th>${t(locale, "table.agent")}</th><th>${t(locale, "table.session")}</th><th>${t(locale, "table.status")}</th><th>${t(locale, "table.duration")}</th><th>${t(locale, "table.heartbeat")}</th><th>${t(locale, "table.actions")}</th></tr></thead><tbody>${rows}</tbody></table></div>`}</section>
19499
20628
  ${logs.join("")}${pagination(locale, "/runs", page, totalPages, total)}`;
19500
20629
  return c.html(renderLayout({ locale, activeTab: "runs", body }));
19501
20630
  });
19502
20631
  app.get("/system", async (c) => {
19503
20632
  const locale = resolveLocale(c);
19504
20633
  const config = loadConfig();
20634
+ const activeConfig = runtimeConfig ?? config;
20635
+ const restartRequired = runtimeConfig !== null && !configsEqual(config, runtimeConfig);
20636
+ const managedRestart = canRestartCurrentGateway();
20637
+ const configState = resolveDashboardConfigState(
20638
+ runtimeConfig !== null,
20639
+ restartRequired,
20640
+ managedRestart
20641
+ );
20642
+ const configStateKey = {
20643
+ foreground: "system.configForeground",
20644
+ applied: "system.configApplied",
20645
+ pending: "system.configPending",
20646
+ manual: "system.configRestartManually"
20647
+ }[configState];
20648
+ const configStateText = t(locale, configStateKey);
19505
20649
  const configPath = getConfigPath();
19506
- const [stats, runningRuns, templates] = await Promise.all([
20650
+ const [stats, runningRuns, templateStats] = await Promise.all([
19507
20651
  TaskService.stats({}),
19508
20652
  TaskRunService.getAllRunningRuns(),
19509
- TaskTemplateService.list(100)
20653
+ TaskTemplateService.stats()
19510
20654
  ]);
19511
- const configExists = existsSync4(configPath);
20655
+ const configExists = existsSync5(configPath);
19512
20656
  const runRows = runningRuns.map((run) => {
19513
- const session = run.sessionId ? `${run.sessionId.slice(4, 7)}***${run.sessionId.slice(-3)}` : "\u2014";
20657
+ const session = maskSessionId(run.sessionId);
19514
20658
  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>
19515
20659
  <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>
19516
20660
  <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>
@@ -19526,10 +20670,10 @@ app.get("/system", async (c) => {
19526
20670
  <div class="field"><label for="hi">${t(locale, "system.heartbeatInterval")}</label>${unitInput("hi", config.worker.heartbeatIntervalMs / 1e3, 5, t(locale, "system.seconds"))}</div>
19527
20671
  <div class="field"><label for="to">${t(locale, "system.taskTimeout")}</label>${unitInput("to", config.worker.taskTimeoutMs / 6e4, 1, t(locale, "system.minutes"))}</div>
19528
20672
  </section>
19529
- <section class="card settings-card reveal-delay-1"><h2 class="settings-title"><span>${icon("templates")}${t(locale, "system.scheduler")}</span><span class="badge ${config.scheduler.enabled ? "b-done" : "b-cancelled"}">${t(locale, config.scheduler.enabled ? "schedule.enabled" : "schedule.disabled")}</span></h2>
20673
+ <section class="card settings-card reveal-delay-1"><h2 class="settings-title"><span>${icon("templates")}${t(locale, "system.scheduler")}</span><span class="badge ${activeConfig.scheduler.enabled ? "b-done" : "b-cancelled"}">${t(locale, activeConfig.scheduler.enabled ? "schedule.enabled" : "schedule.disabled")}</span></h2>
19530
20674
  <div class="switch-field"><label for="se">${t(locale, "system.schedulerEnabled")}</label><label class="switch"><input id="se" type="checkbox" name="se" ${config.scheduler.enabled ? "checked" : ""}><span></span></label></div>
19531
20675
  <div class="field"><label for="si">${t(locale, "system.checkInterval")}</label>${unitInput("si", config.scheduler.checkIntervalMs, 100, t(locale, "system.milliseconds"))}</div>
19532
- <div class="info-row"><span class="info-key">${t(locale, "system.activeTemplates")}</span><span class="info-value">${templates.filter((template) => template.enabled).length} / ${templates.length}</span></div>
20676
+ <div class="info-row"><span class="info-key">${t(locale, "system.activeTemplates")}</span><span class="info-value">${templateStats.enabled} / ${templateStats.total}</span></div>
19533
20677
  </section>
19534
20678
  <section class="card settings-card reveal-delay-2"><h2 class="settings-title"><span>${icon("system")}${t(locale, "system.watchdog")}</span></h2>
19535
20679
  <div class="field"><label for="wt">${t(locale, "system.heartbeatTimeout")}</label>${unitInput("wt", config.watchdog.heartbeatTimeoutMs / 1e3, 10, t(locale, "system.seconds"))}</div>
@@ -19538,10 +20682,10 @@ app.get("/system", async (c) => {
19538
20682
  <div class="field"><label for="rd">${t(locale, "system.retentionDays")}</label>${unitInput("rd", config.watchdog.retentionDays, 1, t(locale, "system.days"))}</div>
19539
20683
  </section>
19540
20684
  </div>
19541
- <div class="save-row"><span class="muted small">${t(locale, "system.saveHint")}</span><button type="submit" class="btn btn-primary">${t(locale, "action.save")}</button></div>
20685
+ <div class="save-row"><span class="muted small">${configStateText}</span><div class="actions"><button type="submit" class="btn">${t(locale, "action.save")}</button>${managedRestart ? `<button type="button" class="btn btn-primary" onclick="saveConfig(true,${runningRuns.length})">${t(locale, "action.saveAndRestart")}</button>` : ""}</div></div>
19542
20686
  </form>
19543
20687
  <section class="panel reveal">
19544
- <div class="panel-head"><h2>${t(locale, "system.runningTasks", { running: runningRuns.length, limit: config.worker.maxConcurrency })}</h2></div>
20688
+ <div class="panel-head"><h2>${t(locale, "system.runningTasks", { running: runningRuns.length, limit: activeConfig.worker.maxConcurrency })}</h2></div>
19545
20689
  ${runningRuns.length === 0 ? emptyState(t(locale, "empty.running"), "") : `<div class="table-wrap"><table class="responsive-table"><thead><tr><th>${t(locale, "table.run")}</th><th>${t(locale, "table.task")}</th><th>${t(locale, "table.session")}</th><th>${t(locale, "table.model")}</th><th>${t(locale, "table.startedAt")}</th><th>${t(locale, "table.heartbeat")}</th><th>${t(locale, "table.pid")}</th><th>${t(locale, "table.duration")}</th></tr></thead><tbody>${runRows}</tbody></table></div>`}
19546
20690
  </section>
19547
20691
  <section class="panel reveal reveal-delay-1"><div class="panel-head"><h2>${t(locale, "system.taskStats")}</h2></div><div class="overview-grid">
@@ -19558,6 +20702,30 @@ app.get("/system", async (c) => {
19558
20702
  <button type="button" class="btn btn-danger" onclick="clearDatabase()">${icon("database")}${t(locale, "action.clearDatabase")}</button></section>`;
19559
20703
  return c.html(renderLayout({ locale, activeTab: "system", body }));
19560
20704
  });
20705
+ app.post("/api/tasks", async (c) => {
20706
+ try {
20707
+ const task = await TaskService.add(parseTaskPayload(await c.req.json()));
20708
+ return c.json({ success: true, task }, 201);
20709
+ } catch (error) {
20710
+ return c.json({
20711
+ error: error instanceof Error ? error.message : String(error)
20712
+ }, 400);
20713
+ }
20714
+ });
20715
+ app.put("/api/tasks/:id", async (c) => {
20716
+ const id = parsePositiveInteger(c.req.param("id"));
20717
+ if (id === null) return c.json({ error: "invalid id" }, 400);
20718
+ try {
20719
+ const input = parseTaskPayload(await c.req.json());
20720
+ const task = await TaskService.update(id, editableTaskPayload(input));
20721
+ if (task) return c.json({ success: true, task });
20722
+ return await TaskService.getById(id) ? c.json({ error: "task status does not allow editing" }, 409) : c.json({ error: "not found" }, 404);
20723
+ } catch (error) {
20724
+ return c.json({
20725
+ error: error instanceof Error ? error.message : String(error)
20726
+ }, 400);
20727
+ }
20728
+ });
19561
20729
  app.get("/api/tasks/:id", async (c) => {
19562
20730
  const id = parsePositiveInteger(c.req.param("id"));
19563
20731
  if (id === null) return c.json({ error: "invalid id" }, 400);
@@ -19573,6 +20741,24 @@ app.get("/api/runs/:id", async (c) => {
19573
20741
  if (!run) return c.json({ error: "not found" }, 404);
19574
20742
  return c.json(run);
19575
20743
  });
20744
+ app.get("/api/runs/:id/session-command", async (c) => {
20745
+ const id = parsePositiveInteger(c.req.param("id"));
20746
+ if (id === null) return c.json({ error: "invalid id" }, 400);
20747
+ const run = await TaskRunService.getById(id);
20748
+ if (!run) return c.json({ error: "not found" }, 404);
20749
+ if (!isValidSessionId(run.sessionId)) return c.json({ error: "session unavailable" }, 409);
20750
+ return c.json({ command: sessionCommand(run.sessionId) });
20751
+ });
20752
+ app.get("/api/gateway/status", (c) => {
20753
+ const savedConfig = loadConfig();
20754
+ const managed = canRestartCurrentGateway();
20755
+ return c.json({
20756
+ pid: process.pid,
20757
+ managed,
20758
+ ready: managed && getGatewayHealth().status === "ok",
20759
+ restartRequired: runtimeConfig === null || !configsEqual(savedConfig, runtimeConfig)
20760
+ });
20761
+ });
19576
20762
  app.get("/api/templates/:id", async (c) => {
19577
20763
  const id = parsePositiveInteger(c.req.param("id"));
19578
20764
  if (id === null) return c.json({ error: "invalid id" }, 400);
@@ -19580,6 +20766,30 @@ app.get("/api/templates/:id", async (c) => {
19580
20766
  if (!template) return c.json({ error: "not found" }, 404);
19581
20767
  return c.json(template);
19582
20768
  });
20769
+ app.post("/api/templates", async (c) => {
20770
+ try {
20771
+ const input = parseTemplatePayload(await c.req.json());
20772
+ const template = await TaskTemplateService.create(input);
20773
+ return c.json({ success: true, template }, 201);
20774
+ } catch (error) {
20775
+ return c.json({
20776
+ error: error instanceof Error ? error.message : String(error)
20777
+ }, 400);
20778
+ }
20779
+ });
20780
+ app.put("/api/templates/:id", async (c) => {
20781
+ const id = parsePositiveInteger(c.req.param("id"));
20782
+ if (id === null) return c.json({ error: "invalid id" }, 400);
20783
+ try {
20784
+ const input = parseTemplatePayload(await c.req.json());
20785
+ const template = await TaskTemplateService.update(id, input);
20786
+ return template ? c.json({ success: true, template }) : c.json({ error: "not found" }, 404);
20787
+ } catch (error) {
20788
+ return c.json({
20789
+ error: error instanceof Error ? error.message : String(error)
20790
+ }, 400);
20791
+ }
20792
+ });
19583
20793
  app.post("/api/tasks/:id/retry", async (c) => {
19584
20794
  const id = parsePositiveInteger(c.req.param("id"));
19585
20795
  if (id === null) return c.json({ error: "invalid id" }, 400);
@@ -19628,22 +20838,31 @@ app.delete("/api/templates/:id", async (c) => {
19628
20838
  app.post("/api/templates/:id/trigger", async (c) => {
19629
20839
  const id = parsePositiveInteger(c.req.param("id"));
19630
20840
  if (id === null) return c.json({ error: "invalid id" }, 400);
19631
- const template = await TaskTemplateService.getById(id);
19632
- if (!template) return c.json({ error: "not found" }, 404);
19633
20841
  const task = await triggerTaskFromTemplate(id);
19634
- if (!task) return c.json({ error: "maxInstances reached" }, 409);
19635
- return c.json({ success: true, taskId: task.id });
20842
+ return task ? c.json({ success: true, taskId: task.id }) : c.json({ error: "not found" }, 404);
19636
20843
  });
19637
20844
  app.put("/api/config", async (c) => {
19638
20845
  try {
19639
- const body = await c.req.json();
20846
+ const rawBody = await c.req.json();
20847
+ if (!rawBody || typeof rawBody !== "object" || Array.isArray(rawBody)) {
20848
+ throw new Error("\u8BF7\u6C42\u5185\u5BB9\u5FC5\u987B\u662F\u5BF9\u8C61");
20849
+ }
20850
+ const body = rawBody;
20851
+ const section = (name) => {
20852
+ const value = body[name];
20853
+ if (value === void 0) return {};
20854
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
20855
+ throw new Error(`${name} \u5FC5\u987B\u662F\u5BF9\u8C61`);
20856
+ }
20857
+ return value;
20858
+ };
19640
20859
  const current = readCurrentConfig();
19641
20860
  const currentWorker = current.worker ?? {};
19642
20861
  const currentScheduler = current.scheduler ?? {};
19643
20862
  const currentWatchdog = current.watchdog ?? {};
19644
- const bodyWorker = body.worker ?? {};
19645
- const bodyScheduler = body.scheduler ?? {};
19646
- const bodyWatchdog = body.watchdog ?? {};
20863
+ const bodyWorker = section("worker");
20864
+ const bodyScheduler = section("scheduler");
20865
+ const bodyWatchdog = section("watchdog");
19647
20866
  const merged = {
19648
20867
  ...current,
19649
20868
  ...body,
@@ -19652,8 +20871,13 @@ app.put("/api/config", async (c) => {
19652
20871
  scheduler: { ...currentScheduler, ...bodyScheduler },
19653
20872
  watchdog: { ...currentWatchdog, ...bodyWatchdog }
19654
20873
  };
19655
- writeConfig(validateConfig(merged));
19656
- return c.json({ success: true });
20874
+ const savedConfig = validateConfig(merged);
20875
+ writeConfig(savedConfig);
20876
+ return c.json({
20877
+ success: true,
20878
+ restartRequired: runtimeConfig === null || !configsEqual(savedConfig, runtimeConfig),
20879
+ managed: canRestartCurrentGateway()
20880
+ });
19657
20881
  } catch (error) {
19658
20882
  return c.json({
19659
20883
  success: false,
@@ -19661,9 +20885,27 @@ app.put("/api/config", async (c) => {
19661
20885
  }, 400);
19662
20886
  }
19663
20887
  });
20888
+ app.post("/api/gateway/restart", async (c) => {
20889
+ const rawBody = await c.req.json().catch(() => null);
20890
+ const confirmation = rawBody && typeof rawBody === "object" && !Array.isArray(rawBody) ? rawBody.confirmation : void 0;
20891
+ if (confirmation !== "RESTART") {
20892
+ return c.json({ error: "confirmation must be RESTART" }, 400);
20893
+ }
20894
+ if (restartScheduled) return c.json({ error: "restart already scheduled" }, 409);
20895
+ if (!canRestartCurrentGateway()) {
20896
+ 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);
20897
+ }
20898
+ restartScheduled = true;
20899
+ const previousPid = process.pid;
20900
+ setTimeout(() => {
20901
+ process.kill(previousPid, "SIGTERM");
20902
+ }, 500);
20903
+ return c.json({ success: true, previousPid }, 202);
20904
+ });
19664
20905
  app.post("/api/database/clear", async (c) => {
19665
- const body = await c.req.json().catch(() => ({}));
19666
- if (body.confirmation !== "CLEAR") {
20906
+ const rawBody = await c.req.json().catch(() => null);
20907
+ const confirmation = rawBody && typeof rawBody === "object" && !Array.isArray(rawBody) ? rawBody.confirmation : void 0;
20908
+ if (confirmation !== "CLEAR") {
19667
20909
  return c.json({ success: false, error: "confirmation must be CLEAR" }, 400);
19668
20910
  }
19669
20911
  try {
@@ -19685,6 +20927,9 @@ var web_default = {
19685
20927
  };
19686
20928
  export {
19687
20929
  dashboardApp,
19688
- web_default as default
20930
+ web_default as default,
20931
+ isSafeDashboardRestartTarget,
20932
+ resolveDashboardConfigState,
20933
+ setDashboardRuntimeConfig
19689
20934
  };
19690
20935
  //# sourceMappingURL=index.js.map