ccpanel 0.1.4 → 0.1.6

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.
@@ -371,6 +371,10 @@ import { z as z4 } from "zod";
371
371
  function projectRoutes(deps) {
372
372
  const app = new Hono4();
373
373
  app.get("/", async (c) => c.json({ projects: await deps.projects.list() }));
374
+ app.get(
375
+ "/known",
376
+ async (c) => c.json({ projects: await deps.projects.known() })
377
+ );
374
378
  app.post("/", async (c) => {
375
379
  const { path } = z4.object({ path: z4.string().min(1) }).parse(await c.req.json());
376
380
  return c.json({ projects: await deps.projects.add(path) });
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  assertNoConflict,
9
9
  createApp,
10
10
  currentMtime
11
- } from "./chunk-HRPQYUJV.js";
11
+ } from "./chunk-LZFMCC5M.js";
12
12
 
13
13
  // src/cli.ts
14
14
  import { homedir, networkInterfaces } from "os";
@@ -447,19 +447,37 @@ var SkillService = class {
447
447
  };
448
448
 
449
449
  // src/core/project-service.ts
450
+ import { stat as stat2 } from "fs/promises";
450
451
  var ProjectService = class {
451
- constructor(store, registryFile) {
452
+ constructor(store, registryFile, userClaudeJson) {
452
453
  this.store = store;
453
454
  this.registryFile = registryFile;
455
+ this.userClaudeJson = userClaudeJson;
454
456
  }
455
457
  store;
456
458
  registryFile;
459
+ userClaudeJson;
457
460
  async list() {
458
461
  const data = await this.store.readJson(
459
462
  this.registryFile
460
463
  );
461
464
  return data?.projects ?? [];
462
465
  }
466
+ // Claude Code 在 ~/.claude.json 的 projects 键下记录所有打开过的项目路径,
467
+ // 作为「添加项目」的候选来源;剔除已注册与已删除的目录。
468
+ async known() {
469
+ const data = await this.store.readJson(this.userClaudeJson);
470
+ const registered = new Set(await this.list());
471
+ const out = [];
472
+ for (const p of Object.keys(data?.projects ?? {})) {
473
+ if (registered.has(p)) continue;
474
+ try {
475
+ if ((await stat2(p)).isDirectory()) out.push(p);
476
+ } catch {
477
+ }
478
+ }
479
+ return out.sort();
480
+ }
463
481
  async add(path) {
464
482
  const current = await this.list();
465
483
  if (!current.includes(path)) current.push(path);
@@ -504,12 +522,12 @@ var MemoryService = class {
504
522
  };
505
523
 
506
524
  // src/core/memory-files-service.ts
507
- import { readdir as readdir3, readFile as readFile5, stat as stat3 } from "fs/promises";
525
+ import { readdir as readdir3, readFile as readFile5, stat as stat4 } from "fs/promises";
508
526
  import { join as join5 } from "path";
509
527
  import matter2 from "gray-matter";
510
528
 
511
529
  // src/core/session-service.ts
512
- import { readdir as readdir2, readFile as readFile4, stat as stat2 } from "fs/promises";
530
+ import { readdir as readdir2, readFile as readFile4, stat as stat3 } from "fs/promises";
513
531
  import { join as join4 } from "path";
514
532
  var MAX_BLOCK = 1e4;
515
533
  var NOISE_PREFIXES = ["<local-command", "<command-", "<system-reminder", "Caveat:"];
@@ -608,7 +626,7 @@ var SessionService = class {
608
626
  const out = [];
609
627
  for (const name of names) {
610
628
  const full = join4(dir, name);
611
- const st = await stat2(full);
629
+ const st = await stat3(full);
612
630
  const { firstPrompt, messageCount } = await this.summarize(full);
613
631
  out.push({
614
632
  id: name.replace(/\.jsonl$/, ""),
@@ -720,7 +738,7 @@ var MemoryFilesService = class {
720
738
  index = await readFile5(full, "utf8");
721
739
  continue;
722
740
  }
723
- const st = await stat3(full);
741
+ const st = await stat4(full);
724
742
  const fm = await this.parseFront(full, name);
725
743
  files.push({ file: name, ...fm, size: st.size, mtime: st.mtimeMs });
726
744
  }
@@ -750,7 +768,7 @@ var MemoryFilesService = class {
750
768
  };
751
769
 
752
770
  // src/core/plugin-service.ts
753
- import { readFile as readFile6, readdir as readdir4, stat as stat4 } from "fs/promises";
771
+ import { readFile as readFile6, readdir as readdir4, stat as stat5 } from "fs/promises";
754
772
  import { isAbsolute as isAbsolute2, join as join6, relative as relative2, resolve as resolve3 } from "path";
755
773
  import matter3 from "gray-matter";
756
774
  var PluginService = class {
@@ -759,7 +777,7 @@ var PluginService = class {
759
777
  }
760
778
  store;
761
779
  async pathExists(p) {
762
- return stat4(p).then(() => true).catch(() => false);
780
+ return stat5(p).then(() => true).catch(() => false);
763
781
  }
764
782
  // 插件元数据要尽量容错:单个文件损坏不应让整页报错(启停写入除外)。
765
783
  async safeReadJson(file) {
@@ -1013,7 +1031,7 @@ var PluginService = class {
1013
1031
  };
1014
1032
 
1015
1033
  // src/core/codex-plugin-service.ts
1016
- import { readFile as readFile7, readdir as readdir5, stat as stat5 } from "fs/promises";
1034
+ import { readFile as readFile7, readdir as readdir5, stat as stat6 } from "fs/promises";
1017
1035
  import { isAbsolute as isAbsolute3, join as join7, relative as relative3, resolve as resolve4 } from "path";
1018
1036
  import matter4 from "gray-matter";
1019
1037
  import { parse as parse2, stringify as stringify2 } from "smol-toml";
@@ -1028,7 +1046,7 @@ var CodexPluginService = class {
1028
1046
  }
1029
1047
  store;
1030
1048
  async pathExists(p) {
1031
- return stat5(p).then(() => true).catch(() => false);
1049
+ return stat6(p).then(() => true).catch(() => false);
1032
1050
  }
1033
1051
  async readConfig(paths) {
1034
1052
  let raw;
@@ -1260,7 +1278,7 @@ var CodexPluginService = class {
1260
1278
  }
1261
1279
  async setEnabled(paths, id, enabled, baseMtime) {
1262
1280
  if (baseMtime !== void 0) {
1263
- const st = await stat5(paths.codexConfigToml);
1281
+ const st = await stat6(paths.codexConfigToml);
1264
1282
  if (Math.abs(st.mtimeMs - baseMtime) > 1) {
1265
1283
  throw new Error("\u914D\u7F6E\u6587\u4EF6\u5DF2\u88AB\u5176\u4ED6\u8FDB\u7A0B\u4FEE\u6539\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5");
1266
1284
  }
@@ -1274,7 +1292,7 @@ var CodexPluginService = class {
1274
1292
 
1275
1293
  // src/core/codex-memory-service.ts
1276
1294
  import Database from "better-sqlite3";
1277
- import { readFile as readFile8, readdir as readdir6, stat as stat6 } from "fs/promises";
1295
+ import { readFile as readFile8, readdir as readdir6, stat as stat7 } from "fs/promises";
1278
1296
  import { basename, extname, isAbsolute as isAbsolute4, join as join8, relative as relative4, resolve as resolve5 } from "path";
1279
1297
  var CodexDataCompatibilityError = class extends Error {
1280
1298
  constructor(message = "Codex \u8BB0\u5FC6\u6570\u636E\u5E93\u683C\u5F0F\u6682\u4E0D\u652F\u6301") {
@@ -1409,7 +1427,7 @@ var CodexMemoryService = class {
1409
1427
  for (const entry of entries) {
1410
1428
  if (!entry.isFile() || !FILE_EXTENSIONS.has(extname(entry.name))) continue;
1411
1429
  const file = join8(paths.codexMemoriesDir, entry.name);
1412
- const st = await stat6(file);
1430
+ const st = await stat7(file);
1413
1431
  out.push({
1414
1432
  id: `file:${entry.name}`,
1415
1433
  name: entry.name,
@@ -1435,7 +1453,7 @@ var CodexMemoryService = class {
1435
1453
  return content.replace(/\s+/g, " ").trim().slice(0, 120);
1436
1454
  }
1437
1455
  async exists(file) {
1438
- return stat6(file).then(() => true).catch((err) => {
1456
+ return stat7(file).then(() => true).catch((err) => {
1439
1457
  if (err.code === "ENOENT") return false;
1440
1458
  throw err;
1441
1459
  });
@@ -1443,7 +1461,7 @@ var CodexMemoryService = class {
1443
1461
  };
1444
1462
 
1445
1463
  // src/core/codex-session-service.ts
1446
- import { readdir as readdir7, readFile as readFile9, stat as stat7 } from "fs/promises";
1464
+ import { readdir as readdir7, readFile as readFile9, stat as stat8 } from "fs/promises";
1447
1465
  import { join as join9, resolve as resolve6 } from "path";
1448
1466
  var MAX_BLOCK2 = 1e4;
1449
1467
  function truncate2(s) {
@@ -1490,6 +1508,18 @@ function blocksFromContent(content) {
1490
1508
  }
1491
1509
  return out;
1492
1510
  }
1511
+ function outputText(output) {
1512
+ if (typeof output === "string") return output;
1513
+ return textFromContent(output);
1514
+ }
1515
+ function summaryText(summary) {
1516
+ if (!Array.isArray(summary)) return "";
1517
+ return summary.map((raw) => {
1518
+ if (!raw || typeof raw !== "object") return "";
1519
+ const s = raw;
1520
+ return typeof s.text === "string" ? s.text : "";
1521
+ }).filter(Boolean).join("\n");
1522
+ }
1493
1523
  function parseFunctionArguments(args) {
1494
1524
  if (typeof args !== "string") return JSON.stringify(args ?? {}, null, 2);
1495
1525
  try {
@@ -1505,7 +1535,9 @@ var CodexSessionService = class {
1505
1535
  this.readIndex(paths)
1506
1536
  ]);
1507
1537
  const project = normalizeProjectPath(projectPath);
1508
- return rollouts.filter((r) => normalizeProjectPath(r.cwd) === project).map((r) => {
1538
+ return rollouts.filter(
1539
+ (r) => normalizeProjectPath(r.cwd) === project && r.threadSource !== "subagent"
1540
+ ).map((r) => {
1509
1541
  const idx = index.get(r.id);
1510
1542
  const mtime = idx?.updatedAt ? Date.parse(idx.updatedAt) : r.mtime;
1511
1543
  return {
@@ -1520,12 +1552,38 @@ var CodexSessionService = class {
1520
1552
  async read(paths, projectPath, id) {
1521
1553
  assertSafeId2(id);
1522
1554
  const project = normalizeProjectPath(projectPath);
1523
- const found = (await this.scanRollouts(paths)).find(
1555
+ const rollouts = await this.scanRollouts(paths);
1556
+ const found = rollouts.find(
1524
1557
  (r) => r.id === id && normalizeProjectPath(r.cwd) === project
1525
1558
  );
1526
1559
  if (!found) throw new Error(`\u672A\u627E\u5230\u4F1A\u8BDD\uFF1A${id}`);
1527
1560
  const parsed = this.parseDetail(await readFile9(found.file, "utf8"));
1528
- return { messages: parsed.messages, subagents: [], skippedRecords: parsed.skipped };
1561
+ const subagents = await this.readSubagents(rollouts, parsed.subagentRefs);
1562
+ return { messages: parsed.messages, subagents, skippedRecords: parsed.skipped };
1563
+ }
1564
+ // 按 sub_agent_activity(started) 引用加载子会话;文件缺失或解析失败均跳过,
1565
+ // 不影响主会话展示。只嵌套一层:子会话内部的 subagentRefs 不再展开。
1566
+ async readSubagents(rollouts, refs) {
1567
+ const out = [];
1568
+ for (const ref of refs) {
1569
+ const rollout = rollouts.find((r) => r.id === ref.threadId);
1570
+ if (!rollout) continue;
1571
+ let parsed;
1572
+ try {
1573
+ parsed = this.parseDetail(await readFile9(rollout.file, "utf8"));
1574
+ } catch {
1575
+ continue;
1576
+ }
1577
+ const firstUser = parsed.messages.find((m) => m.type === "user");
1578
+ out.push({
1579
+ toolUseId: ref.toolUseId,
1580
+ agentId: ref.threadId,
1581
+ agentType: ref.agentPath.split("/").filter(Boolean).pop() ?? "subagent",
1582
+ description: (firstUser?.blocks[0]?.text ?? "").replace(/\s+/g, " ").trim().slice(0, 200),
1583
+ messages: parsed.messages
1584
+ });
1585
+ }
1586
+ return out;
1529
1587
  }
1530
1588
  async scanRollouts(paths) {
1531
1589
  const files = await this.rolloutFiles(paths.codexSessionsDir);
@@ -1556,12 +1614,13 @@ var CodexSessionService = class {
1556
1614
  return out;
1557
1615
  }
1558
1616
  async summarizeRollout(file) {
1559
- const st = await stat7(file);
1617
+ const st = await stat8(file);
1560
1618
  const raw = await readFile9(file, "utf8");
1561
1619
  let id = "";
1562
1620
  let cwd = "";
1563
1621
  let firstPrompt = "";
1564
1622
  let messageCount = 0;
1623
+ let threadSource;
1565
1624
  for (const line of raw.split("\n")) {
1566
1625
  if (!line.trim()) continue;
1567
1626
  let obj;
@@ -1574,6 +1633,7 @@ var CodexSessionService = class {
1574
1633
  if (obj.type === "session_meta" || payload.type === "session_meta") {
1575
1634
  id = String(payload.id ?? id);
1576
1635
  cwd = String(payload.cwd ?? cwd);
1636
+ if (payload.thread_source) threadSource = String(payload.thread_source);
1577
1637
  continue;
1578
1638
  }
1579
1639
  if (payload.type === "message") {
@@ -1589,7 +1649,16 @@ var CodexSessionService = class {
1589
1649
  id = m?.[1] ?? "";
1590
1650
  }
1591
1651
  if (!id || !cwd) return void 0;
1592
- return { id, file, cwd, mtime: st.mtimeMs, size: st.size, firstPrompt, messageCount };
1652
+ return {
1653
+ id,
1654
+ file,
1655
+ cwd,
1656
+ mtime: st.mtimeMs,
1657
+ size: st.size,
1658
+ firstPrompt,
1659
+ messageCount,
1660
+ threadSource
1661
+ };
1593
1662
  }
1594
1663
  async readIndex(paths) {
1595
1664
  const out = /* @__PURE__ */ new Map();
@@ -1619,8 +1688,22 @@ var CodexSessionService = class {
1619
1688
  }
1620
1689
  parseDetail(raw) {
1621
1690
  const messages = [];
1691
+ const subagentRefs = [];
1622
1692
  let skipped = 0;
1623
1693
  let currentAssistant;
1694
+ let pendingReasoning = "";
1695
+ const ensureAssistant = (timestamp) => {
1696
+ if (!currentAssistant) {
1697
+ currentAssistant = {
1698
+ type: "assistant",
1699
+ role: "assistant",
1700
+ timestamp: timestamp ? String(timestamp) : void 0,
1701
+ blocks: []
1702
+ };
1703
+ messages.push(currentAssistant);
1704
+ }
1705
+ return currentAssistant;
1706
+ };
1624
1707
  for (const line of raw.split("\n")) {
1625
1708
  if (!line.trim()) continue;
1626
1709
  let obj;
@@ -1631,6 +1714,30 @@ var CodexSessionService = class {
1631
1714
  continue;
1632
1715
  }
1633
1716
  const payload = payloadOf(obj);
1717
+ if (payload.type === "agent_reasoning") {
1718
+ const text = String(payload.text ?? "").trim();
1719
+ if (text) pendingReasoning = text;
1720
+ continue;
1721
+ }
1722
+ if (payload.type === "reasoning") {
1723
+ const text = pendingReasoning || summaryText(payload.summary);
1724
+ pendingReasoning = "";
1725
+ ensureAssistant(obj.timestamp).blocks.push({
1726
+ kind: "thinking",
1727
+ text: truncate2(text)
1728
+ });
1729
+ continue;
1730
+ }
1731
+ if (payload.type === "sub_agent_activity") {
1732
+ if (payload.kind === "started" && payload.event_id && payload.agent_thread_id) {
1733
+ subagentRefs.push({
1734
+ toolUseId: String(payload.event_id),
1735
+ threadId: String(payload.agent_thread_id),
1736
+ agentPath: String(payload.agent_path ?? "")
1737
+ });
1738
+ }
1739
+ continue;
1740
+ }
1634
1741
  if (payload.type === "message") {
1635
1742
  const role = String(payload.role ?? "");
1636
1743
  if (role !== "user" && role !== "assistant") continue;
@@ -1645,45 +1752,26 @@ var CodexSessionService = class {
1645
1752
  currentAssistant = role === "assistant" ? msg : void 0;
1646
1753
  continue;
1647
1754
  }
1648
- if (payload.type === "function_call") {
1649
- const target = currentAssistant ?? {
1650
- type: "assistant",
1651
- role: "assistant",
1652
- timestamp: obj.timestamp ? String(obj.timestamp) : void 0,
1653
- blocks: []
1654
- };
1655
- if (!currentAssistant) {
1656
- messages.push(target);
1657
- currentAssistant = target;
1658
- }
1659
- target.blocks.push({
1755
+ if (payload.type === "function_call" || payload.type === "custom_tool_call") {
1756
+ const args = payload.type === "custom_tool_call" ? payload.input : payload.arguments;
1757
+ ensureAssistant(obj.timestamp).blocks.push({
1660
1758
  kind: "tool_use",
1661
1759
  name: String(payload.name ?? "tool"),
1662
- text: truncate2(parseFunctionArguments(payload.arguments)),
1760
+ text: truncate2(parseFunctionArguments(args)),
1663
1761
  toolUseId: payload.call_id ? String(payload.call_id) : void 0
1664
1762
  });
1665
1763
  continue;
1666
1764
  }
1667
- if (payload.type === "function_call_output") {
1668
- const target = currentAssistant ?? {
1669
- type: "assistant",
1670
- role: "assistant",
1671
- timestamp: obj.timestamp ? String(obj.timestamp) : void 0,
1672
- blocks: []
1673
- };
1674
- if (!currentAssistant) {
1675
- messages.push(target);
1676
- currentAssistant = target;
1677
- }
1678
- target.blocks.push({
1765
+ if (payload.type === "function_call_output" || payload.type === "custom_tool_call_output") {
1766
+ ensureAssistant(obj.timestamp).blocks.push({
1679
1767
  kind: "tool_result",
1680
- text: truncate2(String(payload.output ?? "")),
1768
+ text: truncate2(outputText(payload.output)),
1681
1769
  isError: payload.is_error === true,
1682
1770
  toolUseId: payload.call_id ? String(payload.call_id) : void 0
1683
1771
  });
1684
1772
  }
1685
1773
  }
1686
- return { messages, skipped };
1774
+ return { messages, skipped, subagentRefs };
1687
1775
  }
1688
1776
  };
1689
1777
 
@@ -1825,7 +1913,7 @@ var AgentCommandService = class {
1825
1913
  };
1826
1914
 
1827
1915
  // src/core/sync-service.ts
1828
- import { cp as cp2, lstat, mkdir as mkdir5, readlink as readlink2, rm as rm3, rmdir, stat as stat8, symlink, unlink } from "fs/promises";
1916
+ import { cp as cp2, lstat, mkdir as mkdir5, readlink as readlink2, rm as rm3, rmdir, stat as stat9, symlink, unlink } from "fs/promises";
1829
1917
  import { dirname as dirname5, join as join11, resolve as resolve7 } from "path";
1830
1918
  var SyncService = class {
1831
1919
  agentsSkillsDir;
@@ -1879,7 +1967,7 @@ var SyncService = class {
1879
1967
  assertSafeName(name);
1880
1968
  const src = join11(this.agentsSkillsDir, name);
1881
1969
  try {
1882
- if (!(await stat8(src)).isDirectory()) throw new Error();
1970
+ if (!(await stat9(src)).isDirectory()) throw new Error();
1883
1971
  } catch {
1884
1972
  throw new Error(`\u672A\u627E\u5230\u516C\u5171 skill\u300C${name}\u300D`);
1885
1973
  }
@@ -1961,7 +2049,7 @@ async function main() {
1961
2049
  store,
1962
2050
  mcp: new McpService(store),
1963
2051
  skills: new SkillService(),
1964
- projects: new ProjectService(store, registryFile),
2052
+ projects: new ProjectService(store, registryFile, join12(home, ".claude.json")),
1965
2053
  memory: new MemoryService(),
1966
2054
  memoryFiles: new MemoryFilesService(),
1967
2055
  sessions: new SessionService(),
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "../chunk-HRPQYUJV.js";
3
+ } from "../chunk-LZFMCC5M.js";
4
4
  export {
5
5
  createApp
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccpanel",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "本地图形化管理 Claude Code 配置:MCP、Skills、子 Agent、Commands、配置文件、Memory、Sessions、Plugins",
5
5
  "type": "module",
6
6
  "repository": {
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.inset-0{top:0;right:0;bottom:0;left:0}.right-0{right:0}.top-0{top:0}.z-40{z-index:40}.mx-1{margin-left:.25rem;margin-right:.25rem}.mb-1{margin-bottom:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.h-40{height:10rem}.h-5{height:1.25rem}.h-full{height:100%}.min-h-0{min-height:0px}.min-h-\[2\.5rem\]{min-height:2.5rem}.min-h-screen{min-height:100vh}.w-2\/5{width:40%}.w-56{width:14rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-\[10rem\]{max-width:10rem}.max-w-\[14rem\]{max-width:14rem}.max-w-lg{max-width:32rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.cursor-pointer{cursor:pointer}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-600{--tw-border-opacity: 1;border-color:rgb(217 119 6 / var(--tw-border-opacity, 1))}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity, 1))}.border-emerald-200{--tw-border-opacity: 1;border-color:rgb(167 243 208 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-violet-200{--tw-border-opacity: 1;border-color:rgb(221 214 254 / var(--tw-border-opacity, 1))}.bg-amber-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-amber-600{--tw-bg-opacity: 1;background-color:rgb(217 119 6 / var(--tw-bg-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/30{background-color:#0000004d}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-emerald-100{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity, 1))}.bg-emerald-50{--tw-bg-opacity: 1;background-color:rgb(236 253 245 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-300{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.bg-violet-100{--tw-bg-opacity: 1;background-color:rgb(237 233 254 / var(--tw-bg-opacity, 1))}.bg-violet-50{--tw-bg-opacity: 1;background-color:rgb(245 243 255 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pl-3{padding-left:.75rem}.pr-2{padding-right:.5rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.normal-case{text-transform:none}.italic{font-style:italic}.text-amber-700{--tw-text-opacity: 1;color:rgb(180 83 9 / var(--tw-text-opacity, 1))}.text-amber-800{--tw-text-opacity: 1;color:rgb(146 64 14 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-blue-700{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.text-emerald-700{--tw-text-opacity: 1;color:rgb(4 120 87 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-slate-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1))}.text-violet-400{--tw-text-opacity: 1;color:rgb(167 139 250 / var(--tw-text-opacity, 1))}.text-violet-500{--tw-text-opacity: 1;color:rgb(139 92 246 / var(--tw-text-opacity, 1))}.text-violet-700{--tw-text-opacity: 1;color:rgb(109 40 217 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.line-through{text-decoration-line:line-through}.opacity-70{opacity:.7}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.hover\:text-red-600:hover{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:shadow:hover{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width: 640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-\[18rem_minmax\(0\,1fr\)\]{grid-template-columns:18rem minmax(0,1fr)}}