opencode-supertask 0.1.34 → 0.1.35

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
@@ -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 join4(chunks, separator) {
12227
+ function join5(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 = join4;
12237
+ sql2.join = join5;
12238
12238
  function identifier(value) {
12239
12239
  return new Name(value);
12240
12240
  }
@@ -13083,8 +13083,17 @@ function mapRelationalRow(tablesConfig, tableConfig, row, buildQueryResultSelect
13083
13083
  }
13084
13084
 
13085
13085
  // src/web/index.tsx
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";
13086
+ import {
13087
+ existsSync as existsSync5,
13088
+ mkdirSync as mkdirSync4,
13089
+ readFileSync as readFileSync3,
13090
+ readdirSync,
13091
+ renameSync as renameSync2,
13092
+ statSync as statSync4,
13093
+ writeFileSync as writeFileSync3
13094
+ } from "fs";
13095
+ import { homedir as homedir4 } from "os";
13096
+ import { basename as basename2, dirname as dirname4, join as join4 } from "path";
13088
13097
 
13089
13098
  // src/core/db/index.ts
13090
13099
  import { Database as Database2 } from "bun:sqlite";
@@ -14571,7 +14580,7 @@ var SQLiteSelectQueryBuilderBase = class extends TypedQueryBuilder {
14571
14580
  return (table, on) => {
14572
14581
  const baseTableName = this.tableName;
14573
14582
  const tableName = getTableLikeName(table);
14574
- if (typeof tableName === "string" && this.config.joins?.some((join4) => join4.alias === tableName)) {
14583
+ if (typeof tableName === "string" && this.config.joins?.some((join5) => join5.alias === tableName)) {
14575
14584
  throw new Error(`Alias "${tableName}" is already used in this query`);
14576
14585
  }
14577
14586
  if (!this.isPartialSelect) {
@@ -15384,7 +15393,7 @@ var SQLiteUpdateBase = class extends QueryPromise {
15384
15393
  createJoin(joinType) {
15385
15394
  return (table, on) => {
15386
15395
  const tableName = getTableLikeName(table);
15387
- if (typeof tableName === "string" && this.config.joins.some((join4) => join4.alias === tableName)) {
15396
+ if (typeof tableName === "string" && this.config.joins.some((join5) => join5.alias === tableName)) {
15388
15397
  throw new Error(`Alias "${tableName}" is already used in this query`);
15389
15398
  }
15390
15399
  if (typeof on === "function") {
@@ -16611,6 +16620,140 @@ function parseDuration(input) {
16611
16620
  return null;
16612
16621
  }
16613
16622
 
16623
+ // src/core/opencode-catalog.ts
16624
+ import { spawn } from "child_process";
16625
+
16626
+ // src/core/task-working-directory.ts
16627
+ import { statSync } from "fs";
16628
+ import { isAbsolute } from "path";
16629
+ var InvalidTaskWorkingDirectoryError = class extends Error {
16630
+ constructor(message) {
16631
+ super(message);
16632
+ this.name = "InvalidTaskWorkingDirectoryError";
16633
+ }
16634
+ };
16635
+ function validateTaskWorkingDirectory(cwd) {
16636
+ if (cwd == null) return;
16637
+ if (!cwd.trim()) {
16638
+ throw new InvalidTaskWorkingDirectoryError("\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u80FD\u4E3A\u7A7A");
16639
+ }
16640
+ if (!isAbsolute(cwd)) {
16641
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84\uFF1A${cwd}`);
16642
+ }
16643
+ let stat;
16644
+ try {
16645
+ stat = statSync(cwd);
16646
+ } catch (error) {
16647
+ const detail = error instanceof Error ? error.message : String(error);
16648
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BBF\u95EE\uFF1A${cwd}\uFF08${detail}\uFF09`);
16649
+ }
16650
+ if (!stat.isDirectory()) {
16651
+ throw new InvalidTaskWorkingDirectoryError(`\u4EFB\u52A1\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u76EE\u5F55\uFF1A${cwd}`);
16652
+ }
16653
+ }
16654
+
16655
+ // src/core/opencode-catalog.ts
16656
+ var CATALOG_CACHE_MS = 3e4;
16657
+ var COMMAND_TIMEOUT_MS = 2e4;
16658
+ var MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
16659
+ var ANSI_PATTERN = /\x1B(?:[@-_][0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
16660
+ var catalogCache = /* @__PURE__ */ new Map();
16661
+ function cleanOutput(value) {
16662
+ return value.replace(ANSI_PATTERN, "").replace(/\r/g, "");
16663
+ }
16664
+ function runOpenCode(executable, args, cwd, timeoutMs) {
16665
+ return new Promise((resolve3, reject) => {
16666
+ const child = spawn(executable, args, {
16667
+ cwd,
16668
+ env: process.env,
16669
+ stdio: ["ignore", "pipe", "pipe"]
16670
+ });
16671
+ let stdout = "";
16672
+ let stderr = "";
16673
+ let failure = null;
16674
+ let finished = false;
16675
+ const append = (current, chunk) => {
16676
+ const next = current + chunk.toString();
16677
+ if (Buffer.byteLength(next) > MAX_OUTPUT_BYTES && failure === null) {
16678
+ failure = new Error(`OpenCode \u8F93\u51FA\u8D85\u8FC7 ${MAX_OUTPUT_BYTES} bytes`);
16679
+ child.kill("SIGTERM");
16680
+ }
16681
+ return next.slice(-MAX_OUTPUT_BYTES);
16682
+ };
16683
+ child.stdout?.on("data", (chunk) => {
16684
+ stdout = append(stdout, chunk);
16685
+ });
16686
+ child.stderr?.on("data", (chunk) => {
16687
+ stderr = append(stderr, chunk);
16688
+ });
16689
+ child.once("error", (error) => {
16690
+ failure ??= error;
16691
+ });
16692
+ const timer = setTimeout(() => {
16693
+ failure ??= new Error(`OpenCode \u547D\u4EE4\u8D85\u8FC7 ${timeoutMs}ms \u672A\u5B8C\u6210`);
16694
+ child.kill("SIGTERM");
16695
+ }, timeoutMs);
16696
+ child.once("close", (code) => {
16697
+ if (finished) return;
16698
+ finished = true;
16699
+ clearTimeout(timer);
16700
+ if (failure) {
16701
+ reject(failure);
16702
+ return;
16703
+ }
16704
+ if (code !== 0) {
16705
+ const detail = cleanOutput(stderr).trim() || `\u9000\u51FA\u7801 ${code ?? "null"}`;
16706
+ reject(new Error(`OpenCode ${args.join(" ")} \u5931\u8D25\uFF1A${detail}`));
16707
+ return;
16708
+ }
16709
+ resolve3(cleanOutput(stdout));
16710
+ });
16711
+ });
16712
+ }
16713
+ function parseOpenCodeModels(output) {
16714
+ return [...new Set(cleanOutput(output).split("\n").map((line) => line.trim()).filter((line) => /^[^\s/]+\/.+/.test(line)))].sort((left, right) => left.localeCompare(right));
16715
+ }
16716
+ function parseOpenCodeAgents(output) {
16717
+ const agents = /* @__PURE__ */ new Map();
16718
+ for (const line of cleanOutput(output).split("\n")) {
16719
+ const match2 = /^([^\s()]+) \((primary|subagent|all)\)$/.exec(line.trim());
16720
+ if (!match2) continue;
16721
+ const name = match2[1];
16722
+ const mode = match2[2];
16723
+ if (name === "supertask-runner") continue;
16724
+ agents.set(name, { name, mode });
16725
+ }
16726
+ const rank = { primary: 0, all: 1, subagent: 2 };
16727
+ return [...agents.values()].sort((left, right) => rank[left.mode] - rank[right.mode] || left.name.localeCompare(right.name));
16728
+ }
16729
+ async function loadOpenCodeCatalog(cwd, options = {}) {
16730
+ validateTaskWorkingDirectory(cwd);
16731
+ const executable = options.executable ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
16732
+ const timeoutMs = options.timeoutMs ?? COMMAND_TIMEOUT_MS;
16733
+ const cacheKey = `${executable}\0${cwd}`;
16734
+ const cached = catalogCache.get(cacheKey);
16735
+ if (options.useCache !== false && cached && cached.expiresAt > Date.now()) {
16736
+ return cached.result;
16737
+ }
16738
+ const result = Promise.all([
16739
+ runOpenCode(executable, ["models"], cwd, timeoutMs),
16740
+ runOpenCode(executable, ["agent", "list"], cwd, timeoutMs)
16741
+ ]).then(([modelsOutput, agentsOutput]) => {
16742
+ const models = parseOpenCodeModels(modelsOutput);
16743
+ const agents = parseOpenCodeAgents(agentsOutput).filter((agent) => agent.mode !== "subagent");
16744
+ if (models.length === 0) throw new Error("OpenCode \u6CA1\u6709\u8FD4\u56DE\u53EF\u7528\u6A21\u578B");
16745
+ if (agents.length === 0) throw new Error("OpenCode \u6CA1\u6709\u8FD4\u56DE\u53EF\u76F4\u63A5\u8FD0\u884C\u7684\u4E3B Agent");
16746
+ return { cwd, models, agents };
16747
+ });
16748
+ if (options.useCache !== false) {
16749
+ catalogCache.set(cacheKey, { expiresAt: Date.now() + CATALOG_CACHE_MS, result });
16750
+ result.catch(() => {
16751
+ if (catalogCache.get(cacheKey)?.result === result) catalogCache.delete(cacheKey);
16752
+ });
16753
+ }
16754
+ return result;
16755
+ }
16756
+
16614
16757
  // src/core/services/database-maintenance.service.ts
16615
16758
  import { Database as Database3 } from "bun:sqlite";
16616
16759
  import { randomUUID } from "crypto";
@@ -16619,7 +16762,7 @@ import {
16619
16762
  existsSync as existsSync2,
16620
16763
  mkdirSync as mkdirSync2,
16621
16764
  renameSync,
16622
- statSync,
16765
+ statSync as statSync2,
16623
16766
  unlinkSync,
16624
16767
  writeFileSync
16625
16768
  } from "fs";
@@ -16728,9 +16871,9 @@ var DatabaseMaintenanceService = class {
16728
16871
  if (!existsSync2(source)) {
16729
16872
  throw new Error(`\u5907\u4EFD\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${source}`);
16730
16873
  }
16731
- const sourceStat = statSync(source);
16874
+ const sourceStat = statSync2(source);
16732
16875
  if (!sourceStat.isFile()) throw new Error(`\u5907\u4EFD\u8DEF\u5F84\u4E0D\u662F\u6587\u4EF6\uFF1A${source}`);
16733
- const liveStat = statSync(livePath);
16876
+ const liveStat = statSync2(livePath);
16734
16877
  if (sourceStat.dev === liveStat.dev && sourceStat.ino === liveStat.ino) {
16735
16878
  throw new Error("\u6062\u590D\u6765\u6E90\u4E0D\u80FD\u662F\u5F53\u524D\u6570\u636E\u5E93\u7684\u7B26\u53F7\u94FE\u63A5\u6216\u786C\u94FE\u63A5\u522B\u540D");
16736
16879
  }
@@ -16971,7 +17114,7 @@ var DatabaseMaintenanceService = class {
16971
17114
  const counts = missingTables.length === 0 ? this.readCounts(sqlite2) : { tasks: 0, taskRuns: 0, taskTemplates: 0 };
16972
17115
  const runningTasks = missingTables.includes("tasks") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
16973
17116
  const runningRuns = missingTables.includes("task_runs") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
16974
- const sizeBytes = path === ":memory:" || !existsSync2(path) ? sqlite2.serialize().byteLength : statSync(path).size;
17117
+ const sizeBytes = path === ":memory:" || !existsSync2(path) ? sqlite2.serialize().byteLength : statSync2(path).size;
16975
17118
  return {
16976
17119
  ok: integrityMessages.length === 1 && integrityMessages[0] === "ok" && foreignKeyViolations === 0 && missingTables.length === 0,
16977
17120
  path: normalizedPath(path),
@@ -17008,7 +17151,7 @@ var DatabaseMaintenanceService = class {
17008
17151
  const check = this.inspectFile(temporary);
17009
17152
  if (!check.ok) throw new Error("\u65B0\u5907\u4EFD\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
17010
17153
  renameSync(temporary, output);
17011
- const finalCheck = { ...check, path: output, sizeBytes: statSync(output).size };
17154
+ const finalCheck = { ...check, path: output, sizeBytes: statSync2(output).size };
17012
17155
  return { path: output, sizeBytes: finalCheck.sizeBytes, check: finalCheck };
17013
17156
  } catch (error) {
17014
17157
  safeUnlink(temporary);
@@ -17244,35 +17387,6 @@ function computeBackoff(retryCount, baseMs = 3e4, maxMs = MAX_BACKOFF_MS) {
17244
17387
  return Math.min(baseMs * Math.pow(2, retryCount - 1), maxMs);
17245
17388
  }
17246
17389
 
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
17390
  // src/core/task-batch.ts
17277
17391
  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
17392
  function normalizeTaskBatchId(batchId) {
@@ -19058,6 +19172,13 @@ var ZH = {
19058
19172
  "action.updateTask": "\u4FDD\u5B58\u4FEE\u6539",
19059
19173
  "action.createTemplate": "\u65B0\u5EFA\u5B9A\u65F6\u4EFB\u52A1",
19060
19174
  "action.saveTemplate": "\u4FDD\u5B58\u5B9A\u65F6\u4EFB\u52A1",
19175
+ "action.chooseFolder": "\u9009\u62E9\u6587\u4EF6\u5939",
19176
+ "action.chooseThisFolder": "\u9009\u62E9\u5F53\u524D\u6587\u4EF6\u5939",
19177
+ "action.home": "\u4E3B\u76EE\u5F55",
19178
+ "action.up": "\u4E0A\u4E00\u7EA7",
19179
+ "action.copyCommand": "\u590D\u5236\u547D\u4EE4",
19180
+ "action.showHidden": "\u663E\u793A\u9690\u85CF\u6587\u4EF6\u5939",
19181
+ "action.hideHidden": "\u9690\u85CF\u9690\u85CF\u6587\u4EF6\u5939",
19061
19182
  "status.pending": "\u5F85\u6267\u884C",
19062
19183
  "status.running": "\u8FD0\u884C\u4E2D",
19063
19184
  "status.done": "\u5DF2\u5B8C\u6210",
@@ -19124,6 +19245,15 @@ var ZH = {
19124
19245
  "schedule.hours": "{count} \u5C0F\u65F6",
19125
19246
  "schedule.days": "{count} \u5929",
19126
19247
  "schedule.overdue": "\u5DF2\u5230\u671F",
19248
+ "duration.unit": "\u65F6\u95F4\u5355\u4F4D",
19249
+ "duration.seconds": "\u79D2",
19250
+ "duration.minutes": "\u5206\u949F",
19251
+ "duration.hours": "\u5C0F\u65F6",
19252
+ "duration.days": "\u5929",
19253
+ "duration.systemDefault": "\u4F7F\u7528 Gateway \u9ED8\u8BA4\u8D85\u65F6",
19254
+ "duration.immediate": "\u7ACB\u5373\u91CD\u8BD5",
19255
+ "duration.custom": "\u81EA\u5B9A\u4E49\u2026",
19256
+ "duration.every": "\u6BCF {duration}",
19127
19257
  "system.worker": "\u4EFB\u52A1\u6267\u884C",
19128
19258
  "system.scheduler": "\u5B9A\u65F6\u4EFB\u52A1\u670D\u52A1",
19129
19259
  "system.watchdog": "\u8FD0\u884C\u76D1\u63A7",
@@ -19171,6 +19301,9 @@ var ZH = {
19171
19301
  "template.interval": "\u6267\u884C\u95F4\u9694",
19172
19302
  "template.runAt": "\u6267\u884C\u65F6\u95F4",
19173
19303
  "template.durationHint": "\u652F\u6301 30s\u30015min\u30011h\u30012d",
19304
+ "template.intervalHint": "\u76F4\u63A5\u9009\u62E9\u5E38\u7528\u9891\u7387\uFF1B\u53EA\u6709\u7279\u6B8A\u9700\u6C42\u624D\u9700\u8981\u81EA\u5B9A\u4E49\u3002",
19305
+ "template.retryBackoffHint": "\u4E00\u6B21\u5931\u8D25\u540E\uFF0C\u7B49\u5F85\u591A\u4E45\u518D\u91CD\u8BD5\u3002",
19306
+ "template.timeoutHint": "\u7559\u7A7A\u8868\u793A\u4F7F\u7528 Gateway \u7684\u9ED8\u8BA4\u4EFB\u52A1\u8D85\u65F6\u3002",
19174
19307
  "template.advanced": "\u66F4\u591A\u6267\u884C\u8BBE\u7F6E",
19175
19308
  "template.category": "\u5206\u7C7B",
19176
19309
  "template.batchId": "\u6279\u6B21 ID",
@@ -19195,6 +19328,28 @@ var ZH = {
19195
19328
  "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
19329
  "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
19330
  "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",
19331
+ "catalog.chooseProject": "\u8BF7\u5148\u9009\u62E9\u9879\u76EE\u76EE\u5F55",
19332
+ "catalog.defaultModel": "\u8DDF\u968F Agent / OpenCode \u9ED8\u8BA4\u6A21\u578B",
19333
+ "catalog.defaultProvider": "\u9ED8\u8BA4\u6A21\u578B",
19334
+ "catalog.provider": "\u6A21\u578B\u63D0\u4F9B\u5546",
19335
+ "catalog.model": "\u5177\u4F53\u6A21\u578B",
19336
+ "catalog.modelHint": "\u5148\u9009\u63D0\u4F9B\u5546\uFF0C\u518D\u9009\u672C\u9879\u76EE opencode models \u8FD4\u56DE\u7684\u6A21\u578B\uFF1B\u9ED8\u8BA4\u9009\u9879\u4E0D\u4F1A\u4F20\u5165 -m\u3002",
19337
+ "catalog.agentHint": "\u6765\u81EA\u5F53\u524D\u9879\u76EE\u7684 opencode agent list\u3002",
19338
+ "catalog.loading": "\u6B63\u5728\u8BFB\u53D6\u6B64\u9879\u76EE\u53EF\u7528\u7684 Agent \u548C\u6A21\u578B\u2026",
19339
+ "catalog.loaded": "\u5DF2\u4ECE\u672C\u673A OpenCode \u8BFB\u53D6 {agents} \u4E2A Agent\u3001{models} \u4E2A\u6A21\u578B\u3002",
19340
+ "catalog.failed": "\u65E0\u6CD5\u8BFB\u53D6\u6B64\u9879\u76EE\u7684 OpenCode \u914D\u7F6E\uFF1A{error}",
19341
+ "catalog.primary": "\u4E3B Agent",
19342
+ "catalog.subagent": "\u5B50 Agent",
19343
+ "catalog.all": "\u901A\u7528 Agent",
19344
+ "directory.title": "\u9009\u62E9\u9879\u76EE\u76EE\u5F55",
19345
+ "directory.subtitle": "\u9009\u62E9\u540E\uFF0C\u7CFB\u7EDF\u4F1A\u5728\u8BE5\u76EE\u5F55\u8FD0\u884C OpenCode\uFF0C\u5E76\u8BFB\u53D6\u8BE5\u9879\u76EE\u53EF\u7528\u7684 Agent \u548C\u6A21\u578B\u3002",
19346
+ "directory.empty": "\u8FD9\u4E2A\u6587\u4EF6\u5939\u4E2D\u6CA1\u6709\u5B50\u6587\u4EF6\u5939",
19347
+ "logs.command": "\u5B9E\u9645\u6267\u884C\u547D\u4EE4",
19348
+ "logs.output": "Agent \u8F93\u51FA",
19349
+ "logs.error": "\u5931\u8D25\u539F\u56E0",
19350
+ "logs.tools": "\u5DE5\u5177\u8C03\u7528",
19351
+ "logs.raw": "\u67E5\u770B\u539F\u59CB\u6267\u884C\u65E5\u5FD7",
19352
+ "logs.noText": "\u8FD9\u6B21\u6267\u884C\u6CA1\u6709\u4EA7\u751F\u53EF\u5C55\u793A\u7684\u6587\u672C\u8F93\u51FA\uFF0C\u8BF7\u67E5\u770B\u539F\u59CB\u65E5\u5FD7\u3002",
19198
19353
  "theme.label": "\u4E3B\u9898",
19199
19354
  "theme.system": "\u8DDF\u968F\u7CFB\u7EDF",
19200
19355
  "theme.light": "\u6D45\u8272",
@@ -19232,6 +19387,7 @@ var ZH = {
19232
19387
  "feedback.templateUpdated": "\u5B9A\u65F6\u4EFB\u52A1\u5DF2\u66F4\u65B0",
19233
19388
  "feedback.configSaved": "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58",
19234
19389
  "feedback.sessionCommandCopied": "\u4F1A\u8BDD\u547D\u4EE4\u5DF2\u590D\u5236\uFF0C\u53EF\u76F4\u63A5\u7C98\u8D34\u5230\u7EC8\u7AEF\u8FD0\u884C",
19390
+ "feedback.commandCopied": "\u6267\u884C\u547D\u4EE4\u5DF2\u590D\u5236",
19235
19391
  "feedback.restarting": "Gateway \u6B63\u5728\u91CD\u542F\uFF0C\u8BF7\u7A0D\u5019\u2026",
19236
19392
  "feedback.restartTimeout": "Gateway \u5C1A\u672A\u6062\u590D\uFF0C\u8BF7\u7A0D\u540E\u5237\u65B0\u6216\u68C0\u67E5 PM2 \u72B6\u6001",
19237
19393
  "feedback.databaseCleared": "\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u5907\u4EFD\u4F4D\u4E8E\uFF1A{path}",
@@ -19280,6 +19436,13 @@ var EN = {
19280
19436
  "action.updateTask": "Save changes",
19281
19437
  "action.createTemplate": "New scheduled task",
19282
19438
  "action.saveTemplate": "Save scheduled task",
19439
+ "action.chooseFolder": "Choose folder",
19440
+ "action.chooseThisFolder": "Choose this folder",
19441
+ "action.home": "Home",
19442
+ "action.up": "Up",
19443
+ "action.copyCommand": "Copy command",
19444
+ "action.showHidden": "Show hidden folders",
19445
+ "action.hideHidden": "Hide hidden folders",
19283
19446
  "status.pending": "Pending",
19284
19447
  "status.running": "Running",
19285
19448
  "status.done": "Done",
@@ -19346,6 +19509,15 @@ var EN = {
19346
19509
  "schedule.hours": "{count} hr",
19347
19510
  "schedule.days": "{count} days",
19348
19511
  "schedule.overdue": "Overdue",
19512
+ "duration.unit": "Time unit",
19513
+ "duration.seconds": "seconds",
19514
+ "duration.minutes": "minutes",
19515
+ "duration.hours": "hours",
19516
+ "duration.days": "days",
19517
+ "duration.systemDefault": "Use Gateway default timeout",
19518
+ "duration.immediate": "Retry immediately",
19519
+ "duration.custom": "Custom\u2026",
19520
+ "duration.every": "Every {duration}",
19349
19521
  "system.worker": "Task execution",
19350
19522
  "system.scheduler": "Scheduled task service",
19351
19523
  "system.watchdog": "Runtime monitor",
@@ -19393,6 +19565,9 @@ var EN = {
19393
19565
  "template.interval": "Interval",
19394
19566
  "template.runAt": "Run at",
19395
19567
  "template.durationHint": "Supports 30s, 5min, 1h, 2d",
19568
+ "template.intervalHint": "Choose a common frequency directly; customize only when necessary.",
19569
+ "template.retryBackoffHint": "How long to wait after a failure before retrying.",
19570
+ "template.timeoutHint": "Leave blank to use the Gateway default task timeout.",
19396
19571
  "template.advanced": "More execution settings",
19397
19572
  "template.category": "Category",
19398
19573
  "template.batchId": "Batch ID",
@@ -19417,6 +19592,28 @@ var EN = {
19417
19592
  "task.batchHint": "Tasks with the same non-empty batch ID run serially. Blank removes the batch constraint; global concurrency and dependencies still apply.",
19418
19593
  "task.projectExisting": "This project has {total} tasks: {running} running, {pending} queued, and {failed} with issues.",
19419
19594
  "task.projectNew": "This is a new project group. It appears in the project list after creation.",
19595
+ "catalog.chooseProject": "Choose a project directory first",
19596
+ "catalog.defaultModel": "Use the Agent / OpenCode default model",
19597
+ "catalog.defaultProvider": "Default model",
19598
+ "catalog.provider": "Model provider",
19599
+ "catalog.model": "Model",
19600
+ "catalog.modelHint": "Choose a provider, then a model returned by opencode models for this project. Default does not pass -m.",
19601
+ "catalog.agentHint": "Loaded from opencode agent list for this project.",
19602
+ "catalog.loading": "Loading Agents and models available to this project\u2026",
19603
+ "catalog.loaded": "Loaded {agents} Agents and {models} models from local OpenCode.",
19604
+ "catalog.failed": "Could not load this project\u2019s OpenCode configuration: {error}",
19605
+ "catalog.primary": "primary Agent",
19606
+ "catalog.subagent": "subagent",
19607
+ "catalog.all": "general Agent",
19608
+ "directory.title": "Choose project directory",
19609
+ "directory.subtitle": "OpenCode runs in this directory, and its project-specific Agents and models are loaded.",
19610
+ "directory.empty": "This folder has no subfolders",
19611
+ "logs.command": "Executed command",
19612
+ "logs.output": "Agent output",
19613
+ "logs.error": "Failure reason",
19614
+ "logs.tools": "Tool calls",
19615
+ "logs.raw": "View raw execution log",
19616
+ "logs.noText": "This run produced no displayable text. Inspect the raw log for details.",
19420
19617
  "theme.label": "Theme",
19421
19618
  "theme.system": "System",
19422
19619
  "theme.light": "Light",
@@ -19454,6 +19651,7 @@ var EN = {
19454
19651
  "feedback.templateUpdated": "Scheduled task updated",
19455
19652
  "feedback.configSaved": "Settings saved",
19456
19653
  "feedback.sessionCommandCopied": "Session command copied. Paste it into a terminal to continue.",
19654
+ "feedback.commandCopied": "Execution command copied",
19457
19655
  "feedback.restarting": "Gateway is restarting\u2026",
19458
19656
  "feedback.restartTimeout": "Gateway has not recovered yet. Refresh later or check PM2 status.",
19459
19657
  "feedback.databaseCleared": "Database cleared. Backup: {path}",
@@ -19523,7 +19721,8 @@ function icon(name, className = "icon") {
19523
19721
  check: '<path d="m5 12 4 4L19 6"/>',
19524
19722
  alert: '<path d="M12 3 2.8 19h18.4L12 3Z"/><path d="M12 9v4M12 17h.01"/>',
19525
19723
  clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
19526
- database: '<ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v6c0 1.66 3.58 3 8 3s8-1.34 8-3V5M4 11v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6"/>'
19724
+ database: '<ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v6c0 1.66 3.58 3 8 3s8-1.34 8-3V5M4 11v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6"/>',
19725
+ folder: '<path d="M3 7.5A2.5 2.5 0 0 1 5.5 5H10l2 2h6.5A2.5 2.5 0 0 1 21 9.5v7A2.5 2.5 0 0 1 18.5 19h-13A2.5 2.5 0 0 1 3 16.5v-9Z"/>'
19527
19726
  };
19528
19727
  return `<svg class="${className}" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${paths[name]}</svg>`;
19529
19728
  }
@@ -19730,6 +19929,18 @@ var STYLES = `
19730
19929
  .danger-card h2 .icon { width:17px; height:17px; }
19731
19930
  .danger-card p { max-width:800px; margin:0 0 14px; color:var(--text-2); font-size:12px; }
19732
19931
  .log-panel { margin:12px 0; animation:reveal .18s ease both; }
19932
+ .log-content { display:grid; gap:14px; padding:16px; }
19933
+ .log-section-head { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:7px; }
19934
+ .run-command,.run-output,.run-error,.run-tools { min-width:0; }
19935
+ .run-command strong,.run-output>strong,.run-error>strong,.run-tools>strong { display:block; margin-bottom:7px; font-size:12px; }
19936
+ .command-cwd { margin-bottom:6px; color:var(--text-3); font-family:"SFMono-Regular",Consolas,monospace; font-size:10px; overflow-wrap:anywhere; }
19937
+ .run-command pre,.run-output pre,.run-error pre { margin:0; padding:12px; overflow:auto; border:1px solid var(--border); border-radius:9px;
19938
+ color:var(--text-2); background:var(--surface-2); font-family:"SFMono-Regular",Consolas,monospace; font-size:11px; white-space:pre-wrap; overflow-wrap:anywhere; }
19939
+ .run-output pre { color:var(--text); font-family:inherit; font-size:13px; line-height:1.65; }
19940
+ .run-error pre { color:var(--red); border-color:color-mix(in srgb,var(--red) 28%,var(--border)); background:var(--red-soft); }
19941
+ .raw-log { padding-top:12px; border-top:1px solid var(--border); }
19942
+ .raw-log summary { color:var(--text-2); cursor:pointer; font-size:12px; font-weight:650; }
19943
+ .raw-log .log-box { margin-top:10px; border-radius:9px; }
19733
19944
  .log-box { max-height:360px; overflow:auto; padding:16px; color:var(--text-2); background:#0b1018; font-family:"SFMono-Regular",Consolas,monospace;
19734
19945
  font-size:12px; white-space:pre-wrap; overflow-wrap:anywhere; }
19735
19946
  :root[data-theme="light"] .log-box { color:#dbe5f3; }
@@ -19748,12 +19959,33 @@ var STYLES = `
19748
19959
  .form-field-wide { grid-column:1 / -1; }
19749
19960
  .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
19961
  outline:none; color:var(--text); background:var(--surface-2); font-weight:450; }
19962
+ .field-action { display:flex; align-items:stretch; gap:7px; }
19963
+ .field-action input { min-width:0; flex:1; }
19964
+ .field-action .btn { flex:0 0 auto; white-space:nowrap; }
19965
+ .model-selector { display:grid; grid-template-columns:minmax(120px,.75fr) minmax(0,1.25fr); gap:7px; }
19966
+ .duration-picker { display:grid; gap:7px; }
19967
+ .duration-control { display:grid; grid-template-columns:minmax(0,1fr) 112px; gap:7px; }
19968
+ .duration-control input,.duration-control select { min-width:0; }
19751
19969
  .form-field textarea { resize:vertical; line-height:1.5; }
19752
19970
  .form-field input:focus,.form-field select:focus,.form-field textarea:focus { border-color:var(--primary); box-shadow:var(--focus); background:var(--surface); }
19753
19971
  .form-field small { color:var(--text-3); font-size:10px; font-weight:450; }
19754
19972
  .advanced-fields { margin-top:18px; padding-top:14px; border-top:1px solid var(--border); }
19755
19973
  .advanced-fields summary { margin-bottom:14px; color:var(--text-2); cursor:pointer; font-size:12px; font-weight:700; }
19756
19974
  .form-note { margin:16px 0 0; color:var(--text-3); font-size:11px; }
19975
+ .catalog-status[data-state="loading"] { color:var(--blue); }
19976
+ .catalog-status[data-state="ready"] { color:var(--green); }
19977
+ .catalog-status[data-state="error"] { color:var(--red); }
19978
+ .directory-dialog { width:min(720px,calc(100% - 32px)); }
19979
+ .directory-toolbar { display:flex; align-items:center; gap:8px; margin-bottom:12px; }
19980
+ .directory-path { min-width:0; flex:1; padding:9px 11px; overflow:hidden; border:1px solid var(--border); border-radius:9px;
19981
+ color:var(--text-2); background:var(--surface-2); font-family:"SFMono-Regular",Consolas,monospace; font-size:11px; text-overflow:ellipsis; white-space:nowrap; }
19982
+ .directory-list { min-height:260px; max-height:48vh; display:grid; align-content:start; gap:6px; overflow:auto; }
19983
+ .directory-item { width:100%; min-height:40px; display:flex; align-items:center; gap:9px; padding:0 11px; border:1px solid transparent; border-radius:9px;
19984
+ color:var(--text-2); background:transparent; cursor:pointer; text-align:left; transition:background-color .15s ease,border-color .15s ease,color .15s ease,transform .12s ease; }
19985
+ .directory-item:hover { color:var(--text); border-color:var(--border); background:var(--surface-2); }
19986
+ .directory-item:active { transform:scale(.99); }
19987
+ .directory-item .icon { width:17px; height:17px; color:var(--primary); flex:0 0 auto; }
19988
+ .directory-empty { min-height:220px; display:grid; place-items:center; color:var(--text-3); }
19757
19989
  .json-view { min-height:160px; margin:0; padding:15px; overflow:auto; border:1px solid var(--border); border-radius:10px; color:var(--text-2);
19758
19990
  background:var(--surface-2); font-size:12px; white-space:pre-wrap; overflow-wrap:anywhere; }
19759
19991
  .confirm-copy { color:var(--text-2); margin:0; }
@@ -19822,6 +20054,9 @@ var STYLES = `
19822
20054
  .project-grid { grid-template-columns:1fr; }
19823
20055
  .template-form-grid { grid-template-columns:1fr; }
19824
20056
  .form-field-wide { grid-column:auto; }
20057
+ .field-action { align-items:stretch; flex-direction:column; }
20058
+ .field-action .btn { width:100%; }
20059
+ .model-selector { grid-template-columns:1fr; }
19825
20060
  }
19826
20061
  @media (max-width:520px) {
19827
20062
  .stats-grid,.stats-grid.three { grid-template-columns:1fr 1fr; }
@@ -19893,7 +20128,20 @@ function clientMessages(locale) {
19893
20128
  "feedback.restartTimeout",
19894
20129
  "template.createTitle",
19895
20130
  "template.editTitle",
19896
- "filter.noResults"
20131
+ "filter.noResults",
20132
+ "catalog.chooseProject",
20133
+ "catalog.defaultModel",
20134
+ "catalog.defaultProvider",
20135
+ "catalog.loading",
20136
+ "catalog.loaded",
20137
+ "catalog.failed",
20138
+ "catalog.primary",
20139
+ "catalog.subagent",
20140
+ "catalog.all",
20141
+ "directory.empty",
20142
+ "feedback.commandCopied",
20143
+ "action.showHidden",
20144
+ "action.hideHidden"
19897
20145
  ];
19898
20146
  return Object.fromEntries(keys.map((key) => [key, t(locale, key)]));
19899
20147
  }
@@ -19961,6 +20209,11 @@ function renderLayout(options) {
19961
20209
  <footer>${t(locale, "app.footer")}</footer>
19962
20210
  </div>
19963
20211
  <div id="toast-region" class="toast-region" role="status" aria-live="polite"></div>
20212
+ <dialog id="directory-dialog" class="directory-dialog">
20213
+ <div class="dialog-head"><div><h2>${t(locale, "directory.title")}</h2><p>${t(locale, "directory.subtitle")}</p></div><button type="button" class="icon-button" onclick="document.getElementById('directory-dialog').close()" aria-label="${t(locale, "action.close")}">${icon("close")}</button></div>
20214
+ <div class="dialog-body"><div class="directory-toolbar"><button id="directory-home" type="button" class="btn">${t(locale, "action.home")}</button><button id="directory-up" type="button" class="btn">${t(locale, "action.up")}</button><button id="directory-hidden" type="button" class="btn">${t(locale, "action.showHidden")}</button><div id="directory-path" class="directory-path"></div></div><div id="directory-list" class="directory-list"></div></div>
20215
+ <div class="dialog-actions"><button type="button" class="btn" onclick="document.getElementById('directory-dialog').close()">${t(locale, "action.cancel")}</button><button id="directory-choose" type="button" class="btn btn-primary">${icon("folder")}${t(locale, "action.chooseThisFolder")}</button></div>
20216
+ </dialog>
19964
20217
  <dialog id="detail-dialog">
19965
20218
  <div class="dialog-head"><div><h2>${t(locale, "details.title")}</h2><p>${t(locale, "details.subtitle")}</p></div><button class="icon-button" onclick="document.getElementById('detail-dialog').close()" aria-label="${t(locale, "action.close")}">${icon("close")}</button></div>
19966
20219
  <div class="dialog-body"><pre id="detail-content" class="json-view"></pre></div>
@@ -19999,21 +20252,38 @@ function renderLayout(options) {
19999
20252
  const showDetail=id=>showRecord('/api/tasks/'+id);const showRunDetail=id=>showRecord('/api/runs/'+id);const showTemplateDetail=id=>showRecord('/api/templates/'+id);
20000
20253
  async function copyDetails(){try{await navigator.clipboard.writeText(document.getElementById('detail-content').textContent);showToast(text('details.copySuccess'))}catch{showToast(text('feedback.copyFailed'),'error')}}
20001
20254
  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')}}
20255
+ async function copyRunCommand(id){try{await navigator.clipboard.writeText(document.getElementById('command-'+id).textContent);showToast(text('feedback.commandCopied'))}catch{showToast(text('feedback.copyFailed'),'error')}}
20002
20256
  function taskField(name){return document.getElementById('task-'+name)}
20257
+ function templateField(name){return document.getElementById('template-'+name)}
20003
20258
  function taskProjects(){const node=document.getElementById('task-project-data');if(!node)return {};try{return JSON.parse(node.textContent||'{}')}catch{return {}}}
20004
20259
  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'}
20260
+ const catalogTimers={};const catalogRequests={};const catalogModels={};
20261
+ function catalogField(prefix,name){return document.getElementById(prefix+'-'+name)}
20262
+ function resetCatalog(prefix){const agent=catalogField(prefix,'agent');const provider=catalogField(prefix,'model-provider');const model=catalogField(prefix,'model');if(!agent||!provider||!model)return;catalogModels[prefix]=[];agent.replaceChildren(new Option(text('catalog.chooseProject'),''));provider.replaceChildren(new Option(text('catalog.defaultProvider'),''));model.replaceChildren(new Option(text('catalog.defaultModel'),'default'));agent.disabled=false;provider.disabled=true;model.disabled=true;catalogField(prefix,'catalog-status').textContent='';}
20263
+ function appendCurrentOption(select,value){if(!value||[...select.options].some(option=>option.value===value))return;select.appendChild(new Option(value,value));}
20264
+ function modelProvider(value){const slash=value.indexOf('/');return slash>0?value.slice(0,slash):''}
20265
+ function populateModelOptions(prefix,preferredModel=''){const provider=catalogField(prefix,'model-provider');const model=catalogField(prefix,'model');if(!provider||!model)return;const selectedProvider=provider.value;model.replaceChildren();if(!selectedProvider){model.appendChild(new Option(text('catalog.defaultModel'),'default'));model.disabled=true;return}const available=(catalogModels[prefix]||[]).filter(value=>modelProvider(value)===selectedProvider);for(const value of available)model.appendChild(new Option(value,value));if(preferredModel&&available.includes(preferredModel))model.value=preferredModel;model.disabled=available.length===0}
20266
+ async function loadCatalog(prefix,preferredAgent='',preferredModel='default',preserveUnavailable=false){const cwd=catalogField(prefix,'cwd')?.value.trim()||'';const status=catalogField(prefix,'catalog-status');const agent=catalogField(prefix,'agent');const provider=catalogField(prefix,'model-provider');const model=catalogField(prefix,'model');if(!cwd||!status||!agent||!provider||!model){if(!cwd)resetCatalog(prefix);return}const request=(catalogRequests[prefix]||0)+1;catalogRequests[prefix]=request;status.dataset.state='loading';status.textContent=text('catalog.loading');agent.disabled=true;provider.disabled=true;model.disabled=true;try{const data=await readJson(await fetch('/api/opencode/catalog?cwd='+encodeURIComponent(cwd)));if(catalogRequests[prefix]!==request||catalogField(prefix,'cwd').value.trim()!==cwd)return;agent.replaceChildren();for(const item of data.agents){const label=item.name+' \u2014 '+text('catalog.'+item.mode);agent.appendChild(new Option(label,item.name))}if(preserveUnavailable)appendCurrentOption(agent,preferredAgent);const defaultAgent=preferredAgent||data.agents.find(item=>item.name==='build')?.name||data.agents.find(item=>item.mode==='primary')?.name||data.agents[0]?.name||'';agent.value=[...agent.options].some(option=>option.value===defaultAgent)?defaultAgent:(agent.options[0]?.value||'');catalogModels[prefix]=[...data.models];if(preserveUnavailable&&preferredModel&&preferredModel!=='default'&&!catalogModels[prefix].includes(preferredModel))catalogModels[prefix].push(preferredModel);provider.replaceChildren(new Option(text('catalog.defaultProvider'),''));for(const name of [...new Set(catalogModels[prefix].map(modelProvider).filter(Boolean))].sort())provider.appendChild(new Option(name,name));const preferredProvider=preferredModel==='default'?'':modelProvider(preferredModel);provider.value=[...provider.options].some(option=>option.value===preferredProvider)?preferredProvider:'';populateModelOptions(prefix,preferredModel);status.dataset.state='ready';status.textContent=text('catalog.loaded',{agents:data.agents.length,models:data.models.length})}catch(error){if(catalogRequests[prefix]!==request)return;resetCatalog(prefix);if(preserveUnavailable){appendCurrentOption(agent,preferredAgent);if(preferredModel&&preferredModel!=='default'){catalogModels[prefix]=[preferredModel];appendCurrentOption(provider,modelProvider(preferredModel));provider.value=modelProvider(preferredModel);populateModelOptions(prefix,preferredModel)}}status.dataset.state='error';status.textContent=text('catalog.failed',{error:error.message})}finally{if(catalogRequests[prefix]===request){agent.disabled=false;provider.disabled=false;if(provider.value)model.disabled=false}}}
20267
+ function scheduleCatalogLoad(prefix){clearTimeout(catalogTimers[prefix]);catalogTimers[prefix]=setTimeout(()=>loadCatalog(prefix),450)}
20268
+ let directoryTargetId='';let directoryCurrent='';let directoryEntries=[];let directoryShowHidden=false;
20269
+ function renderDirectoryEntries(){const list=document.getElementById('directory-list');list.replaceChildren();const entries=directoryEntries.filter(entry=>directoryShowHidden||!entry.hidden);const hidden=document.getElementById('directory-hidden');hidden.textContent=text(directoryShowHidden?'action.hideHidden':'action.showHidden');if(entries.length===0){const empty=document.createElement('div');empty.className='directory-empty';empty.textContent=text('directory.empty');list.appendChild(empty);return}for(const entry of entries){const button=document.createElement('button');button.type='button';button.className='directory-item';button.innerHTML='${icon("folder")}<span></span>';button.querySelector('span').textContent=entry.name;button.onclick=()=>browseDirectory(entry.path);list.appendChild(button)}}
20270
+ async function browseDirectory(path=''){const choose=document.getElementById('directory-choose');choose.disabled=true;try{const suffix=path?'?path='+encodeURIComponent(path):'';const data=await readJson(await fetch('/api/filesystem/directories'+suffix));directoryCurrent=data.path;directoryEntries=data.directories;document.getElementById('directory-path').textContent=data.path;document.getElementById('directory-up').disabled=data.parent===data.path;document.getElementById('directory-up').onclick=()=>browseDirectory(data.parent);document.getElementById('directory-home').onclick=()=>browseDirectory(data.home);renderDirectoryEntries();choose.disabled=false;return true}catch(error){directoryCurrent='';directoryEntries=[];document.getElementById('directory-path').textContent='';renderDirectoryEntries();showToast(error.message,'error');return false}}
20271
+ document.getElementById('directory-hidden').onclick=()=>{directoryShowHidden=!directoryShowHidden;renderDirectoryEntries()};
20272
+ async function openDirectoryPicker(targetId){const input=document.getElementById(targetId);if(!input||input.readOnly)return;directoryTargetId=targetId;directoryShowHidden=false;document.getElementById('directory-dialog').showModal();if(!await browseDirectory(input.value.trim()||''))await browseDirectory('')}
20273
+ document.getElementById('directory-choose').onclick=()=>{const input=document.getElementById(directoryTargetId);if(!input||!directoryCurrent)return;input.value=directoryCurrent;input.dispatchEvent(new Event('input',{bubbles:true}));document.getElementById('directory-dialog').close();const prefix=directoryTargetId.startsWith('task-')?'task':'template';loadCatalog(prefix)};
20274
+ function updateDurationControl(id){const preset=document.getElementById(id+'-preset');const custom=document.getElementById(id+'-custom');const input=document.getElementById(id+'-value');const visible=preset.value==='custom';custom.hidden=!visible;input.required=visible;if(visible&&!input.value)input.value='1'}
20275
+ function readDuration(id){const preset=document.getElementById(id+'-preset').value;if(preset==='')return '';if(preset!=='custom')return preset==='0'?'0':preset+'ms';const value=document.getElementById(id+'-value').value.trim();return value===''?'':value+document.getElementById(id+'-unit').value}
20276
+ function setDuration(id,milliseconds){const preset=document.getElementById(id+'-preset');const exact=milliseconds==null?'':String(milliseconds);if([...preset.options].some(option=>option.value===exact)){preset.value=exact;updateDurationControl(id);return}preset.value='custom';const input=document.getElementById(id+'-value');const unit=document.getElementById(id+'-unit');const units=[['d',86400000],['h',3600000],['min',60000],['s',1000]];let matched=false;for(const [name,factor] of units){if(milliseconds!=null&&(milliseconds===0||milliseconds%factor===0)){input.value=String(milliseconds/factor);unit.value=name;matched=true;break}}if(!matched){input.value=String((milliseconds??60000)/1000);unit.value='s'}updateDurationControl(id)}
20277
+ function openTaskCreator(){const form=document.getElementById('task-form');form.reset();taskField('id').value='';taskField('cwd').readOnly=false;taskField('cwd-picker').hidden=false;taskField('cwd').value=form.dataset.defaultCwd||'';setDuration('task-retry-backoff',30000);setDuration('task-timeout',null);taskField('dialog-title').textContent=text('task.createTitle');taskField('save').textContent=text('action.saveTask');updateTaskProjectStatus();resetCatalog('task');document.getElementById('task-dialog').showModal();if(taskField('cwd').value)loadCatalog('task');setTimeout(()=>taskField('name').focus(),50)}
20278
+ async function openTaskEditor(id){try{const data=await readJson(await fetch('/api/tasks/'+id));taskField('id').value=String(id);taskField('cwd').value=data.cwd||'';taskField('cwd').readOnly=true;taskField('cwd-picker').hidden=true;taskField('name').value=data.name||'';taskField('prompt').value=data.prompt||'';taskField('category').value=data.category||'general';taskField('batch').value=data.batchId||'';taskField('importance').value=String(data.importance??3);taskField('urgency').value=String(data.urgency??3);taskField('max-retries').value=String(data.maxRetries??3);setDuration('task-retry-backoff',data.retryBackoffMs??30000);setDuration('task-timeout',data.timeoutMs);taskField('dialog-title').textContent=text('task.editTitle');taskField('save').textContent=text('action.updateTask');updateTaskProjectStatus();resetCatalog('task');document.getElementById('task-dialog').showModal();loadCatalog('task',data.agent||'',data.model||'default',true);setTimeout(()=>taskField('name').focus(),50)}catch(error){showToast(error.message,'error')}}
20279
+ async function saveTask(event){event.preventDefault();const form=document.getElementById('task-form');if(!form.reportValidity())return;const id=taskField('id').value;const body={name:taskField('name').value,cwd:taskField('cwd').value,agent:taskField('agent').value,model:taskField('model').value,prompt:taskField('prompt').value,category:taskField('category').value,batchId:taskField('batch').value,importance:Number(taskField('importance').value),urgency:Number(taskField('urgency').value),maxRetries:Number(taskField('max-retries').value),retryBackoff:readDuration('task-retry-backoff'),timeout:readDuration('task-timeout')};const button=taskField('save');button.disabled=true;try{const data=await readJson(await fetch(id?'/api/tasks/'+id:'/api/tasks',{method:id?'PUT':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}));showToast(text(id?'feedback.taskUpdated':'feedback.taskCreated',{id:data.task.id}));document.getElementById('task-dialog').close();setTimeout(()=>location.assign(id?location.href:'/?cwd='+encodeURIComponent(data.task.cwd||'')),450)}catch(error){showToast(error.message,'error')}finally{button.disabled=false}}
20010
20280
  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}}
20281
+ function updateTemplateScheduleFields(){const type=templateField('schedule-type').value;const fields={cron:templateField('cron-field'),recurring:templateField('interval-field'),delayed:templateField('run-at-field')};for(const [name,node] of Object.entries(fields)){node.hidden=name!==type;for(const control of node.querySelectorAll('input,select'))control.required=false;const required=name==='recurring'?node.querySelector('[id$="-preset"]'):node.querySelector('input');if(required)required.required=name===type;if(name==='recurring'&&name===type)updateDurationControl('template-interval')}}
20012
20282
  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
20283
  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}}
20284
+ function openTemplateCreator(){const form=document.getElementById('template-form');form.reset();templateField('id').value='';templateField('dialog-title').textContent=text('template.createTitle');setDuration('template-interval',3600000);setDuration('template-retry-backoff',30000);setDuration('template-timeout',null);setOriginalRunAt(null);templateField('run-at').value=localDateTime(Date.now()+3600000);updateTemplateScheduleFields();resetCatalog('template');document.getElementById('template-dialog').showModal();if(templateField('cwd').value)loadCatalog('template');setTimeout(()=>templateField('name').focus(),50)}
20285
+ async function openTemplateEditor(id){try{const data=await readJson(await fetch('/api/templates/'+id));templateField('id').value=String(id);templateField('dialog-title').textContent=text('template.editTitle');templateField('name').value=data.name||'';templateField('cwd').value=data.cwd||'';templateField('prompt').value=data.prompt||'';templateField('schedule-type').value=data.scheduleType;templateField('cron').value=data.cronExpr||'';setDuration('template-interval',data.intervalMs);setOriginalRunAt(data.runAt||null);if(!data.runAt)templateField('run-at').value=localDateTime(Date.now()+3600000);templateField('category').value=data.category||'general';templateField('batch').value=data.batchId||'';templateField('importance').value=String(data.importance??3);templateField('urgency').value=String(data.urgency??3);templateField('max-instances').value=String(data.maxInstances??1);templateField('max-retries').value=String(data.maxRetries??3);setDuration('template-retry-backoff',data.retryBackoffMs??30000);setDuration('template-timeout',data.timeoutMs);updateTemplateScheduleFields();resetCatalog('template');document.getElementById('template-dialog').showModal();loadCatalog('template',data.agent||'',data.model||'default',true);setTimeout(()=>templateField('name').focus(),50)}catch(error){showToast(error.message,'error')}}
20286
+ async function saveTemplate(event){event.preventDefault();const form=document.getElementById('template-form');if(!form.reportValidity())return;const id=templateField('id').value;const type=templateField('schedule-type').value;const body={name:templateField('name').value,cwd:templateField('cwd').value,agent:templateField('agent').value,model:templateField('model').value,prompt:templateField('prompt').value,scheduleType:type,cronExpr:templateField('cron').value,interval:readDuration('template-interval'),runAt:type==='delayed'?selectedRunAt():null,category:templateField('category').value,batchId:templateField('batch').value,importance:Number(templateField('importance').value),urgency:Number(templateField('urgency').value),maxInstances:Number(templateField('max-instances').value),maxRetries:Number(templateField('max-retries').value),retryBackoff:readDuration('template-retry-backoff'),timeout:readDuration('template-timeout')};const button=templateField('save');button.disabled=true;try{await readJson(await fetch(id?'/api/templates/'+id:'/api/templates',{method:id?'PUT':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}));showToast(text(id?'feedback.templateUpdated':'feedback.templateCreated'));document.getElementById('template-dialog').close();setTimeout(()=>location.assign(id?location.href:'/templates'),450)}catch(error){showToast(error.message,'error')}finally{button.disabled=false}}
20017
20287
  async function enableTmpl(id){try{await readJson(await fetch('/api/templates/'+id+'/enable',{method:'POST'}));location.reload()}catch(error){showToast(error.message,'error')}}
20018
20288
  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')}}
20019
20289
  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')}}
@@ -20083,6 +20353,23 @@ function parsePositiveInteger(value) {
20083
20353
  function parseTaskStatus(value) {
20084
20354
  return TASK_STATUSES.has(value) ? value : null;
20085
20355
  }
20356
+ function listChildDirectories(path) {
20357
+ return readdirSync(path, { withFileTypes: true }).flatMap((entry) => {
20358
+ const entryPath = join4(path, entry.name);
20359
+ if (entry.isDirectory()) return [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }];
20360
+ if (!entry.isSymbolicLink()) return [];
20361
+ try {
20362
+ return statSync4(entryPath).isDirectory() ? [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }] : [];
20363
+ } catch {
20364
+ return [];
20365
+ }
20366
+ }).sort((left, right) => {
20367
+ const leftHidden = left.name.startsWith(".");
20368
+ const rightHidden = right.name.startsWith(".");
20369
+ if (leftHidden !== rightHidden) return leftHidden ? 1 : -1;
20370
+ return left.name.localeCompare(right.name);
20371
+ });
20372
+ }
20086
20373
  function parseTaskPayload(value) {
20087
20374
  if (!value || typeof value !== "object" || Array.isArray(value)) {
20088
20375
  throw new Error("\u8BF7\u6C42\u5185\u5BB9\u5FC5\u987B\u662F\u5BF9\u8C61");
@@ -20260,6 +20547,33 @@ app.get("/health", (c) => {
20260
20547
  const health = getGatewayHealth();
20261
20548
  return c.json(health, health.status === "ok" ? 200 : 503);
20262
20549
  });
20550
+ app.get("/api/filesystem/directories", (c) => {
20551
+ const requestedPath = c.req.query("path")?.trim() || homedir4();
20552
+ try {
20553
+ validateTaskWorkingDirectory(requestedPath);
20554
+ return c.json({
20555
+ path: requestedPath,
20556
+ parent: dirname4(requestedPath),
20557
+ home: homedir4(),
20558
+ directories: listChildDirectories(requestedPath)
20559
+ });
20560
+ } catch (error) {
20561
+ return c.json({
20562
+ error: error instanceof Error ? error.message : String(error)
20563
+ }, 400);
20564
+ }
20565
+ });
20566
+ app.get("/api/opencode/catalog", async (c) => {
20567
+ const cwd = c.req.query("cwd")?.trim();
20568
+ if (!cwd) return c.json({ error: "cwd \u4E0D\u80FD\u4E3A\u7A7A" }, 400);
20569
+ try {
20570
+ return c.json(await loadOpenCodeCatalog(cwd));
20571
+ } catch (error) {
20572
+ return c.json({
20573
+ error: error instanceof Error ? error.message : String(error)
20574
+ }, 400);
20575
+ }
20576
+ });
20263
20577
  function formatDuration(startAt, endAt) {
20264
20578
  if (!startAt) return "\u2014";
20265
20579
  const start = new Date(startAt).getTime();
@@ -20270,6 +20584,66 @@ function formatDuration(startAt, endAt) {
20270
20584
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
20271
20585
  return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m`;
20272
20586
  }
20587
+ function recordValue(value) {
20588
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
20589
+ }
20590
+ function shellQuote(value) {
20591
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
20592
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
20593
+ }
20594
+ function presentRunLog(log) {
20595
+ let command = null;
20596
+ const textParts = [];
20597
+ const errors = [];
20598
+ const tools = [];
20599
+ for (const line of log.split("\n")) {
20600
+ const trimmed = line.trim();
20601
+ if (!trimmed) continue;
20602
+ let parsed = null;
20603
+ try {
20604
+ parsed = recordValue(JSON.parse(trimmed));
20605
+ } catch {
20606
+ errors.push(line);
20607
+ continue;
20608
+ }
20609
+ if (!parsed) continue;
20610
+ if (parsed.type === "supertask_command") {
20611
+ const executable = typeof parsed.executable === "string" ? parsed.executable : null;
20612
+ const cwd = typeof parsed.cwd === "string" ? parsed.cwd : null;
20613
+ const args = Array.isArray(parsed.args) && parsed.args.every((item) => typeof item === "string") ? parsed.args : null;
20614
+ if (executable && cwd && args) {
20615
+ command = {
20616
+ cwd,
20617
+ command: `cd ${shellQuote(cwd)} && ${[executable, ...args].map(shellQuote).join(" ")}`
20618
+ };
20619
+ }
20620
+ continue;
20621
+ }
20622
+ const part = recordValue(parsed.part);
20623
+ const eventType = typeof parsed.type === "string" ? parsed.type : "";
20624
+ const partType = typeof part?.type === "string" ? part.type : "";
20625
+ const text2 = typeof part?.text === "string" ? part.text : typeof parsed.text === "string" ? parsed.text : null;
20626
+ if (text2 && (eventType === "text" || partType === "text")) textParts.push(text2);
20627
+ const tool = typeof part?.tool === "string" ? part.tool : typeof parsed.tool === "string" ? parsed.tool : null;
20628
+ if (tool && (eventType === "tool_use" || partType === "tool")) tools.push(tool);
20629
+ const error = typeof parsed.error === "string" ? parsed.error : typeof part?.error === "string" ? part.error : null;
20630
+ if (error) errors.push(error);
20631
+ }
20632
+ return {
20633
+ command,
20634
+ text: textParts.join("\n").trim(),
20635
+ errors: [...new Set(errors)],
20636
+ tools
20637
+ };
20638
+ }
20639
+ function renderRunLog(runId, taskName, log, locale) {
20640
+ const presentation = presentRunLog(log);
20641
+ const command = presentation.command ? `<div class="run-command"><div class="log-section-head"><strong>${t(locale, "logs.command")}</strong><button type="button" class="btn" onclick="copyRunCommand(${runId})">${icon("copy")}${t(locale, "action.copyCommand")}</button></div><div class="command-cwd">${esc(presentation.command.cwd)}</div><pre id="command-${runId}">${esc(presentation.command.command)}</pre></div>` : "";
20642
+ const errors = presentation.errors.length > 0 ? `<div class="run-error"><strong>${t(locale, "logs.error")}</strong><pre>${esc(presentation.errors.join("\n"))}</pre></div>` : "";
20643
+ const tools = presentation.tools.length > 0 ? `<div class="run-tools"><strong>${t(locale, "logs.tools")}</strong><div class="actions">${presentation.tools.map((tool) => `<span class="tag">${esc(tool)}</span>`).join("")}</div></div>` : "";
20644
+ const output = `<div class="run-output"><strong>${t(locale, "logs.output")}</strong><pre>${esc(presentation.text || t(locale, "logs.noText"))}</pre></div>`;
20645
+ return `<section id="log-${runId}" class="panel log-panel" hidden><div class="panel-head"><h3>Run #${runId} \xB7 ${esc(taskName)}</h3></div><div class="log-content">${command}${errors}${output}${tools}<details class="raw-log"><summary>${t(locale, "logs.raw")}</summary><div class="log-box">${esc(log)}</div></details></div></section>`;
20646
+ }
20273
20647
  function esc(value) {
20274
20648
  if (!value) return "";
20275
20649
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -20301,6 +20675,25 @@ function emptyState(title, hint, code = "") {
20301
20675
  return `<div class="empty-state"><div><div class="empty-icon">${icon("inbox")}</div>
20302
20676
  <h3>${title}</h3><p>${hint}</p>${code ? `<code>${code}</code>` : ""}</div></div>`;
20303
20677
  }
20678
+ function durationControl(locale, id, value, kind) {
20679
+ const units = [
20680
+ { value: "s", label: t(locale, "duration.seconds") },
20681
+ { value: "min", label: t(locale, "duration.minutes") },
20682
+ { value: "h", label: t(locale, "duration.hours") },
20683
+ { value: "d", label: t(locale, "duration.days") }
20684
+ ];
20685
+ const values = kind === "interval" ? [3e5, 9e5, 18e5, 36e5, 216e5, 432e5, 864e5] : kind === "retry" ? [0, 1e4, 3e4, 6e4, 3e5] : [3e5, 9e5, 18e5, 36e5, 72e5, 144e5];
20686
+ const presets = [
20687
+ ...kind === "timeout" ? [{ value: "", label: t(locale, "duration.systemDefault") }] : [],
20688
+ ...values.map((milliseconds) => ({
20689
+ value: String(milliseconds),
20690
+ label: milliseconds === 0 ? t(locale, "duration.immediate") : kind === "interval" ? t(locale, "duration.every", { duration: formatInterval(milliseconds, locale) }) : formatInterval(milliseconds, locale)
20691
+ })),
20692
+ { value: "custom", label: t(locale, "duration.custom") }
20693
+ ];
20694
+ const selected = value === null ? "" : String(value);
20695
+ return `<div class="duration-picker"><select id="${id}-preset" onchange="updateDurationControl('${id}')">${presets.map((item) => `<option value="${item.value}" ${item.value === selected ? "selected" : ""}>${item.label}</option>`).join("")}</select><div id="${id}-custom" class="duration-control" hidden><input id="${id}-value" type="number" min="${kind === "retry" ? 0 : 0.1}" step="0.1" inputmode="decimal"><select id="${id}-unit" aria-label="${t(locale, "duration.unit")}">${units.map((item) => `<option value="${item.value}">${item.label}</option>`).join("")}</select></div></div>`;
20696
+ }
20304
20697
  function formatInterval(milliseconds, locale) {
20305
20698
  const formatter = new Intl.NumberFormat(locale === "en" ? "en-US" : "zh-CN", {
20306
20699
  maximumFractionDigits: 1
@@ -20465,13 +20858,14 @@ app.get("/", async (c) => {
20465
20858
  <div class="dialog-body">
20466
20859
  <div class="template-form-grid">
20467
20860
  <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>
20861
+ <label class="form-field"><span>${t(locale, "template.cwd")}</span><div class="field-action"><input id="task-cwd" required autocomplete="off" list="task-cwd-options" placeholder="/path/to/project" oninput="updateTaskProjectStatus();scheduleCatalogLoad('task')"><button id="task-cwd-picker" type="button" class="btn" onclick="openDirectoryPicker('task-cwd')">${icon("folder")}${t(locale, "action.chooseFolder")}</button></div><small>${t(locale, "template.cwdHint")}</small></label>
20469
20862
  <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>
20863
+ <label class="form-field"><span>${t(locale, "template.agent")}</span><select id="task-agent" required><option value="">${t(locale, "catalog.chooseProject")}</option></select><small>${t(locale, "catalog.agentHint")}</small></label>
20864
+ <label class="form-field"><span>${t(locale, "template.model")}</span><div class="model-selector"><select id="task-model-provider" aria-label="${t(locale, "catalog.provider")}" onchange="populateModelOptions('task')"><option value="">${t(locale, "catalog.defaultProvider")}</option></select><select id="task-model" required aria-label="${t(locale, "catalog.model")}" disabled><option value="default">${t(locale, "catalog.defaultModel")}</option></select></div><small>${t(locale, "catalog.modelHint")}</small></label>
20472
20865
  <label class="form-field form-field-wide"><span>${t(locale, "template.prompt")}</span><textarea id="task-prompt" rows="6" required></textarea></label>
20473
20866
  </div>
20474
20867
  <p id="task-project-status" class="form-note"></p>
20868
+ <p id="task-catalog-status" class="form-note catalog-status"></p>
20475
20869
  <details class="advanced-fields">
20476
20870
  <summary>${t(locale, "template.advanced")}</summary>
20477
20871
  <div class="template-form-grid">
@@ -20480,8 +20874,8 @@ app.get("/", async (c) => {
20480
20874
  <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
20875
  <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
20876
  <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>
20877
+ <label class="form-field"><span>${t(locale, "template.retryBackoff")}</span>${durationControl(locale, "task-retry-backoff", 3e4, "retry")}<small>${t(locale, "template.retryBackoffHint")}</small></label>
20878
+ <label class="form-field"><span>${t(locale, "template.timeout")}</span>${durationControl(locale, "task-timeout", null, "timeout")}<small>${t(locale, "template.timeoutHint")}</small></label>
20485
20879
  </div>
20486
20880
  </details>
20487
20881
  </div>
@@ -20541,15 +20935,16 @@ app.get("/templates", async (c) => {
20541
20935
  <div class="dialog-body">
20542
20936
  <div class="template-form-grid">
20543
20937
  <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>
20938
+ <label class="form-field"><span>${t(locale, "template.cwd")}</span><div class="field-action"><input id="template-cwd" required autocomplete="off" placeholder="/path/to/project" oninput="scheduleCatalogLoad('template')"><button type="button" class="btn" onclick="openDirectoryPicker('template-cwd')">${icon("folder")}${t(locale, "action.chooseFolder")}</button></div><small>${t(locale, "template.cwdHint")}</small></label>
20939
+ <label class="form-field"><span>${t(locale, "template.agent")}</span><select id="template-agent" required><option value="">${t(locale, "catalog.chooseProject")}</option></select><small>${t(locale, "catalog.agentHint")}</small></label>
20940
+ <label class="form-field"><span>${t(locale, "template.model")}</span><div class="model-selector"><select id="template-model-provider" aria-label="${t(locale, "catalog.provider")}" onchange="populateModelOptions('template')"><option value="">${t(locale, "catalog.defaultProvider")}</option></select><select id="template-model" required aria-label="${t(locale, "catalog.model")}" disabled><option value="default">${t(locale, "catalog.defaultModel")}</option></select></div><small>${t(locale, "catalog.modelHint")}</small></label>
20547
20941
  <label class="form-field form-field-wide"><span>${t(locale, "template.prompt")}</span><textarea id="template-prompt" rows="6" required></textarea></label>
20548
20942
  <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
20943
  <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>
20944
+ <label id="template-interval-field" class="form-field"><span>${t(locale, "template.interval")}</span>${durationControl(locale, "template-interval", 36e5, "interval")}<small>${t(locale, "template.intervalHint")}</small></label>
20551
20945
  <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
20946
  </div>
20947
+ <p id="template-catalog-status" class="form-note catalog-status"></p>
20553
20948
  <details class="advanced-fields">
20554
20949
  <summary>${t(locale, "template.advanced")}</summary>
20555
20950
  <div class="template-form-grid">
@@ -20559,8 +20954,8 @@ app.get("/templates", async (c) => {
20559
20954
  <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
20955
  <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
20956
  <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>
20957
+ <label class="form-field"><span>${t(locale, "template.retryBackoff")}</span>${durationControl(locale, "template-retry-backoff", 3e4, "retry")}<small>${t(locale, "template.retryBackoffHint")}</small></label>
20958
+ <label class="form-field"><span>${t(locale, "template.timeout")}</span>${durationControl(locale, "template-timeout", null, "timeout")}<small>${t(locale, "template.timeoutHint")}</small></label>
20564
20959
  </div>
20565
20960
  </details>
20566
20961
  <p class="form-note">${t(locale, "template.futureOnly")}</p>
@@ -20602,7 +20997,7 @@ app.get("/runs", async (c) => {
20602
20997
  const status = safeStatus(run.status);
20603
20998
  const resumable = isValidSessionId(run.sessionId);
20604
20999
  if (run.log) {
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>`);
21000
+ logs.push(renderRunLog(run.id, run.taskName, run.log, locale));
20606
21001
  }
20607
21002
  return `<tr>
20608
21003
  <td class="faint" data-label="${t(locale, "table.run")}">#${run.id}</td>
@@ -20929,6 +21324,7 @@ export {
20929
21324
  dashboardApp,
20930
21325
  web_default as default,
20931
21326
  isSafeDashboardRestartTarget,
21327
+ presentRunLog,
20932
21328
  resolveDashboardConfigState,
20933
21329
  setDashboardRuntimeConfig
20934
21330
  };